Implemented a TCP server for unit testing (#373)

This allows us to test activities and fragments that require JSON RPC calls.
The ApplicationTest for JSON method and notification calls is mainly added
as example and for testing the MockTcpServer and friends.
This commit is contained in:
Martijn Brekhof 2017-03-21 10:21:44 +01:00 committed by Synced Synapse
parent fcd1730784
commit a1cdedb93f
9 changed files with 1306 additions and 0 deletions

View File

@ -0,0 +1,207 @@
/*
* Copyright 2016 Martijn Brekhof. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.xbmc.kore.tests.jsonrpc.method;
import android.os.Handler;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.xbmc.kore.BuildConfig;
import org.xbmc.kore.host.HostInfo;
import org.xbmc.kore.jsonrpc.ApiCallback;
import org.xbmc.kore.jsonrpc.HostConnection;
import org.xbmc.kore.jsonrpc.type.ApplicationType;
import org.xbmc.kore.jsonrpc.type.GlobalType;
import org.xbmc.kore.testutils.tcpserver.MockTcpServer;
import org.xbmc.kore.testutils.tcpserver.handlers.ApplicationHandler;
import org.xbmc.kore.testutils.tcpserver.handlers.JSONConnectionHandlerManager;
import org.xbmc.kore.utils.RoboThreadRunner;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 23)
public class ApplicationTest {
private HostConnection hostConnection;
private MockTcpServer server;
private ApplicationHandler applicationHandler;
private JSONConnectionHandlerManager manager;
@Before
public void setup() throws Exception {
applicationHandler = new ApplicationHandler();
manager = new JSONConnectionHandlerManager();
manager.addHandler(applicationHandler);
server = new MockTcpServer(manager);
server.start();
HostInfo hostInfo = new HostInfo("TESTHOST", server.getHostName(), HostConnection.PROTOCOL_TCP,
HostInfo.DEFAULT_HTTP_PORT, server.getPort(), null, null, true,
HostInfo.DEFAULT_EVENT_SERVER_PORT,
false);
hostConnection = new HostConnection(hostInfo);
}
@After
public void tearDown() throws Exception {
server.shutdown();
hostConnection.disconnect();
}
@Test
public void getPropertiesTest() throws Exception {
org.xbmc.kore.jsonrpc.method.Application.GetProperties properties =
new org.xbmc.kore.jsonrpc.method.Application.GetProperties(org.xbmc.kore.jsonrpc.method.Application.GetProperties.MUTED);
applicationHandler.setMuted(true, false);
hostConnection.execute(properties, new ApiCallback<ApplicationType.PropertyValue>() {
@Override
public void onSuccess(ApplicationType.PropertyValue result) {
assertNotNull(result);
assertTrue(result.muted);
RoboThreadRunner.stop();
}
@Override
public void onError(int errorCode, String description) {
fail("errorCode="+errorCode+", description="+description);
RoboThreadRunner.stop();
}
}, new Handler());
assertTrue(RoboThreadRunner.run(10));
}
@Test
public void setMuteTrueTest() throws Exception {
applicationHandler.setMuted(false, false);
sendSetMute(true);
assertTrue(RoboThreadRunner.run(10));
}
@Test
public void setMuteFalseTest() throws Exception {
applicationHandler.setMuted(true, false);
sendSetMute(false);
assertTrue(RoboThreadRunner.run(10));
}
@Test
public void incrementVolumeTest() throws Exception {
org.xbmc.kore.jsonrpc.method.Application.SetVolume setVolume =
new org.xbmc.kore.jsonrpc.method.Application.SetVolume(GlobalType.IncrementDecrement.INCREMENT);
applicationHandler.setVolume(77, false);
hostConnection.execute(setVolume, new ApiCallback<Integer>() {
@Override
public void onSuccess(Integer result) {
assertNotNull(result);
assertTrue(result == 78);
RoboThreadRunner.stop();
}
@Override
public void onError(int errorCode, String description) {
fail("errorCode="+errorCode+", description="+description);
RoboThreadRunner.stop();
}
}, new Handler());
assertTrue(RoboThreadRunner.run(10));
}
@Test
public void decrementVolumeTest() throws Exception {
org.xbmc.kore.jsonrpc.method.Application.SetVolume setVolume =
new org.xbmc.kore.jsonrpc.method.Application.SetVolume(GlobalType.IncrementDecrement.DECREMENT);
applicationHandler.setVolume(77, false);
hostConnection.execute(setVolume, new ApiCallback<Integer>() {
@Override
public void onSuccess(Integer result) {
assertNotNull(result);
assertTrue(result == 76);
RoboThreadRunner.stop();
}
@Override
public void onError(int errorCode, String description) {
fail("errorCode="+errorCode+", description="+description);
RoboThreadRunner.stop();
}
}, new Handler());
assertTrue(RoboThreadRunner.run(10));
}
@Test
public void setVolumeTest() throws Exception {
org.xbmc.kore.jsonrpc.method.Application.SetVolume setVolume =
new org.xbmc.kore.jsonrpc.method.Application.SetVolume(83);
applicationHandler.setVolume(77, false);
hostConnection.execute(setVolume, new ApiCallback<Integer>() {
@Override
public void onSuccess(Integer result) {
assertNotNull(result);
assertTrue(result == 83);
RoboThreadRunner.stop();
}
@Override
public void onError(int errorCode, String description) {
fail("errorCode="+errorCode+", description="+description);
RoboThreadRunner.stop();
}
}, new Handler());
assertTrue(RoboThreadRunner.run(10));
}
/**
* Sends the SetMute method to toggle the mute state
* @throws InterruptedException
*/
private void sendSetMute(final boolean expectedMuteState) throws InterruptedException {
org.xbmc.kore.jsonrpc.method.Application.SetMute mute =
new org.xbmc.kore.jsonrpc.method.Application.SetMute();
hostConnection.execute(mute, new ApiCallback<Boolean>() {
@Override
public void onSuccess(Boolean result) {
assertTrue(result == expectedMuteState);
RoboThreadRunner.stop();
}
@Override
public void onError(int errorCode, String description) {
fail(description);
RoboThreadRunner.stop();
}
}, new Handler());
}
}

View File

@ -0,0 +1,169 @@
/*
* Copyright 2016 Martijn Brekhof. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.xbmc.kore.tests.jsonrpc.notifications;
import android.os.Handler;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLog;
import org.xbmc.kore.BuildConfig;
import org.xbmc.kore.host.HostInfo;
import org.xbmc.kore.jsonrpc.ApiCallback;
import org.xbmc.kore.jsonrpc.HostConnection;
import org.xbmc.kore.jsonrpc.notification.Application;
import org.xbmc.kore.testutils.tcpserver.MockTcpServer;
import org.xbmc.kore.testutils.tcpserver.handlers.ApplicationHandler;
import org.xbmc.kore.testutils.tcpserver.handlers.JSONConnectionHandlerManager;
import org.xbmc.kore.utils.RoboThreadRunner;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 23)
public class ApplicationTest {
private HostConnection hostConnection;
private MockTcpServer server;
private ApplicationHandler applicationHandler;
@Before
public void setup() throws Exception {
ShadowLog.stream = System.out;
applicationHandler = new ApplicationHandler();
JSONConnectionHandlerManager manager = new JSONConnectionHandlerManager();
manager.addHandler(applicationHandler);
server = new MockTcpServer(manager);
server.start();
HostInfo hostInfo = new HostInfo("TESTHOST", server.getHostName(), HostConnection.PROTOCOL_TCP,
HostInfo.DEFAULT_HTTP_PORT, server.getPort(), null, null, true,
HostInfo.DEFAULT_EVENT_SERVER_PORT, false);
hostConnection = new HostConnection(hostInfo);
}
@After
public void tearDown() throws Exception {
server.shutdown();
hostConnection.disconnect();
}
@Test
public void onVolumeChanged() throws InterruptedException {
HostConnection.ApplicationNotificationsObserver observer =
new HostConnection.ApplicationNotificationsObserver() {
@Override
public void onVolumeChanged(Application.OnVolumeChanged notification) {
RoboThreadRunner.stop();
assertTrue(notification.volume == 84);
}
};
hostConnection.registerApplicationNotificationsObserver(observer, new Handler());
applicationHandler.setVolume(82, false);
sendSetVolumeCommand(84);
assertTrue(RoboThreadRunner.run(10));
}
@Test
public void onVolumeChangedMuted() throws InterruptedException {
HostConnection.ApplicationNotificationsObserver observer =
new HostConnection.ApplicationNotificationsObserver() {
@Override
public void onVolumeChanged(Application.OnVolumeChanged notification) {
RoboThreadRunner.stop();
assertTrue(notification.muted);
}
};
hostConnection.registerApplicationNotificationsObserver(observer, new Handler());
applicationHandler.setMuted(false, false);
sendToggleMuteCommand();
assertTrue(RoboThreadRunner.run(10));
}
@Test
public void onVolumeChangedNotMuted() throws InterruptedException {
HostConnection.ApplicationNotificationsObserver observer =
new HostConnection.ApplicationNotificationsObserver() {
@Override
public void onVolumeChanged(Application.OnVolumeChanged notification) {
RoboThreadRunner.stop();
assertFalse(notification.muted);
}
};
hostConnection.registerApplicationNotificationsObserver(observer, new Handler());
applicationHandler.setMuted(true, false);
sendToggleMuteCommand();
assertTrue(RoboThreadRunner.run(10));
}
private void sendToggleMuteCommand() {
org.xbmc.kore.jsonrpc.method.Application.SetMute mute =
new org.xbmc.kore.jsonrpc.method.Application.SetMute();
hostConnection.execute(mute, new ApiCallback<Boolean>() {
@Override
public void onSuccess(Boolean result) {
}
@Override
public void onError(int errorCode, String description) {
RoboThreadRunner.stop();
fail("errorCode="+errorCode+", description="+description);
}
}, new Handler());
}
private void sendSetVolumeCommand(int volume) {
org.xbmc.kore.jsonrpc.method.Application.SetVolume setVolume =
new org.xbmc.kore.jsonrpc.method.Application.SetVolume(volume);
hostConnection.execute(setVolume, new ApiCallback<Integer>() {
@Override
public void onSuccess(Integer result) {
}
@Override
public void onError(int errorCode, String description) {
RoboThreadRunner.stop();
fail("errorCode="+errorCode+", description="+description);
}
}, new Handler());
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2016 Martijn Brekhof. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.xbmc.kore.utils;
import org.robolectric.Robolectric;
public class RoboThreadRunner {
private static boolean running;
/**
* Runs the thread until {@link #stop()} is called or timeout is exceeded.
* This call blocks until either {@link #stop()} is called or timeout is exceeded.
* @param timeoutSeconds timeout in seconds
* @return false if timeout exceeded, true otherwise
* @throws InterruptedException
*/
public static boolean run(long timeoutSeconds) throws InterruptedException {
running = true;
long timeout = timeoutSeconds * 100;
while ( running && ( timeout-- > 0 )) {
Robolectric.getForegroundThreadScheduler().advanceToLastPostedRunnable();
Thread.sleep(100);
}
return timeout > 0;
}
public static void stop() {
running = false;
}
}

View File

@ -0,0 +1,236 @@
/*
* Copyright 2016 Martijn Brekhof. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.xbmc.kore.testutils.tcpserver;
import com.squareup.okhttp.internal.Util;
import org.xbmc.kore.utils.LogUtils;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.Collections;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import javax.net.ServerSocketFactory;
public class MockTcpServer {
public static final String TAG = LogUtils.makeLogTag(MockTcpServer.class);
private ServerSocketFactory serverSocketFactory = ServerSocketFactory.getDefault();
private ServerSocket serverSocket;
private boolean started;
private ExecutorService executor;
private int port = -1;
private StringBuffer request;
private InetSocketAddress inetSocketAddress;
private final Set<Socket> openClientSockets =
Collections.newSetFromMap(new ConcurrentHashMap<Socket, Boolean>());
private final TcpServerConnectionHandler connectionHandler;
public interface TcpServerConnectionHandler {
/**
* Processes received input
* @param c character received
* @return id of associated response if any, -1 if more input is needed.
*/
void processInput(char c);
/**
* Gets the answer for this handler that should be returned to the server after input has been
* processed successfully
* @return answer or null if no answer is available
*/
String getResponse();
}
public MockTcpServer(TcpServerConnectionHandler handler) {
connectionHandler = handler;
}
/**
* Starts the server on localhost on a random free port
* @throws IOException
*/
public void start() throws IOException {
start(new InetSocketAddress(InetAddress.getByName("localhost"), 0));
}
/**
*
* @param inetSocketAddress set portnumber to 0 to select a random free port
* @throws IOException
*/
public void start(InetSocketAddress inetSocketAddress) throws IOException {
if (started) throw new IllegalStateException("start() already called");
started = true;
this.inetSocketAddress = inetSocketAddress;
serverSocket = serverSocketFactory.createServerSocket();
// Reuse port if not using a random port
serverSocket.setReuseAddress(inetSocketAddress.getPort() != 0);
serverSocket.bind(inetSocketAddress, 50);
executor = Executors.newCachedThreadPool(Util.threadFactory("MockTcpServer", false));
port = serverSocket.getLocalPort();
executor.execute(new Runnable() {
@Override
public void run() {
try {
acceptConnection();
} catch (Throwable e) {
LogUtils.LOGE(TAG, " failed unexpectedly: " + e);
}
// Release all sockets and all threads, even if any close fails.
Util.closeQuietly(serverSocket);
for (Iterator<Socket> s = openClientSockets.iterator(); s.hasNext(); ) {
Util.closeQuietly(s.next());
s.remove();
}
executor.shutdown();
}
private void acceptConnection() throws Exception {
while (true) {
Socket socket;
try {
socket = serverSocket.accept();
} catch (SocketException e) {
//Socket closed
return;
}
openClientSockets.add(socket);
serveConnection(socket);
}
}
});
}
public synchronized void shutdown() throws IOException {
if (!started) return;
if (serverSocket == null) throw new IllegalStateException("shutdown() before start()");
serverSocket.close();
// Await shutdown.
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
throw new IOException("Gave up waiting for executor to shut down");
}
} catch (InterruptedException e) {
throw new AssertionError();
}
}
/**
* Gets the local port of this server socket or -1 if it is not bound
* @return the local port this server is listening on.
*/
public int getPort() {
return port;
}
public String getHostName() {
if ( inetSocketAddress == null )
throw new RuntimeException("Must start server before getting hostname");
return inetSocketAddress.getHostName();
}
private void serveConnection(final Socket socket) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
handleInput();
} catch (IOException e) {
LogUtils.LOGW(TAG, "processing input from " + socket.getInetAddress() + " failed: " + e);
}
}
private void handleInput() throws IOException {
InputStreamReader in = new InputStreamReader(socket.getInputStream());
request = new StringBuffer();
int i;
while ((i = in.read()) != -1) {
request.append((char) i);
synchronized (connectionHandler) {
connectionHandler.processInput((char) i);
}
}
socket.close();
openClientSockets.remove(socket);
}
});
executor.execute(new Runnable() {
@Override
public void run() {
try {
while (true) {
sendResponse();
Thread.sleep(1000);
if ( serverSocket.isClosed() )
return;
}
} catch (IOException e) {
LogUtils.LOGW(TAG, " sending response from " + socket.getInetAddress() + " failed: " + e);
} catch (InterruptedException e) {
LogUtils.LOGW(TAG, " wait interrupted" + e);
}
}
private void sendResponse() throws IOException {
PrintWriter out =
new PrintWriter(socket.getOutputStream(), true);
String answer = connectionHandler.getResponse();
if (answer != null) {
out.print(answer);
out.flush();
}
}
});
}
@Override
public String toString() {
return "MockTcpServer[" + port + "]";
}
}

View File

@ -0,0 +1,138 @@
/*
* Copyright 2016 Martijn Brekhof. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.xbmc.kore.testutils.tcpserver.handlers;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.xbmc.kore.jsonrpc.type.GlobalType;
import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonResponse;
import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.methods.Application;
import org.xbmc.kore.utils.LogUtils;
import java.util.ArrayList;
import static org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.notifications.Application.OnVolumeChanged;
/**
* Simulates Application JSON-RPC API
*/
public class ApplicationHandler implements JSONConnectionHandlerManager.ConnectionHandler {
private static final String TAG = LogUtils.makeLogTag(ApplicationHandler.class);
private boolean muted;
private int volume;
private static final String ID_NODE = "id";
private static final String PARAMS_NODE = "params";
private static final String PROPERTIES_NODE = "properties";
private ArrayList<JsonResponse> jsonNotifications = new ArrayList<>();
/**
* Sets the muted state and sends a notification
* @param muted
* @param notify true if OnVolumeChanged should be sent, false otherwise
*/
public void setMuted(boolean muted, boolean notify) {
this.muted = muted;
if (notify)
jsonNotifications.add(new OnVolumeChanged(muted, volume));
}
/**
* Sets the volume and sends a notification
* @param volume
* @param notify true if OnVolumeChanged should be sent, false otherwise
*/
public void setVolume(int volume, boolean notify) {
this.volume = volume;
if (notify)
jsonNotifications.add(new OnVolumeChanged(muted, volume));
}
@Override
public ArrayList<JsonResponse> getNotification() {
ArrayList<JsonResponse> jsonResponses = new ArrayList<>(jsonNotifications);
jsonNotifications.clear();
return jsonResponses;
}
@Override
public void reset() {
this.volume = 0;
this.muted = false;
}
@Override
public String[] getType() {
return new String[]{Application.GetProperties.METHOD_NAME,
Application.SetMute.METHOD_NAME,
Application.SetVolume.METHOD_NAME};
}
@Override
public ArrayList<JsonResponse> getResponse(String method, ObjectNode jsonRequest) {
ArrayList<JsonResponse> jsonResponses = new ArrayList<>();
int methodId = jsonRequest.get(ID_NODE).asInt(-1);
switch (method) {
case Application.GetProperties.METHOD_NAME:
Application.GetProperties response = new Application.GetProperties(methodId);
JsonNode jsonNode = jsonRequest.get(PARAMS_NODE).get(PROPERTIES_NODE);
for (JsonNode node : jsonNode) {
switch(node.asText()) {
case Application.GetProperties.MUTED:
response.addMuteState(muted);
break;
case Application.GetProperties.VOLUME:
response.addVolume(volume);
break;
}
}
jsonResponses.add(response);
break;
case Application.SetMute.METHOD_NAME:
setMuted(!muted, true);
jsonResponses.add(new Application.SetMute(methodId, muted));
break;
case Application.SetVolume.METHOD_NAME:
JsonNode params = jsonRequest.get(PARAMS_NODE);
String value = params.get("volume").asText();
switch (value) {
case GlobalType.IncrementDecrement.INCREMENT:
setVolume(volume + 1, true);
break;
case GlobalType.IncrementDecrement.DECREMENT:
setVolume(volume - 1, true);
break;
default:
setVolume(Integer.parseInt(value), true);
break;
}
jsonResponses.add(new Application.SetVolume(methodId, volume));
break;
default:
LogUtils.LOGD(TAG, "method: " + method + ", not implemented");
}
return jsonResponses;
}
}

View File

@ -0,0 +1,171 @@
/*
* Copyright 2016 Martijn Brekhof. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.xbmc.kore.testutils.tcpserver.handlers;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.xbmc.kore.testutils.tcpserver.MockTcpServer;
import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonResponse;
import org.xbmc.kore.utils.LogUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.TimeoutException;
import static org.xbmc.kore.jsonrpc.ApiMethod.ID_NODE;
import static org.xbmc.kore.jsonrpc.ApiMethod.METHOD_NODE;
public class JSONConnectionHandlerManager implements MockTcpServer.TcpServerConnectionHandler {
public static final String TAG = LogUtils.makeLogTag(JSONConnectionHandlerManager.class);
private HashMap<String, ConnectionHandler> handlersByType = new HashMap<>();
private HashSet<ConnectionHandler> handlers = new HashSet<>();
private StringBuffer buffer = new StringBuffer();
private int amountOfOpenBrackets = 0;
private final ObjectMapper objectMapper = new ObjectMapper();
private int responseCount;
private HashMap<String, ArrayList<JsonResponse>> clientResponses = new HashMap<>();
public interface ConnectionHandler {
/**
* Used to determine which methods the handler implements
* @return list of JSON method names
*/
String[] getType();
/**
* Used to get the response from a handler implementing the requested
* method.
* @param method requested method
* @param jsonRequest json node containing the original request
* @return {@link JsonResponse} that should be sent to the client
*/
ArrayList<JsonResponse> getResponse(String method, ObjectNode jsonRequest);
/**
* Used to get any notifications from the handler.
* @return {@link JsonResponse} that should be sent to the client
*/
ArrayList<JsonResponse> getNotification();
/**
* Should set the state of the handler to its initial state
*/
void reset();
}
public void addHandler(ConnectionHandler handler) throws Exception {
for(String type : handler.getType()) {
handlersByType.put(type, handler);
}
handlers.add(handler);
}
@Override
public void processInput(char c) {
buffer.append(c);
if ( c == '{' ) {
amountOfOpenBrackets++;
} else if ( c == '}' ) {
amountOfOpenBrackets--;
}
if ( amountOfOpenBrackets == 0 ) {
String input = buffer.toString();
buffer = new StringBuffer();
processJSONInput(input);
}
}
private void processJSONInput(String input) {
try {
JsonParser parser = objectMapper.getFactory().createParser(input);
ObjectNode jsonRequest = objectMapper.readTree(parser);
int methodId = jsonRequest.get(ID_NODE).asInt();
String method = jsonRequest.get(METHOD_NODE).asText();
ConnectionHandler connectionHandler = handlersByType.get(method);
if ( connectionHandler != null ) {
ArrayList<JsonResponse> responses = connectionHandler.getResponse(method, jsonRequest);
if (responses != null) {
addResponse(methodId, responses);
}
}
} catch (IOException e) {
LogUtils.LOGE(getClass().getSimpleName(), e.getMessage());
}
}
@Override
public String getResponse() {
StringBuffer stringBuffer = new StringBuffer();
//Handle responses
Collection<ArrayList<JsonResponse>> jsonResponses = clientResponses.values();
for(ArrayList<JsonResponse> arrayList : jsonResponses) {
for (JsonResponse response : arrayList) {
stringBuffer.append(response.toJsonString() + "\n");
}
}
clientResponses.clear();
//Handle notifications
for(ConnectionHandler handler : handlers) {
ArrayList<JsonResponse> jsonNotifications = handler.getNotification();
if (jsonNotifications != null) {
for (JsonResponse jsonResponse : jsonNotifications) {
stringBuffer.append(jsonResponse.toJsonString() +"\n");
}
}
}
responseCount++;
return stringBuffer.toString();
}
/**
* Waits until at least one response has been processed before returning
*/
public void waitForNextResponse(long timeOutMillis) throws TimeoutException {
responseCount = 0;
while ((responseCount < 2) && (timeOutMillis > 0)) {
try {
Thread.sleep(500);
timeOutMillis -= 500;
} catch (InterruptedException e) {
}
}
if (timeOutMillis < 0)
throw new TimeoutException();
}
private void addResponse(int id, ArrayList<JsonResponse> jsonResponses) {
ArrayList<JsonResponse> responses = clientResponses.get(String.valueOf(id));
if (responses == null) {
responses = new ArrayList<>();
clientResponses.put(String.valueOf(id), responses);
}
responses.addAll(jsonResponses);
}
}

View File

@ -0,0 +1,199 @@
/*
* Copyright 2016 Martijn Brekhof. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.xbmc.kore.utils.LogUtils;
public abstract class JsonResponse {
private final String TAG = LogUtils.makeLogTag(JsonResponse.class);
private final ObjectNode jsonResponse;
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final String RESULT_NODE = "result";
private static final String PARAMS_NODE = "params";
private static final String METHOD_NODE = "method";
private static final String DATA_NODE = "data";
private static final String ID_NODE = "id";
private static final String JSONRPC_NODE = "jsonrpc";
public enum TYPE {
OBJECT,
ARRAY
};
public JsonResponse() {
jsonResponse = objectMapper.createObjectNode();
jsonResponse.put(JSONRPC_NODE, "2.0");
}
public JsonResponse(int id) {
this();
jsonResponse.put(ID_NODE, id);
}
protected ObjectNode createObjectNode() {
return objectMapper.createObjectNode();
}
protected ArrayNode createArrayNode() {
return objectMapper.createArrayNode();
}
/**
* Returns the node used to hold the result. First call will create the
* result node for the given type
* @param type that result node should be when first created
* @return result node
*/
protected JsonNode getResultNode(TYPE type) {
JsonNode result;
if(jsonResponse.has(RESULT_NODE)) {
result = jsonResponse.get(RESULT_NODE);
if( result.isArray() && type != TYPE.ARRAY ) {
LogUtils.LOGE(TAG, "requested result node of type Object but response contains result node of type Array");
return null;
}
} else {
switch (type) {
case ARRAY:
result = objectMapper.createArrayNode();
break;
case OBJECT:
default:
result = objectMapper.createObjectNode();
break;
}
jsonResponse.set(RESULT_NODE, result);
}
return result;
}
/**
* Returns the parameters node of the json request object
* Creates one if necessary
* @return Parameters node
*/
private ObjectNode getParametersNode() {
ObjectNode params;
if (jsonResponse.has(PARAMS_NODE)) {
params = (ObjectNode)jsonResponse.get(PARAMS_NODE);
} else {
params = objectMapper.createObjectNode();
jsonResponse.set(PARAMS_NODE, params);
}
return params;
}
private ObjectNode getDataNode() {
ObjectNode data = null;
if (jsonResponse.has(PARAMS_NODE)) {
ObjectNode params = (ObjectNode)jsonResponse.get(PARAMS_NODE);
if(params.has(DATA_NODE)) {
data = (ObjectNode) params.get(DATA_NODE);
}
}
if ( data == null ) {
data = objectMapper.createObjectNode();
ObjectNode params = getParametersNode();
params.set(DATA_NODE, data);
}
return data;
}
protected void setResultToResponse(boolean value) {
jsonResponse.put(RESULT_NODE, value);
}
protected void setResultToResponse(int value) {
jsonResponse.put(RESULT_NODE, value);
}
protected void setResultToResponse(String value) {
jsonResponse.put(RESULT_NODE, value);
}
/**
* Adds the value to the array in node with the given key.
* If the array does not exist it will be created
* and added.
* @param node ObjectNode that should contain an entry with key with an array as value
* @param key the key of the item in ObjectNode that should hold the array
* @param value the value to be added to the array
*/
protected void addToArrayNode(ObjectNode node, String key, String value) {
JsonNode jsonNode = node.get(key);
if (jsonNode == null) {
jsonNode = objectMapper.createArrayNode();
node.set(key, jsonNode);
}
if (jsonNode.isArray()) {
((ArrayNode) jsonNode).add(value);
} else {
LogUtils.LOGE(TAG, "JsonNode at key: " + key + " not of type ArrayNode." );
}
}
protected void addToArrayNode(ObjectNode node, String key, ObjectNode value) {
JsonNode jsonNode = node.get(key);
if (jsonNode == null) {
jsonNode = objectMapper.createArrayNode();
node.set(key, jsonNode);
}
if (jsonNode.isArray()) {
((ArrayNode) jsonNode).add(value);
} else {
LogUtils.LOGE(TAG, "JsonNode at key: " + key + " not of type ArrayNode." );
}
}
protected void addDataToResponse(String parameter, boolean value) {
getDataNode().put(parameter, value);
}
protected void addDataToResponse(String parameter, int value) {
getDataNode().put(parameter, value);
}
protected void addParameterToResponse(String parameter, String value) {
getParametersNode().put(parameter, value);
}
protected void addMethodToResponse(String method) {
jsonResponse.put(METHOD_NODE, method);
}
public ObjectNode getResponseNode() {
return jsonResponse;
}
public String toJsonString() {
return jsonResponse.toString();
}
}

View File

@ -0,0 +1,99 @@
/*
* Copyright 2016 Martijn Brekhof. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.methods;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonResponse;
/**
* Serverside JSON RPC responses in Application.*
*/
public class Application {
/**
* JSON response for Application.SetMute request
*
* Example:
* Query: {"jsonrpc":"2.0","method":"Application.SetMute","id":1,"params":{"mute":"toggle"}}
* Answer: muted: {"id":1,"jsonrpc":"2.0","result":false}
* not muted: {"id":1,"jsonrpc":"2.0","result":true}
*
* @return JSON string
*/
public static class SetMute extends JsonResponse {
public final static String METHOD_NAME = "Application.SetMute";
public SetMute(int id, boolean muteState) {
super(id);
setResultToResponse(muteState);
}
}
/**
* JSON response for GetProperties requests
*
* Example:
* Query: {"jsonrpc":"2.0","method":"Application.GetProperties","id":1,"params":{"properties":["muted"]}}
* Answer: {"id":1,"jsonrpc":"2.0","result":{"muted":true}}
*
* @return JSON string
*/
public static class GetProperties extends JsonResponse {
public final static String METHOD_NAME = "Application.GetProperties";
public final static String MUTED = "muted";
public final static String VOLUME = "volume";
private ObjectNode node = null;
public GetProperties(int id) {
super(id);
}
public void addMuteState(boolean muteState) {
node = (ObjectNode) getResultNode(TYPE.OBJECT);
node.put(MUTED, muteState);
}
public void addVolume(int volume) {
node = (ObjectNode) getResultNode(TYPE.OBJECT);
node.put(VOLUME, volume);
}
}
/**
* JSON response for Application.SetVolume request
*
* Examples:
* Query: {"jsonrpc":"2.0","method":"Application.SetVolume","id":1,"params":{"volume":100}}
* Answer: {"id":1,"jsonrpc":"2.0","result":100}
*
* Query: {"jsonrpc":"2.0","method":"Application.SetVolume","id":1,"params":{"volume":"decrement"}}
* Answer: {"id":1,"jsonrpc":"2.0","result":99}
*
* @return JSON string
*/
public static class SetVolume extends JsonResponse {
public final static String METHOD_NAME = "Application.SetVolume";
public SetVolume(int id, int volume) {
super(id);
setResultToResponse(volume);
}
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright 2016 Martijn Brekhof. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.notifications;
import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonResponse;
public class Application {
/**
* JSON response for Application.OnVolumeChanged notification
*
* Example:
* Answer: {"jsonrpc":"2.0","method":"Application.OnVolumeChanged","params":{"data":{"muted":false,"volume":100},"sender":"xbmc"}}
*
* @return JSON string
*/
public static class OnVolumeChanged extends JsonResponse {
public final static String METHOD_NAME = "Application.OnVolumeChanged";
public OnVolumeChanged(boolean muteState, int volume) {
super();
addMethodToResponse(METHOD_NAME);
addDataToResponse("volume", volume);
addDataToResponse("muted", muteState);
addParameterToResponse("sender", "xbmc");
}
}
}