diff --git a/.classpath b/.classpath index 381bcf0..0a893cb 100644 --- a/.classpath +++ b/.classpath @@ -10,5 +10,9 @@ + + + + diff --git a/build.xml b/build.xml index 76fd29a..e1f45ee 100644 --- a/build.xml +++ b/build.xml @@ -14,7 +14,7 @@ - + diff --git a/required_libraries/CTRE_Phoenix-sources.jar b/required_libraries/CTRE_Phoenix-sources.jar new file mode 100644 index 0000000..12cb27d Binary files /dev/null and b/required_libraries/CTRE_Phoenix-sources.jar differ diff --git a/required_libraries/CTRE_Phoenix.jar b/required_libraries/CTRE_Phoenix.jar new file mode 100644 index 0000000..0c79a61 Binary files /dev/null and b/required_libraries/CTRE_Phoenix.jar differ diff --git a/required_libraries/annotations-12.0.jar b/required_libraries/annotations-12.0.jar new file mode 100644 index 0000000..7f8b362 Binary files /dev/null and b/required_libraries/annotations-12.0.jar differ diff --git a/required_libraries/jackson-annotations-2.9.6.jar b/required_libraries/jackson-annotations-2.9.6.jar new file mode 100644 index 0000000..51412a5 Binary files /dev/null and b/required_libraries/jackson-annotations-2.9.6.jar differ diff --git a/required_libraries/jackson-core-2.9.6.jar b/required_libraries/jackson-core-2.9.6.jar new file mode 100644 index 0000000..09e7dd2 Binary files /dev/null and b/required_libraries/jackson-core-2.9.6.jar differ diff --git a/required_libraries/jackson-databind-2.9.6.jar b/required_libraries/jackson-databind-2.9.6.jar new file mode 100644 index 0000000..e8eb658 Binary files /dev/null and b/required_libraries/jackson-databind-2.9.6.jar differ diff --git a/required_libraries/libCTRE_PhoenixCCI.so b/required_libraries/libCTRE_PhoenixCCI.so new file mode 100644 index 0000000..3303809 Binary files /dev/null and b/required_libraries/libCTRE_PhoenixCCI.so differ diff --git a/required_libraries/navx_frc.jar b/required_libraries/navx_frc.jar new file mode 100644 index 0000000..ea595d7 Binary files /dev/null and b/required_libraries/navx_frc.jar differ diff --git a/src/com/github/arteam/simplejsonrpc/client/JsonRpcClient.java b/src/com/github/arteam/simplejsonrpc/client/JsonRpcClient.java new file mode 100644 index 0000000..7bb00f7 --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/client/JsonRpcClient.java @@ -0,0 +1,152 @@ +package com.github.arteam.simplejsonrpc.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.arteam.simplejsonrpc.client.builder.BatchRequestBuilder; +import com.github.arteam.simplejsonrpc.client.builder.NotificationRequestBuilder; +import com.github.arteam.simplejsonrpc.client.builder.ObjectApiBuilder; +import com.github.arteam.simplejsonrpc.client.builder.RequestBuilder; +import com.github.arteam.simplejsonrpc.client.generator.IdGenerator; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Proxy; + +/** + * Date: 8/9/14 + * Time: 8:58 PM + *

+ * JSON-RPC client. Represents a factory for a fluent client API {@link com.github.arteam.simplejsonrpc.client.builder.RequestBuilder}. + * It's parametrized by {@link Transport} and Jackson {@link ObjectMapper} + * + * @author Artem Prigoda + */ +public class JsonRpcClient { + + /** + * Transport for performing JSON-RPC requests and returning responses + */ + @NotNull + private Transport transport; + + /** + * JSON mapper for conversion between JSON and Java types + */ + @NotNull + private ObjectMapper mapper; + + /** + * Constructs a new JSON-RPC client with a specified transport + * + * @param transport transport implementation + */ + public JsonRpcClient(@NotNull Transport transport) { + this(transport, new ObjectMapper()); + } + + /** + * Constructs a new JSON-RPC client with a specified transport and user-definder JSON mapper + * + * @param transport transport implementation + * @param mapper JSON mapper + */ + public JsonRpcClient(@NotNull Transport transport, @NotNull ObjectMapper mapper) { + this.transport = transport; + this.mapper = mapper; + } + + /** + * Creates a builder of a JSON-RPC request in initial state + * + * @return request builder + */ + @NotNull + public RequestBuilder createRequest() { + return new RequestBuilder(transport, mapper); + } + + /** + * Creates a builder of a JSON-RPC notification request in initial state + * + * @return notification request builder + */ + @NotNull + public NotificationRequestBuilder createNotification() { + return new NotificationRequestBuilder(transport, mapper); + } + + /** + * Creates a builder of a JSON-RPC batch request in initial state + * + * @return batch request builder + */ + @NotNull + public BatchRequestBuilder createBatchRequest() { + return new BatchRequestBuilder(transport, mapper); + } + + /** + * Creates a new proxy for accessing a remote JSON-RPC service through an interface + * + * @param clazz interface metadata + * @param interface type + * @return a new proxy + */ + @SuppressWarnings("unchecked") + @NotNull + public T onDemand(@NotNull Class clazz) { + return (T) Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{clazz}, + new ObjectApiBuilder(clazz, transport, mapper, null, null)); + } + + /** + * Creates a new proxy for accessing a remote JSON-RPC service through an interface + * with a custom id generator that overrides the interface generator. + * + * @param clazz interface metadata + * @param idGenerator custom id generator + * @param interface type + * @return a new proxy + */ + @SuppressWarnings("unchecked") + @NotNull + public T onDemand(@NotNull Class clazz, @NotNull IdGenerator idGenerator) { + return (T) Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{clazz}, + new ObjectApiBuilder(clazz, transport, mapper, null, idGenerator)); + } + + /** + * Creates a new proxy for accessing a remote JSON-RPC service through an interface + * with a custom type of request params. + * It applies for all methods and overrides interface and method level settings. + * + * @param clazz interface metadata + * @param paramsType custom type of request params + * @param interface type + * @return a new proxy + */ + @SuppressWarnings("unchecked") + @NotNull + public T onDemand(@NotNull Class clazz, @NotNull ParamsType paramsType) { + return (T) Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{clazz}, + new ObjectApiBuilder(clazz, transport, mapper, paramsType, null)); + } + + /** + * Creates a new proxy for accessing a remote JSON-RPC service through an interface + * with a custom id generator and custom type of request params. + * The generator overrides the interface generator. + * The type applies for all methods and overrides interface and method level settings. + * + * @param clazz interface metadata + * @param idGenerator custom id generator + * @param paramsType custom type of request params + * @param interface type + * @return a new proxy + */ + @SuppressWarnings("unchecked") + @NotNull + public T onDemand(Class clazz, @NotNull ParamsType paramsType, @NotNull IdGenerator idGenerator) { + return (T) Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{clazz}, + new ObjectApiBuilder(clazz, transport, mapper, paramsType, idGenerator)); + } + +} diff --git a/src/com/github/arteam/simplejsonrpc/client/JsonRpcId.java b/src/com/github/arteam/simplejsonrpc/client/JsonRpcId.java new file mode 100644 index 0000000..e31fd17 --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/client/JsonRpcId.java @@ -0,0 +1,21 @@ +package com.github.arteam.simplejsonrpc.client; + +import com.github.arteam.simplejsonrpc.client.generator.IdGenerator; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Date: 24.08.14 + * Time: 18:14 + * + * @author Artem Prigoda + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface JsonRpcId { + + Class> value(); +} diff --git a/src/com/github/arteam/simplejsonrpc/client/JsonRpcParams.java b/src/com/github/arteam/simplejsonrpc/client/JsonRpcParams.java new file mode 100644 index 0000000..2afa08b --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/client/JsonRpcParams.java @@ -0,0 +1,17 @@ +package com.github.arteam.simplejsonrpc.client; + +import java.lang.annotation.*; + +/** + * Date: 11/4/14 + * Time: 10:45 PM + * + * @author Artem Prigoda + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +@Documented +public @interface JsonRpcParams { + + ParamsType value() default ParamsType.MAP; +} diff --git a/src/com/github/arteam/simplejsonrpc/client/ParamsType.java b/src/com/github/arteam/simplejsonrpc/client/ParamsType.java new file mode 100644 index 0000000..758389e --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/client/ParamsType.java @@ -0,0 +1,12 @@ +package com.github.arteam.simplejsonrpc.client; + +/** + * Date: 11/4/14 + * Time: 10:14 PM + * Style of JSON-RPC parameters representation (map or array) + * + * @author Artem Prigoda + */ +public enum ParamsType { + MAP, ARRAY +} diff --git a/src/com/github/arteam/simplejsonrpc/client/Transport.java b/src/com/github/arteam/simplejsonrpc/client/Transport.java new file mode 100644 index 0000000..aa22343 --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/client/Transport.java @@ -0,0 +1,26 @@ +package com.github.arteam.simplejsonrpc.client; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; + +/** + * Date: 8/9/14 + * Time: 8:52 PM + *

+ * Abstract transport for JSON-RPC communication + * + * @author Artem Prigoda + */ +public interface Transport { + + /** + * Passes a JSON-RPC request in a text form to a backend and + * returns a JSON-RPC response in a text form as well + * + * @param request JSON-RPC request as a string + * @return JSON-RPC response as a string + */ + @NotNull + public String pass(@NotNull String request) throws IOException; +} diff --git a/src/com/github/arteam/simplejsonrpc/client/builder/AbstractBuilder.java b/src/com/github/arteam/simplejsonrpc/client/builder/AbstractBuilder.java new file mode 100644 index 0000000..035a4b3 --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/client/builder/AbstractBuilder.java @@ -0,0 +1,103 @@ +package com.github.arteam.simplejsonrpc.client.builder; + +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 com.fasterxml.jackson.databind.node.ValueNode; +import com.github.arteam.simplejsonrpc.client.Transport; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Date: 10/12/14 + * Time: 6:48 PM + * Abstract builder for JSON-RPC requests + * + * @author Artem Prigoda + */ +public class AbstractBuilder { + + // Protocol constants + protected static final String VERSION_2_0 = "2.0"; + protected static final String RESULT = "result"; + protected static final String ERROR = "error"; + protected static final String JSONRPC = "jsonrpc"; + protected static final String ID = "id"; + protected static final String METHOD = "method"; + protected static final String PARAMS = "params"; + + /** + * Transport for performing a text request and returning a text response + */ + @NotNull + protected final Transport transport; + + /** + * Jackson mapper for JSON processing + */ + @NotNull + protected final ObjectMapper mapper; + + public AbstractBuilder(@NotNull Transport transport, @NotNull ObjectMapper mapper) { + this.transport = transport; + this.mapper = mapper; + } + + /** + * Builds request params as a JSON array + * + * @param values request params + * @return a new JSON array + */ + @NotNull + protected ArrayNode arrayParams(@NotNull Object[] values) { + ArrayNode newArrayParams = mapper.createArrayNode(); + for (Object value : values) { + newArrayParams.add(mapper.valueToTree(value)); + } + return newArrayParams; + } + + /** + * Builds request params as a JSON object + * + * @param params request params + * @return a new JSON object + */ + @NotNull + protected ObjectNode objectParams(@NotNull Map params) { + ObjectNode objectNode = mapper.createObjectNode(); + for (String key : params.keySet()) { + objectNode.set(key, mapper.valueToTree(params.get(key))); + } + return objectNode; + } + + /** + * Creates a new JSON-RPC request as a JSON object + * + * @param id request id + * @param method request method + * @param params request params + * @return a new request as a JSON object + */ + @NotNull + protected ObjectNode request(@NotNull ValueNode id, @NotNull String method, + @NotNull JsonNode params) { + if (method.isEmpty()) { + throw new IllegalArgumentException("Method is not set"); + } + ObjectNode requestNode = mapper.createObjectNode(); + requestNode.put(JSONRPC, VERSION_2_0); + requestNode.put(METHOD, method); + requestNode.set(PARAMS, params); + if (!id.isNull()) { + requestNode.set(ID, id); + } + return requestNode; + } +} diff --git a/src/com/github/arteam/simplejsonrpc/client/builder/BatchRequestBuilder.java b/src/com/github/arteam/simplejsonrpc/client/builder/BatchRequestBuilder.java new file mode 100644 index 0000000..bdc1e9c --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/client/builder/BatchRequestBuilder.java @@ -0,0 +1,620 @@ +package com.github.arteam.simplejsonrpc.client.builder; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.*; +import com.fasterxml.jackson.databind.type.SimpleType; +import com.github.arteam.simplejsonrpc.client.Transport; +import com.github.arteam.simplejsonrpc.client.exception.JsonRpcBatchException; +import com.github.arteam.simplejsonrpc.core.domain.ErrorMessage; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Date: 10/12/14 + * Time: 6:23 PM + *

+ * Fluent builder of batch JSON-RPC requests. + *

+ * It provides facility to create a list of requests, set expected response types, execute the JSON-RPC request + * and process the JSON-RPC response. + *

+ * Return type is a map of responses (Java objects) by request ids. In cases of errors it throws + * {@code JsonRpcBatchException} with detailed status of success and failed requests. + *

+ * It delegates JSON processing to Jackson {@link ObjectMapper} and actual request performing to {@link com.github.arteam.simplejsonrpc.client.Transport}. + *

+ * The basic pattern is following: + *

+ * Map result = client.createBatchRequest()
+ *      .add("43121", "findByInitials", "Steven", "Stamkos")
+ *      .add("43122", "findByInitials", "Jack", "Allen")
+ *      .keysType(String.class)
+ *      .returnType(Player.class)
+ *      .execute();
+ * 
+ * @author Artem Prigoda + */ +public class BatchRequestBuilder extends AbstractBuilder { + + /** + * List of requests + */ + @NotNull + private final List requests; + + /** + * Map of expected return types by request ids + */ + @NotNull + private final Map returnTypes; + + /** + * Type of request ids + */ + @Nullable + private final Class keysType; + + /** + * Expected return type for all requests + *

+ * This property works exclusively with {@code returnTypes}. Only one of them should be set. + */ + @Nullable + private final JavaType returnType; + + /** + * Creates a new batch request builder in an initial state + * + * @param transport transport for request performing + * @param mapper mapper for JSON processing + */ + public BatchRequestBuilder(@NotNull Transport transport, @NotNull ObjectMapper mapper) { + this(transport, mapper, new ArrayList(), new HashMap(), null, null); + } + + /** + * Creates a new batch request builder as a part of a chain + * + * @param transport transport for request performing + * @param mapper mapper for JSON processing + * @param requests new requests + * @param returnTypes new return types + * @param keysType new key type + * @param returnType new values type + */ + public BatchRequestBuilder(@NotNull Transport transport, @NotNull ObjectMapper mapper, + @NotNull List requests, @NotNull Map returnTypes, + @Nullable Class keysType, @Nullable JavaType returnType) { + super(transport, mapper); + this.requests = requests; + this.returnTypes = returnTypes; + this.keysType = keysType; + this.returnType = returnType; + } + + /** + * Adds a new request without specifying a return type + * + * @param id request id as a long value + * @param method request method + * @param params request params as an array + * @return the current builder + */ + @NotNull + public BatchRequestBuilder add(long id, @NotNull String method, @NotNull Object... params) { + requests.add(request(new LongNode(id), method, arrayParams(params))); + return this; + } + + /** + * Adds a new request without specifying a return type + * + * @param id request id as an int value + * @param method request method + * @param params request params as an array + * @return the current builder + */ + @NotNull + public BatchRequestBuilder add(int id, @NotNull String method, @NotNull Object... params) { + requests.add(request(new IntNode(id), method, arrayParams(params))); + return this; + } + + /** + * Adds a new request without specifying a return type + * + * @param id request id as a text value + * @param method request method + * @param params request params as an array + * @return the current builder + */ + @NotNull + public BatchRequestBuilder add(String id, @NotNull String method, @NotNull Object... params) { + requests.add(request(new TextNode(id), method, arrayParams(params))); + return this; + } + + /** + * Adds a new notification request without specifying a return type + * + * @param method request method + * @param params request params as an array + * @return the current builder + */ + @NotNull + public BatchRequestBuilder add(@NotNull String method, @NotNull Object... params) { + requests.add(request(NullNode.instance, method, arrayParams(params))); + return this; + } + + /** + * Adds a new request without specifying a return type + * + * @param id request id as a long value + * @param method request method + * @param params request params as a map of parameter names to values + * @return the current builder + */ + @NotNull + public BatchRequestBuilder add(long id, @NotNull String method, @NotNull Map params) { + requests.add(request(new LongNode(id), method, objectParams(params))); + return this; + } + + /** + * Adds a new request without specifying a return type + * + * @param id request id as an int value + * @param method request method + * @param params request params as a map of parameter names to values + * @return the current builder + */ + @NotNull + public BatchRequestBuilder add(int id, @NotNull String method, @NotNull Map params) { + requests.add(request(new IntNode(id), method, objectParams(params))); + return this; + } + + /** + * Adds a new request without specifying a return type + * + * @param id request id as a text value + * @param method request method + * @param params request params as a map of parameter names to values + * @return the current builder + */ + @NotNull + public BatchRequestBuilder add(String id, @NotNull String method, @NotNull Map params) { + requests.add(request(new TextNode(id), method, objectParams(params))); + return this; + } + + /** + * Adds a new notification request without specifying a return type + * + * @param method request method + * @param params request params as an array + * @return the current builder + */ + @NotNull + public BatchRequestBuilder add(@NotNull String method, @NotNull Map params) { + requests.add(request(NullNode.instance, method, objectParams(params))); + return this; + } + + /** + * Adds a new request with a return type + * + * @param id request id as a long value + * @param method request method + * @param params request params as an array + * @param responseType expected response type + * @return the current builder + */ + @NotNull + public BatchRequestBuilder add(long id, @NotNull String method, @NotNull Object[] params, + @NotNull Class responseType) { + return add(id, method, params).returnType(id, responseType); + } + + /** + * Adds a new request with a return type + * + * @param id request id as an int value + * @param method request method + * @param params request params as an array + * @param responseType expected response type + * @return the current builder + */ + @NotNull + public BatchRequestBuilder add(int id, @NotNull String method, @NotNull Object[] params, + @NotNull Class responseType) { + return add(id, method, params).returnType(id, responseType); + } + + /** + * Adds a new request with a return type + * + * @param id request id as a text value + * @param method request method + * @param params request params as an array + * @param responseType expected response type + * @return the current builder + */ + @NotNull + public BatchRequestBuilder add(String id, @NotNull String method, @NotNull Object[] params, + @NotNull Class responseType) { + return add(id, method, params).returnType(id, responseType); + } + + /** + * Adds a new request with a return type + * + * @param id request id as a long value + * @param method request method + * @param params request params as a map of parameter names to values + * @param responseType expected response type + * @return the current builder + */ + @NotNull + public BatchRequestBuilder add(long id, @NotNull String method, @NotNull Map params, + @NotNull Class responseType) { + return add(id, method, params).returnType(id, responseType); + } + + /** + * Adds a new request with a return type + * + * @param id request id as an int value + * @param method request method + * @param params request params as a map of parameter names to values + * @param responseType expected response type + * @return the current builder + */ + @NotNull + public BatchRequestBuilder add(int id, @NotNull String method, @NotNull Map params, + @NotNull Class responseType) { + return add(id, method, params).returnType(id, responseType); + } + + /** + * Adds a new request with a return type + * + * @param id request id as a text value + * @param method request method + * @param params request params as a map of parameter names to values + * @param responseType expected response type + * @return the current builder + */ + @NotNull + public BatchRequestBuilder add(String id, @NotNull String method, @NotNull Map params, + @NotNull Class responseType) { + return add(id, method, params).returnType(id, responseType); + } + + /** + * Adds a new request with a complex return type + * + * @param id request id as a long value + * @param method request method + * @param params request params as an array + * @param typeReference expected complex response type + * @return the current builder + */ + @NotNull + public BatchRequestBuilder add(long id, @NotNull String method, @NotNull Object[] params, + @NotNull TypeReference typeReference) { + return add(id, method, params).returnType(id, typeReference); + } + + /** + * Adds a new request with a complex return type + * + * @param id request id as an int value + * @param method request method + * @param params request params as an array + * @param typeReference expected complex response type + * @return the current builder + */ + @NotNull + public BatchRequestBuilder add(int id, @NotNull String method, @NotNull Object[] params, + @NotNull TypeReference typeReference) { + return add(id, method, params).returnType(id, typeReference); + } + + /** + * Adds a new request with a complex return type + * + * @param id request id as a text value + * @param method request method + * @param params request params as an array + * @param typeReference expected complex response type + * @return the current builder + */ + @NotNull + public BatchRequestBuilder add(String id, @NotNull String method, @NotNull Object[] params, + @NotNull TypeReference typeReference) { + return add(id, method, params).returnType(id, typeReference); + } + + /** + * Adds a new request with a complex return type + * + * @param id request id as a long value + * @param method request method + * @param params request params as a map of parameter names to values + * @param typeReference expected complex response type + * @return the current builder + */ + @NotNull + public BatchRequestBuilder add(long id, @NotNull String method, @NotNull Map params, + @NotNull TypeReference typeReference) { + return add(id, method, params).returnType(id, typeReference); + } + + /** + * Adds a new request with a complex return type + * + * @param id request id as an int value + * @param method request method + * @param params request params as a map of parameter names to values + * @param typeReference expected complex response type + * @return the current builder + */ + @NotNull + public BatchRequestBuilder add(int id, @NotNull String method, @NotNull Map params, + @NotNull TypeReference typeReference) { + return add(id, method, params).returnType(id, typeReference); + } + + /** + * Adds a new request with a complex return type + * + * @param id request id as a text value + * @param method request method + * @param params request params as a map of parameter names to values + * @param typeReference expected complex response type + * @return the current builder + */ + @NotNull + public BatchRequestBuilder add(String id, @NotNull String method, @NotNull Map params, + @NotNull TypeReference typeReference) { + return add(id, method, params).returnType(id, typeReference); + } + + /** + * Sets an expected return type for a request + * + * @param id request id + * @param responseType expected response type + * @return a new builder + */ + private BatchRequestBuilder returnType(Object id, @NotNull Class responseType) { + returnTypes.put(id, SimpleType.construct(responseType)); + return this; + } + + /** + * Sets an expected return type as a complex type for a request + * + * @param id request id + * @param typeReference expected response type as a complex type + * @return a new builder + */ + private BatchRequestBuilder returnType(Object id, @NotNull TypeReference typeReference) { + returnTypes.put(id, mapper.getTypeFactory().constructType(typeReference.getType())); + return this; + } + + /** + * Sets type of request keys. + * The purpose of this method is providing static and runtime type safety of processing of batch responses + * + * @param keysClass type of keys + * @param type of keys + * @return a new builder + */ + public BatchRequestBuilder keysType(@NotNull Class keysClass) { + return new BatchRequestBuilder(transport, mapper, requests, returnTypes, keysClass, returnType); + } + + /** + * Sets an expected response type of requests. + * This method is preferred when requests have the same response type. + * + * @param valuesClass expected requests return type + * @param expected requests return type + * @return a new builder + */ + public BatchRequestBuilder returnType(@NotNull Class valuesClass) { + return new BatchRequestBuilder(transport, mapper, requests, returnTypes, keysType, + SimpleType.construct(valuesClass)); + } + + /** + * Sets an expected complex response type of requests. + * + * @param tr expected complex requests return type + * @param expected requests return type + * @return a new builder + */ + public BatchRequestBuilder returnType(@NotNull TypeReference tr) { + return new BatchRequestBuilder(transport, mapper, requests, returnTypes, keysType, + mapper.constructType(tr.getType())); + } + + /** + * Validates, executes the request and process response + * + * @return map of responses by request ids + */ + @NotNull + public Map execute() { + validateRequest(); + String textResponse = executeRequest(); + return processBatchResponse(textResponse); + } + + /** + * Validates the request as a valid batch JSON-RPC request + */ + private void validateRequest() { + if (requests.isEmpty()) { + throw new IllegalArgumentException("Requests are not set"); + } + + List requestIds = requestIds(); + if (returnType == null) { + for (Object id : requestIds) { + if (!returnTypes.containsKey(id)) { + throw new IllegalArgumentException("Return type isn't specified for " + + "request with id='" + id + "'"); + } + } + } else if (!returnTypes.isEmpty()) { + throw new IllegalArgumentException("Common and detailed configurations of return types shouldn't be mixed"); + } + + for (Object id : requestIds) { + checkIdType(id); + } + } + + /** + * Executes the request through the transport + * + * @return backend response as a string + */ + @NotNull + private String executeRequest() { + try { + return transport.pass(mapper.writeValueAsString(requests)); + } catch (IOException e) { + throw new IllegalStateException("I/O error during a request processing", e); + } + } + + /** + * Processes JSON-RPC batch response + * + * @param textResponse response as a string + * @return map of responses (Java objects) by request ids + */ + @NotNull + @SuppressWarnings("unchecked") + private Map processBatchResponse(@NotNull String textResponse) { + Map successes = new HashMap(); + Map errors = new HashMap(); + List requestIds = requestIds(); + + try { + JsonNode jsonResponses = mapper.readTree(textResponse); + // If it's an empty response + if (jsonResponses.isTextual() && jsonResponses.asText().isEmpty() && requestIds.isEmpty()) { + return new HashMap(); + } + // Not an array + if (jsonResponses.getNodeType() != JsonNodeType.ARRAY) { + throw new IllegalStateException("Expected array but was " + jsonResponses.getNodeType()); + } + + for (JsonNode responseNode : (ArrayNode) jsonResponses) { + processSingleResponse(responseNode, requestIds, successes, errors); + } + } catch (IOException e) { + throw new IllegalStateException("Unable parse a JSON response: " + textResponse, e); + } + if (!errors.isEmpty()) { + throw new JsonRpcBatchException("Errors happened during batch request processing", successes, errors); + } + return (Map) successes; + } + + private void processSingleResponse(@NotNull JsonNode responseNode, @NotNull List requestIds, + @NotNull Map successes, + @NotNull Map errors) + throws JsonProcessingException { + checkVersion(responseNode, responseNode.get(JSONRPC)); + + JsonNode result = responseNode.get(RESULT); + JsonNode error = responseNode.get(ERROR); + if (error == null && result == null) { + throw new IllegalStateException("Neither result or error is set in response: " + responseNode); + } + + // Check id and convert it to long if necessary + Object idValue = nodeValue(responseNode.get(ID)); + if (keysType == Long.class && idValue.getClass() == Integer.class) { + idValue = ((Integer) idValue).longValue(); + } + + if (!requestIds.contains(idValue)) { + throw new IllegalStateException("Unspecified id: '" + idValue + "' in response"); + } + + if (result != null) { + JavaType actualReturnType = returnType != null ? returnType : returnTypes.get(idValue); + successes.put(idValue, mapper.convertValue(result, actualReturnType)); + } else { + // Process as an error + errors.put(idValue, mapper.treeToValue(error, ErrorMessage.class)); + } + } + + private void checkVersion(JsonNode responseNode, JsonNode version) { + if (version == null) { + throw new IllegalStateException("Not a JSON-RPC response: " + responseNode); + } + if (!version.asText().equals(VERSION_2_0)) { + throw new IllegalStateException("Bad protocol version in a response: " + responseNode); + } + } + + private void checkIdType(@NotNull Object id) { + if (keysType != null && !keysType.equals(id.getClass())) { + throw new IllegalArgumentException("Id: '" + id + "' has wrong type: '" + id.getClass().getSimpleName() + + "'. Should be: '" + keysType.getSimpleName() + "'"); + } + } + + @NotNull + private List requestIds() { + List ids = new ArrayList(requests.size()); + for (ObjectNode request : requests) { + JsonNode id = request.get(ID); + if (id != null) { + ids.add(nodeValue(id)); + } + } + return ids; + } + + // Visible for tests + @NotNull + List getRequests() { + return requests; + } + + @NotNull + private static Object nodeValue(@NotNull JsonNode id) { + if (id.isLong()) { + return id.longValue(); + } else if (id.isInt()) { + return id.intValue(); + } else if (id.isTextual()) { + return id.textValue(); + } + throw new IllegalArgumentException("Wrong id=" + id); + } + +} diff --git a/src/com/github/arteam/simplejsonrpc/client/builder/NotificationRequestBuilder.java b/src/com/github/arteam/simplejsonrpc/client/builder/NotificationRequestBuilder.java new file mode 100644 index 0000000..40c9afb --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/client/builder/NotificationRequestBuilder.java @@ -0,0 +1,86 @@ +package com.github.arteam.simplejsonrpc.client.builder; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.arteam.simplejsonrpc.client.Transport; +import org.jetbrains.annotations.NotNull; + +/** + * Date: 8/17/14 + * Time: 11:09 PM + *

+ * Type-safe builder of JSON-RPC notification requests. + *

+ * It uses underlying {@link RequestBuilder} to build a request, but not permits setting a request id. + * Also it doesn't expect any response from the server, so there is no response parsing. + * + * @author Artem Prigoda + */ +public class NotificationRequestBuilder { + + /** + * Delegated request builder + */ + private RequestBuilder requestBuilder; + + /** + * Creates a new notification request builder + * + * @param transport transport for request performing + * @param mapper mapper for JSON processing + */ + public NotificationRequestBuilder(@NotNull Transport transport, @NotNull ObjectMapper mapper) { + requestBuilder = new RequestBuilder(transport, mapper); + } + + /** + * Creates a new notification request builder as a chain of builders + * + * @param requestBuilder a new notification request builder + */ + private NotificationRequestBuilder(RequestBuilder requestBuilder) { + this.requestBuilder = requestBuilder; + } + + /** + * Sets a request method + * + * @param method a request method + * @return new builder + */ + @NotNull + public NotificationRequestBuilder method(@NotNull String method) { + return new NotificationRequestBuilder(requestBuilder.method(method)); + } + + /** + * Adds a new parameter to current request parameters. + * + * @param name parameter name + * @param value parameter value + * @return new builder + */ + @NotNull + public NotificationRequestBuilder param(@NotNull String name, @NotNull Object value) { + return new NotificationRequestBuilder(requestBuilder.param(name, value)); + } + + /** + * Sets request parameters to request parameters. + * Parameters are interpreted according to its positions. + * + * @param values array of parameters + * @return new builder + */ + @NotNull + public NotificationRequestBuilder params(@NotNull Object... values) { + return new NotificationRequestBuilder(requestBuilder.params(values)); + } + + /** + * Execute a request through {@link Transport} + */ + public void execute() { + requestBuilder.executeRequest(); + } + +} diff --git a/src/com/github/arteam/simplejsonrpc/client/builder/ObjectApiBuilder.java b/src/com/github/arteam/simplejsonrpc/client/builder/ObjectApiBuilder.java new file mode 100644 index 0000000..4067b30 --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/client/builder/ObjectApiBuilder.java @@ -0,0 +1,160 @@ +package com.github.arteam.simplejsonrpc.client.builder; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.*; +import com.github.arteam.simplejsonrpc.client.ParamsType; +import com.github.arteam.simplejsonrpc.client.Transport; +import com.github.arteam.simplejsonrpc.client.exception.JsonRpcException; +import com.github.arteam.simplejsonrpc.client.generator.IdGenerator; +import com.github.arteam.simplejsonrpc.client.metadata.ClassMetadata; +import com.github.arteam.simplejsonrpc.client.metadata.MethodMetadata; +import com.github.arteam.simplejsonrpc.client.metadata.ParameterMetadata; +import com.github.arteam.simplejsonrpc.core.domain.ErrorMessage; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; + +/** + * Date: 24.08.14 + * Time: 17:33 + * Proxy for accessing a remote JSON-RPC service trough an interface. + * + * @author Artem Prigoda + */ +public class ObjectApiBuilder extends AbstractBuilder implements InvocationHandler { + + @Nullable + private ParamsType userParamsType; + + @Nullable + private IdGenerator userIdGenerator; + + @NotNull + private ClassMetadata classMetadata; + + /** + * Crate a new proxy for an interface + * + * @param clazz service interface + * @param transport transport abstraction + * @param mapper json mapper + * @param userParamsType custom type of request params + * @param userIdGenerator custom id generator + */ + public ObjectApiBuilder(@NotNull Class clazz, @NotNull Transport transport, @NotNull ObjectMapper mapper, + @Nullable ParamsType userParamsType, @Nullable IdGenerator userIdGenerator) { + super(transport, mapper); + this.classMetadata = Reflections.getClassMetadata(clazz); + this.userParamsType = userParamsType; + this.userIdGenerator = userIdGenerator; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Check that it's a JSON-RPC method + MethodMetadata methodMetadata = classMetadata.getMethods().get(method); + if (methodMetadata == null) { + throw new IllegalStateException("Method '" + method.getName() + "' is not JSON-RPC available"); + } + + // Get method name (annotation or the actual name), params and id generator + String methodName = methodMetadata.getName(); + JsonNode params = getParams(methodMetadata, args, getParamsType(classMetadata, methodMetadata)); + IdGenerator idGenerator = userIdGenerator != null ? userIdGenerator : classMetadata.getIdGenerator(); + + // Construct a request + ValueNode id = new POJONode(idGenerator.generate()); + String textResponse = execute(request(id, methodName, params)); + + // Parse a response + JsonNode responseNode = mapper.readTree(textResponse); + JsonNode result = responseNode.get(RESULT); + JsonNode error = responseNode.get(ERROR); + if (result != null) { + JavaType returnType = mapper.getTypeFactory().constructType(method.getGenericReturnType()); + if (returnType.getRawClass() == void.class) { + return null; + } + return mapper.convertValue(result, returnType); + } else { + ErrorMessage errorMessage = mapper.treeToValue(error, ErrorMessage.class); + throw new JsonRpcException(errorMessage); + } + } + + /** + * Get request params in a JSON representation (map or array) + */ + @NotNull + private JsonNode getParams(@NotNull MethodMetadata method, @NotNull Object[] args, + @NotNull ParamsType paramsType) { + ObjectNode paramsAsMap = mapper.createObjectNode(); + ArrayNode paramsAsArray = mapper.createArrayNode(); + for (String paramName : method.getParams().keySet()) { + ParameterMetadata parameterMetadata = method.getParams().get(paramName); + int index = parameterMetadata.getIndex(); + JsonNode jsonArg = mapper.valueToTree(args[index]); + if (jsonArg == null || jsonArg == NullNode.instance) { + if (parameterMetadata.isOptional()) { + if (paramsType == ParamsType.ARRAY) { + paramsAsArray.add(NullNode.instance); + } + } else { + throw new IllegalArgumentException("Parameter '" + paramName + + "' of method '" + method.getName() + "' is mandatory and can't be null"); + } + } else { + if (paramsType == ParamsType.MAP) { + paramsAsMap.set(paramName, jsonArg); + } else if (paramsType == ParamsType.ARRAY) { + paramsAsArray.add(jsonArg); + } + } + } + return paramsType == ParamsType.MAP ? paramsAsMap : paramsAsArray; + } + + /** + * Execute a request on a remote service and return a textual representation of a response + * + * @param request json representation of a request + * @return service response as a string + */ + @NotNull + private String execute(@NotNull ObjectNode request) { + try { + return transport.pass(mapper.writeValueAsString(request)); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Unable convert " + request + " to JSON", e); + } catch (IOException e) { + throw new IllegalStateException("I/O error during request processing", e); + } + } + + /** + * Get style of params for a request. + * It could be either on a method, class or user level. MAP is a fallback choice as default. + * + * @param classMetadata metadata of a service interface + * @param methodMetadata metadata of a method + * @return type of params + */ + @NotNull + private ParamsType getParamsType(@NotNull ClassMetadata classMetadata, @NotNull MethodMetadata methodMetadata) { + if (userParamsType != null) { + return userParamsType; + } else if (methodMetadata.getParamsType() != null) { + return methodMetadata.getParamsType(); + } else if (classMetadata.getParamsType() != null) { + return classMetadata.getParamsType(); + } + return ParamsType.MAP; + } + +} diff --git a/src/com/github/arteam/simplejsonrpc/client/builder/Reflections.java b/src/com/github/arteam/simplejsonrpc/client/builder/Reflections.java new file mode 100644 index 0000000..7de5b46 --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/client/builder/Reflections.java @@ -0,0 +1,129 @@ +package com.github.arteam.simplejsonrpc.client.builder; + +import com.github.arteam.simplejsonrpc.client.JsonRpcId; +import com.github.arteam.simplejsonrpc.client.JsonRpcParams; +import com.github.arteam.simplejsonrpc.client.ParamsType; +import com.github.arteam.simplejsonrpc.client.generator.AtomicLongIdGenerator; +import com.github.arteam.simplejsonrpc.client.generator.IdGenerator; +import com.github.arteam.simplejsonrpc.client.metadata.ClassMetadata; +import com.github.arteam.simplejsonrpc.client.metadata.MethodMetadata; +import com.github.arteam.simplejsonrpc.client.metadata.ParameterMetadata; +import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcMethod; +import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcOptional; +import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcParam; +import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcService; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Date: 11/17/14 + * Time: 8:04 PM + * Utility class for gathering meta-information about client proxies through reflection + * + * @author Artem Prigoda + */ +class Reflections { + + private Reflections() { + } + + /** + * Gets remote service interface metadata + * + * @param clazz an interface for representing a remote service + * @return class metadata + */ + @NotNull + public static ClassMetadata getClassMetadata(@NotNull Class clazz) { + Map methodsMetadata = new HashMap(32); + Class searchClass = clazz; + while (searchClass != null) { + JsonRpcService rpcServiceAnn = getAnnotation(searchClass.getAnnotations(), JsonRpcService.class); + if (rpcServiceAnn == null) { + throw new IllegalStateException("Class '" + clazz.getCanonicalName() + + "' is not annotated as @JsonRpcService"); + } + Method[] methods = searchClass.getMethods(); + for (Method method : methods) { + Annotation[] methodAnnotations = method.getDeclaredAnnotations(); + JsonRpcMethod rpcMethodAnn = getAnnotation(methodAnnotations, JsonRpcMethod.class); + if (rpcMethodAnn == null) { + throw new IllegalStateException("Method '" + method.getName() + "' is not annotated as @JsonRpcMethod"); + } + + // LinkedHashMap is needed to support method parameter ordering + Map paramsMetadata = new LinkedHashMap(8); + Annotation[][] parametersAnnotations = method.getParameterAnnotations(); + for (int i = 0; i < parametersAnnotations.length; i++) { + Annotation[] parametersAnnotation = parametersAnnotations[i]; + // Check that it's a JSON-RPC param + JsonRpcParam rpcParamAnn = getAnnotation(parametersAnnotation, JsonRpcParam.class); + if (rpcParamAnn == null) { + throw new IllegalStateException("Parameter with index=" + i + " of method '" + method.getName() + + "' is not annotated with @JsonRpcParam"); + } + // Check that's a param could be an optional + JsonRpcOptional optionalAnn = getAnnotation(parametersAnnotation, JsonRpcOptional.class); + ParameterMetadata parameterMetadata = new ParameterMetadata(i, optionalAnn != null); + if (paramsMetadata.put(rpcParamAnn.value(), parameterMetadata) != null) { + throw new IllegalStateException("Two parameters of method '" + method.getName() + "' have the " + + "same name '" + rpcParamAnn.value() + "'"); + } + + } + String name = !rpcMethodAnn.value().isEmpty() ? rpcMethodAnn.value() : method.getName(); + ParamsType paramsType = getParamsType(methodAnnotations); + methodsMetadata.put(method, new MethodMetadata(name, paramsType, paramsMetadata)); + } + searchClass = searchClass.getSuperclass(); + } + + Annotation[] classAnnotations = clazz.getDeclaredAnnotations(); + IdGenerator idGenerator = getIdGenerator(classAnnotations); + ParamsType paramsType = getParamsType(classAnnotations); + return new ClassMetadata(paramsType, idGenerator, methodsMetadata); + } + + + /** + * Get an actual id generator + */ + @NotNull + private static IdGenerator getIdGenerator(@NotNull Annotation[] classAnnotations) { + JsonRpcId jsonRpcIdAnn = getAnnotation(classAnnotations, JsonRpcId.class); + Class> idGeneratorClazz = (jsonRpcIdAnn == null) ? + AtomicLongIdGenerator.class : jsonRpcIdAnn.value(); + try { + return idGeneratorClazz.newInstance(); + } catch (Exception e) { + throw new IllegalStateException("Unable instantiate id generator: " + idGeneratorClazz, e); + } + } + + @Nullable + private static ParamsType getParamsType(@NotNull Annotation[] annotations) { + JsonRpcParams rpcParamsAnn = getAnnotation(annotations, JsonRpcParams.class); + return rpcParamsAnn != null ? rpcParamsAnn.value() : null; + + } + + @SuppressWarnings("unchecked") + @Nullable + private static T getAnnotation(@Nullable Annotation[] annotations, + @NotNull Class clazz) { + if (annotations != null) { + for (Annotation annotation : annotations) { + if (annotation.annotationType().equals(clazz)) { + return (T) annotation; + } + } + } + return null; + } +} diff --git a/src/com/github/arteam/simplejsonrpc/client/builder/RequestBuilder.java b/src/com/github/arteam/simplejsonrpc/client/builder/RequestBuilder.java new file mode 100644 index 0000000..c151dab --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/client/builder/RequestBuilder.java @@ -0,0 +1,381 @@ +package com.github.arteam.simplejsonrpc.client.builder; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.*; +import com.fasterxml.jackson.databind.type.SimpleType; +import com.github.arteam.simplejsonrpc.client.Transport; +import com.github.arteam.simplejsonrpc.client.exception.JsonRpcException; +import com.github.arteam.simplejsonrpc.core.domain.ErrorMessage; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Date: 8/9/14 + * Time: 9:04 PM + *

+ * Type-safe builder of JSON-RPC requests. + *

+ * It introduces fluent API to build a request, set an expected response type and perform the request. + * Builder is immutable: every mutation creates a new object, so it's safe to use in multi-threaded environment. + *

+ * It delegates JSON processing to Jackson {@link ObjectMapper} and actual request performing to {@link com.github.arteam.simplejsonrpc.client.Transport}. + * + * @author Artem Prigoda + */ +public class RequestBuilder extends AbstractBuilder { + + /** + * JSON-RPC request method + */ + @NotNull + private final String method; + + /** + * JSON-RPC request id + */ + @NotNull + private final ValueNode id; + + /** + * JSON-RPC request params as a map + */ + @NotNull + private final ObjectNode objectParams; + + /** + * JSON-RPC request params as an array + */ + @NotNull + private final ArrayNode arrayParams; + + /** + * Generic type for representing expected response type + */ + @NotNull + private final JavaType javaType; + + /** + * Creates a new default request builder without actual parameters + * + * @param transport transport for request performing + * @param mapper mapper for JSON processing + */ + public RequestBuilder(@NotNull Transport transport, @NotNull ObjectMapper mapper) { + super(transport, mapper); + id = NullNode.instance; + objectParams = mapper.createObjectNode(); + arrayParams = mapper.createArrayNode(); + method = ""; + javaType = SimpleType.construct(Object.class); + } + + /** + * Creates new builder as part of a chain of builders to a full-initialized type-safe builder + * + * @param transport new transport + * @param mapper new mapper + * @param method new method + * @param id new id + * @param objectParams new object params + * @param arrayParams new array params + * @param javaType new response type + */ + private RequestBuilder(@NotNull Transport transport, @NotNull ObjectMapper mapper, @NotNull String method, + @NotNull ValueNode id, @NotNull ObjectNode objectParams, @NotNull ArrayNode arrayParams, + @NotNull JavaType javaType) { + super(transport, mapper); + this.method = method; + this.id = id; + this.objectParams = objectParams; + this.arrayParams = arrayParams; + this.javaType = javaType; + } + + /** + * Sets a request id as a long value + * + * @param id a request id + * @return new builder + */ + @NotNull + public RequestBuilder id(@NotNull Long id) { + return new RequestBuilder(transport, mapper, method, new LongNode(id), objectParams, arrayParams, javaType); + } + + /** + * Sets a request id as an integer value + * + * @param id a request id + * @return new builder + */ + @NotNull + public RequestBuilder id(@NotNull Integer id) { + return new RequestBuilder(transport, mapper, method, new IntNode(id), objectParams, arrayParams, javaType); + } + + /** + * Sets a request id as a string value + * + * @param id a request id + * @return new builder + */ + @NotNull + public RequestBuilder id(@NotNull String id) { + return new RequestBuilder(transport, mapper, method, new TextNode(id), objectParams, arrayParams, javaType); + } + + /** + * Sets a request method + * + * @param method a request method + * @return new builder + */ + @NotNull + public RequestBuilder method(@NotNull String method) { + return new RequestBuilder(transport, mapper, method, id, objectParams, arrayParams, javaType); + } + + /** + * Adds a new parameter to current request parameters. + *

+ * Caution: If you set request parameters this way, you should follow this convention + * during all the building process like that: + *

+     * client.createRequest()
+     *       .method("find")
+     *       .id(43121)
+     *       .param("firstName", "Steven")
+     *       .param("lastName", "Stamkos")
+     *       .returnAs(Player.class)
+     *       .execute();
+     * 
+ *

+ * Calls to params method are not permitted after this method has been invoked. + * + * @param name parameter name + * @param value parameter value + * @return new builder + */ + @NotNull + public RequestBuilder param(@NotNull String name, @NotNull Object value) { + ObjectNode newObjectParams = objectParams.deepCopy(); + newObjectParams.set(name, mapper.valueToTree(value)); + return new RequestBuilder(transport, mapper, method, id, newObjectParams, arrayParams, javaType); + } + + /** + * Sets request parameters to request parameters. + * Parameters are interpreted according to its positions. + * + * @param values array of parameters + * @return new builder + */ + @NotNull + public RequestBuilder params(@NotNull Object... values) { + return new RequestBuilder(transport, mapper, method, id, objectParams, arrayParams(values), javaType); + } + + /** + * Sets expected return type. This method is suitable for non-generic types + * + * @param responseType expected return type + * @param new return type + * @return new builder + */ + @NotNull + public RequestBuilder returnAs(@NotNull Class responseType) { + return new RequestBuilder(transport, mapper, method, id, objectParams, arrayParams, + SimpleType.construct(responseType)); + } + + /** + * Sets expected return type as a list of objects + * + * @param elementType type of elements of a list + * @param generic list type + * @return new builder + */ + @NotNull + public RequestBuilder> returnAsList(@NotNull Class elementType) { + return new RequestBuilder>(transport, mapper, method, id, objectParams, arrayParams, + mapper.getTypeFactory().constructCollectionType(List.class, elementType)); + } + + /** + * Sets expected return type as a set of objects + * + * @param elementType type of elements of a set + * @param generic set type + * @return new builder + */ + @NotNull + public RequestBuilder> returnAsSet(@NotNull Class elementType) { + return new RequestBuilder>(transport, mapper, method, id, objectParams, arrayParams, + mapper.getTypeFactory().constructCollectionType(Set.class, elementType)); + } + + /** + * Sets expected return type as a collection of objects. + * This method is suitable for non-standard collections like {@link java.util.Queue} + * + * @param elementType type of elements of a collection + * @param generic collection type + * @return new builder + */ + @NotNull + public RequestBuilder> returnAsCollection(@NotNull Class collectionType, + @NotNull Class elementType) { + return new RequestBuilder>(transport, mapper, method, id, objectParams, arrayParams, + mapper.getTypeFactory().constructCollectionType(collectionType, elementType)); + } + + /** + * Sets expected return type as an array + * + * @param elementType type of elements of an array + * @param generic array type + * @return new builder + */ + @NotNull + public RequestBuilder returnAsArray(@NotNull Class elementType) { + return new RequestBuilder(transport, mapper, method, id, objectParams, arrayParams, + mapper.getTypeFactory().constructArrayType(elementType)); + } + + /** + * Sets expected return type as a map of objects. + * Because JSON type system the map should have strings as keys. + * + * @param mapClass expected map interface or implementation, + * e.g. {@link java.util.Map}, {@link java.util.HashMap}. + * {@link java.util.LinkedHashMap}, {@link java.util.SortedMap} + * @param valueType map value type + * @param generic map value type + * @return new builder + */ + @NotNull + public RequestBuilder> returnAsMap(@NotNull Class mapClass, + @NotNull Class valueType) { + return new RequestBuilder>(transport, mapper, method, id, objectParams, arrayParams, + mapper.getTypeFactory().constructMapType(mapClass, String.class, valueType)); + } + + /** + * Sets expected return type as a generic type, e.g. Guava Optional. + * Generic type is set as a type reference like that: + *

 new TypeReference>() {} 
+ * + * @param tr type reference + * @param a generic type + * @return new builder + */ + @NotNull + public RequestBuilder returnAs(@NotNull TypeReference tr) { + return new RequestBuilder(transport, mapper, method, id, objectParams, arrayParams, + mapper.getTypeFactory().constructType(tr.getType())); + } + + /** + * Execute a request through {@link Transport} and convert a not null response to an expected type + * + * @return expected not null response + * @throws JsonRpcException in case of JSON-RPC error, returned by the server + * @throws IllegalStateException if the response is null + */ + @NotNull + public T execute() { + T result = executeAndConvert(); + if (result == null) { + throw new IllegalStateException("Response is null. Use 'executeNullable' if this is acceptable"); + } + return result; + } + + /** + * Execute a request through {@link Transport} and convert a nullable response to an expected type + * + * @return expected response + * @throws JsonRpcException in case of JSON-RPC error, returned by the server + */ + @Nullable + public T executeNullable() { + return executeAndConvert(); + } + + @Nullable + @SuppressWarnings("unchecked") + private T executeAndConvert() { + String textResponse = executeRequest(); + + try { + JsonNode responseNode = mapper.readTree(textResponse); + JsonNode result = responseNode.get(RESULT); + JsonNode error = responseNode.get(ERROR); + JsonNode version = responseNode.get(JSONRPC); + JsonNode id = responseNode.get(ID); + + if (version == null) { + throw new IllegalStateException("Not a JSON-RPC response: " + responseNode); + } + if (!version.asText().equals(VERSION_2_0)) { + throw new IllegalStateException("Bad protocol version in a response: " + responseNode); + } + if (id == null) { + throw new IllegalStateException("Unspecified id in a response: " + responseNode); + } + + if (error == null) { + if (result != null) { + return mapper.convertValue(result, javaType); + } else { + throw new IllegalStateException("Neither result or error is set in a response: " + responseNode); + } + } else { + ErrorMessage errorMessage = mapper.treeToValue(error, ErrorMessage.class); + throw new JsonRpcException(errorMessage); + } + } catch (JsonProcessingException e) { + throw new IllegalStateException("Unable parse a JSON response: " + textResponse, e); + } catch (IOException e) { + throw new IllegalStateException("I/O error during a response processing", e); + } + } + + String executeRequest() { + ObjectNode requestNode = request(id, method, params()); + String textRequest; + String textResponse; + try { + textRequest = mapper.writeValueAsString(requestNode); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Unable convert " + requestNode + " to JSON", e); + } + try { + textResponse = transport.pass(textRequest); + } catch (IOException e) { + throw new IllegalStateException("I/O error during a request processing", e); + } + return textResponse; + } + + @NotNull + private JsonNode params() { + if (objectParams.size() > 0) { + if (arrayParams.size() > 0) { + throw new IllegalArgumentException("Both object and array params are set"); + } + return objectParams; + } + return arrayParams; + } +} diff --git a/src/com/github/arteam/simplejsonrpc/client/exception/JsonRpcBatchException.java b/src/com/github/arteam/simplejsonrpc/client/exception/JsonRpcBatchException.java new file mode 100644 index 0000000..737d723 --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/client/exception/JsonRpcBatchException.java @@ -0,0 +1,44 @@ +package com.github.arteam.simplejsonrpc.client.exception; + +import com.github.arteam.simplejsonrpc.core.domain.ErrorMessage; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +/** + * Date: 10/13/14 + * Time: 8:17 PM + * Exception that occurs when batch JSON-RPC request is not completely successful + * + * @author Artem Prigoda + */ +public class JsonRpcBatchException extends RuntimeException { + + /** + * Succeeded requests + */ + @NotNull + private Map successes; + + /** + * Failed requests + */ + @NotNull + private Map errors; + + public JsonRpcBatchException(String message, @NotNull Map successes, @NotNull Map errors) { + super(message); + this.successes = successes; + this.errors = errors; + } + + @NotNull + public Map getSuccesses() { + return successes; + } + + @NotNull + public Map getErrors() { + return errors; + } +} diff --git a/src/com/github/arteam/simplejsonrpc/client/exception/JsonRpcException.java b/src/com/github/arteam/simplejsonrpc/client/exception/JsonRpcException.java new file mode 100644 index 0000000..4fd740f --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/client/exception/JsonRpcException.java @@ -0,0 +1,30 @@ +package com.github.arteam.simplejsonrpc.client.exception; + +import com.github.arteam.simplejsonrpc.core.domain.ErrorMessage; +import org.jetbrains.annotations.NotNull; + +/** + * Date: 8/9/14 + * Time: 10:08 PM + * Represents JSON-RPC error returned by a server + * + * @author Artem Prigoda + */ +public class JsonRpcException extends RuntimeException { + + /** + * Actual error message + */ + @NotNull + private ErrorMessage errorMessage; + + public JsonRpcException(@NotNull ErrorMessage errorMessage) { + super(errorMessage.toString()); + this.errorMessage = errorMessage; + } + + @NotNull + public ErrorMessage getErrorMessage() { + return errorMessage; + } +} diff --git a/src/com/github/arteam/simplejsonrpc/client/generator/AtomicLongIdGenerator.java b/src/com/github/arteam/simplejsonrpc/client/generator/AtomicLongIdGenerator.java new file mode 100644 index 0000000..57ae703 --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/client/generator/AtomicLongIdGenerator.java @@ -0,0 +1,22 @@ +package com.github.arteam.simplejsonrpc.client.generator; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * Date: 12/30/14 + * Time: 11:19 PM + *

+ * Return id from an atomic long counter + * It's the most reliable and straightforward way to generate identifiers + * + * @author Artem Prigoda + */ +public class AtomicLongIdGenerator implements IdGenerator { + + private final AtomicLong counter = new AtomicLong(0L); + + @Override + public Long generate() { + return counter.incrementAndGet(); + } +} diff --git a/src/com/github/arteam/simplejsonrpc/client/generator/CurrentTimeIdGenerator.java b/src/com/github/arteam/simplejsonrpc/client/generator/CurrentTimeIdGenerator.java new file mode 100644 index 0000000..13500db --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/client/generator/CurrentTimeIdGenerator.java @@ -0,0 +1,16 @@ +package com.github.arteam.simplejsonrpc.client.generator; + +/** + * Date: 24.08.14 + * Time: 18:20 + * Return current time as id. + * Not reliable if you need to guarantee uniqueness of request ids + * + * @author Artem Prigoda + */ +public class CurrentTimeIdGenerator implements IdGenerator { + @Override + public Long generate() { + return System.currentTimeMillis(); + } +} diff --git a/src/com/github/arteam/simplejsonrpc/client/generator/IdGenerator.java b/src/com/github/arteam/simplejsonrpc/client/generator/IdGenerator.java new file mode 100644 index 0000000..400a6b8 --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/client/generator/IdGenerator.java @@ -0,0 +1,13 @@ +package com.github.arteam.simplejsonrpc.client.generator; + +/** + * Date: 24.08.14 + * Time: 18:12 + * Strategy for generation request identificators + * + * @author Artem Prigoda + */ +public interface IdGenerator { + + T generate(); +} diff --git a/src/com/github/arteam/simplejsonrpc/client/generator/SecureRandomIdGenerator.java b/src/com/github/arteam/simplejsonrpc/client/generator/SecureRandomIdGenerator.java new file mode 100644 index 0000000..41d12f7 --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/client/generator/SecureRandomIdGenerator.java @@ -0,0 +1,36 @@ +package com.github.arteam.simplejsonrpc.client.generator; + +import org.jetbrains.annotations.NotNull; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +/** + * Date: 1/12/15 + * Time: 11:17 PM + * Abstract generator of secure random identifiers + * + * @author Artem Prigoda + */ +abstract class SecureRandomIdGenerator implements IdGenerator { + + private static final String SHA_1_PRNG = "SHA1PRNG"; + + @NotNull + protected final SecureRandom secureRandom; + + protected SecureRandomIdGenerator() { + secureRandom = initSecureRandom(); + } + + @NotNull + private static SecureRandom initSecureRandom() { + try { + SecureRandom secureRandom = SecureRandom.getInstance(SHA_1_PRNG); + secureRandom.nextBytes(new byte[]{}); // Important to seed immediately after creation + return secureRandom; + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/com/github/arteam/simplejsonrpc/client/generator/SecureRandomIntIdGenerator.java b/src/com/github/arteam/simplejsonrpc/client/generator/SecureRandomIntIdGenerator.java new file mode 100644 index 0000000..de5899e --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/client/generator/SecureRandomIntIdGenerator.java @@ -0,0 +1,32 @@ +package com.github.arteam.simplejsonrpc.client.generator; + +/** + * Date: 1/12/15 + * Time: 11:38 PM + * Generates secure random positive integers under limit + * By default limit is 65536 + * + * @author Artem Prigoda + */ +public class SecureRandomIntIdGenerator extends SecureRandomIdGenerator { + + private static final int DEFAULT_LIMIT = 65536; + + private final int limit; + + public SecureRandomIntIdGenerator() { + limit = DEFAULT_LIMIT; + } + + public SecureRandomIntIdGenerator(int limit) { + if (limit <= 0) { + throw new IllegalArgumentException("Limit should be positive"); + } + this.limit = limit; + } + + @Override + public Integer generate() { + return secureRandom.nextInt(limit); + } +} diff --git a/src/com/github/arteam/simplejsonrpc/client/generator/SecureRandomLongIdGenerator.java b/src/com/github/arteam/simplejsonrpc/client/generator/SecureRandomLongIdGenerator.java new file mode 100644 index 0000000..5856042 --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/client/generator/SecureRandomLongIdGenerator.java @@ -0,0 +1,16 @@ +package com.github.arteam.simplejsonrpc.client.generator; + +/** + * Date: 1/12/15 + * Time: 11:12 PM + * Generate secure random positive long identifiers + * + * @author Artem Prigoda + */ +public class SecureRandomLongIdGenerator extends SecureRandomIdGenerator { + + @Override + public Long generate() { + return secureRandom.nextLong() >>> 1; + } +} diff --git a/src/com/github/arteam/simplejsonrpc/client/generator/SecureRandomStringIdGenerator.java b/src/com/github/arteam/simplejsonrpc/client/generator/SecureRandomStringIdGenerator.java new file mode 100644 index 0000000..71269ae --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/client/generator/SecureRandomStringIdGenerator.java @@ -0,0 +1,65 @@ +package com.github.arteam.simplejsonrpc.client.generator; + +import org.jetbrains.annotations.NotNull; + +/** + * Date: 12/30/14 + * Time: 11:45 PM + * Generate secure random strings consisting of HEX symbols with length of 40 characters + * + * @author Artem Prigoda + */ +public class SecureRandomStringIdGenerator extends SecureRandomIdGenerator { + + private static final char[] ALPHABET = "0123456789abcdef".toCharArray(); + private static final int DEFAULT_CHUNK_SIZE = 20; + + private final int chunkSize; + + /** + * Create a default generator + */ + public SecureRandomStringIdGenerator() { + chunkSize = DEFAULT_CHUNK_SIZE; + } + + /** + * Create generator with a specific identifiers length + * + * @param idLength custom identifier length (it should pow of 2) + */ + public SecureRandomStringIdGenerator(int idLength) { + if (idLength < 2) { + throw new IllegalArgumentException("Bad message length: '" + idLength + "'. It should be >= 2"); + } + this.chunkSize = idLength / 2; + } + + @Override + public String generate() { + byte[] buffer = new byte[chunkSize]; + secureRandom.nextBytes(buffer); + return hexString(buffer); + } + + + /** + * Convert binary data to HEX representation. + * Every byte is converted to 2 HEX symbols (one symbol for every 4 bits) + * + * @param source source chunk of data + * @return string representation of the chunk as HEX values + */ + @NotNull + private static String hexString(@NotNull byte[] source) { + char[] result = new char[source.length * 2]; + for (int i = 0; i < source.length; i++) { + int unsigned = source[i] & 0xFF; + int first4Bytes = unsigned >>> 4; + int last4Bytes = unsigned & 0x0F; + result[i * 2] = ALPHABET[first4Bytes]; + result[i * 2 + 1] = ALPHABET[last4Bytes]; + } + return new String(result); + } +} diff --git a/src/com/github/arteam/simplejsonrpc/client/metadata/ClassMetadata.java b/src/com/github/arteam/simplejsonrpc/client/metadata/ClassMetadata.java new file mode 100644 index 0000000..2b0f488 --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/client/metadata/ClassMetadata.java @@ -0,0 +1,53 @@ +package com.github.arteam.simplejsonrpc.client.metadata; + +import com.github.arteam.simplejsonrpc.client.ParamsType; +import com.github.arteam.simplejsonrpc.client.generator.IdGenerator; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Method; +import java.util.Map; + +/** + * Date: 8/1/14 + * Time: 7:42 PM + * Metadata about a Java class + * + * @author Artem Prigoda + */ +public class ClassMetadata { + + @Nullable + private final ParamsType paramsType; + + @NotNull + private final IdGenerator idGenerator; + + /** + * Map of JSON-RPC 2.0 methods by rpc name + */ + @NotNull + private final Map methods; + + public ClassMetadata(@Nullable ParamsType paramsType, @NotNull IdGenerator idGenerator, + @NotNull Map methods) { + this.paramsType = paramsType; + this.idGenerator = idGenerator; + this.methods = methods; + } + + @Nullable + public ParamsType getParamsType() { + return paramsType; + } + + @NotNull + public IdGenerator getIdGenerator() { + return idGenerator; + } + + @NotNull + public Map getMethods() { + return methods; + } +} diff --git a/src/com/github/arteam/simplejsonrpc/client/metadata/MethodMetadata.java b/src/com/github/arteam/simplejsonrpc/client/metadata/MethodMetadata.java new file mode 100644 index 0000000..62e0cdf --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/client/metadata/MethodMetadata.java @@ -0,0 +1,57 @@ +package com.github.arteam.simplejsonrpc.client.metadata; + +import com.github.arteam.simplejsonrpc.client.ParamsType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Method; +import java.util.Map; + +/** + * Date: 8/1/14 + * Time: 7:42 PM + * Metadata about a Java method + * + * @author Artem Prigoda + */ +public class MethodMetadata { + + @NotNull + private final String name; + + @Nullable + private final ParamsType paramsType; + + /** + * Map of method params by RPC name + */ + @NotNull + private final Map params; + + public MethodMetadata(@NotNull String name, @Nullable ParamsType paramsType, @NotNull Map params) { + this.params = params; + this.name = name; + this.paramsType = paramsType; + } + @NotNull + public Map getParams() { + return params; + } + + @NotNull + public String getName() { + return name; + } + + @Nullable + public ParamsType getParamsType() { + return paramsType; + } + + @Override + public String toString() { + return "MethodMetadata{" + + " params=" + params + + '}'; + } +} diff --git a/src/com/github/arteam/simplejsonrpc/client/metadata/ParameterMetadata.java b/src/com/github/arteam/simplejsonrpc/client/metadata/ParameterMetadata.java new file mode 100644 index 0000000..e89fb28 --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/client/metadata/ParameterMetadata.java @@ -0,0 +1,42 @@ +package com.github.arteam.simplejsonrpc.client.metadata; + +/** + * Date: 8/1/14 + * Time: 7:44 PM + * Method parameter metadata + * + * @author Artem Prigoda + */ +public class ParameterMetadata { + + /** + * Index in method arguments + */ + private final int index; + + /** + * Whether parameter is optional + */ + private final boolean optional; + + public ParameterMetadata(int index, boolean optional) { + this.index = index; + this.optional = optional; + } + + public int getIndex() { + return index; + } + + public boolean isOptional() { + return optional; + } + + @Override + public String toString() { + return "ParameterMetadata{" + + "index=" + index + + ", optional=" + optional + + '}'; + } +} diff --git a/src/com/github/arteam/simplejsonrpc/core/annotation/JsonRpcError.java b/src/com/github/arteam/simplejsonrpc/core/annotation/JsonRpcError.java new file mode 100644 index 0000000..089bbd5 --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/core/annotation/JsonRpcError.java @@ -0,0 +1,33 @@ +package com.github.arteam.simplejsonrpc.core.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Date: 7/31/14 + * Time: 6:03 PM + * Annotation for marking an exception as a JSON-RPC error + * + * @author Artem Prigoda + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface JsonRpcError { + + /** + * JSON-RPC error code + * + * @return error code + */ + int code() default 0; + + /** + * JSON-RPC error message. + * If empty then the exception message will be used + * + * @return error message + */ + String message() default ""; +} diff --git a/src/com/github/arteam/simplejsonrpc/core/annotation/JsonRpcMethod.java b/src/com/github/arteam/simplejsonrpc/core/annotation/JsonRpcMethod.java new file mode 100644 index 0000000..c577792 --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/core/annotation/JsonRpcMethod.java @@ -0,0 +1,27 @@ +package com.github.arteam.simplejsonrpc.core.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Date: 07.06.14 + * Time: 13:02 + *

+ * Annotation for marking a method as eligible for calling from the web. + * Makes sense only for public non-static methods. + * + * @author Artem Prigoda + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface JsonRpcMethod { + + /** + * Method RPC name. By default the actual method name is used. + * + * @return method RPC name + */ + String value() default ""; +} diff --git a/src/com/github/arteam/simplejsonrpc/core/annotation/JsonRpcOptional.java b/src/com/github/arteam/simplejsonrpc/core/annotation/JsonRpcOptional.java new file mode 100644 index 0000000..bb61e36 --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/core/annotation/JsonRpcOptional.java @@ -0,0 +1,22 @@ +package com.github.arteam.simplejsonrpc.core.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Date: 6/15/14 + * Time: 1:49 AM + *

+ * Annotation for marking a parameter as an optional. + *

+ * It means a client isn't forced to pass this parameter to the method. If the client doesn't provide it, + * {@code null} value is used for complex types and an appropriate default value for primitives. + * + * @author Artem Prigoda + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface JsonRpcOptional { +} diff --git a/src/com/github/arteam/simplejsonrpc/core/annotation/JsonRpcParam.java b/src/com/github/arteam/simplejsonrpc/core/annotation/JsonRpcParam.java new file mode 100644 index 0000000..7e83af8 --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/core/annotation/JsonRpcParam.java @@ -0,0 +1,32 @@ +package com.github.arteam.simplejsonrpc.core.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Date: 07.06.14 + * Time: 13:02 + * Annotation for marking RPC method parameter. + *

+ * Because Java doesn't retain information about method names in a class file and + * therefore this information is not available in runtime, this annotation MUST + * be placed on all the method parameters. + *

+ * Otherwise {@link IllegalArgumentException} will be generated in runtime and + * an error message will be returned to a client. + * + * @author Artem Prigoda + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface JsonRpcParam { + + /** + * RPC method parameter name. MUST be specified. + * + * @return parameter name + */ + public String value(); +} diff --git a/src/com/github/arteam/simplejsonrpc/core/annotation/JsonRpcService.java b/src/com/github/arteam/simplejsonrpc/core/annotation/JsonRpcService.java new file mode 100644 index 0000000..c8f5f87 --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/core/annotation/JsonRpcService.java @@ -0,0 +1,16 @@ +package com.github.arteam.simplejsonrpc.core.annotation; + +import java.lang.annotation.*; + +/** + * Date: 8/2/14 + * Time: 6:10 PM + * Annotation for marking a service as a JSON-RPC service + * + * @author Artem Prigoda + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface JsonRpcService { +} diff --git a/src/com/github/arteam/simplejsonrpc/core/domain/ErrorMessage.java b/src/com/github/arteam/simplejsonrpc/core/domain/ErrorMessage.java new file mode 100644 index 0000000..5d0ef1c --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/core/domain/ErrorMessage.java @@ -0,0 +1,42 @@ +package com.github.arteam.simplejsonrpc.core.domain; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.jetbrains.annotations.NotNull; + +/** + * Date: 07.06.14 + * Time: 15:16 + *

+ * Representation of a JSON-RPC error message + * + * @author Artem Prigoda + */ +public class ErrorMessage { + + @JsonProperty("code") + private final int code; + + @NotNull + @JsonProperty("message") + private final String message; + + public ErrorMessage(@JsonProperty("code") int code, + @JsonProperty("message") @NotNull String message) { + this.code = code; + this.message = message; + } + + public int getCode() { + return code; + } + + @NotNull + public String getMessage() { + return message; + } + + @Override + public String toString() { + return "ErrorMessage{code=" + code + ", message=" + message + "}"; + } +} diff --git a/src/com/github/arteam/simplejsonrpc/core/domain/ErrorResponse.java b/src/com/github/arteam/simplejsonrpc/core/domain/ErrorResponse.java new file mode 100644 index 0000000..5deb652 --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/core/domain/ErrorResponse.java @@ -0,0 +1,39 @@ +package com.github.arteam.simplejsonrpc.core.domain; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.ValueNode; +import org.jetbrains.annotations.NotNull; + +/** + * Date: 07.06.14 + * Time: 12:35 + *

+ * Representation of a JSON-RPC error response + * + * @author Artem Prigoda + */ +public class ErrorResponse extends Response { + + @NotNull + @JsonProperty("error") + private final ErrorMessage error; + + @JsonCreator + public ErrorResponse(@JsonProperty("id") @NotNull ValueNode id, + @JsonProperty("error") @NotNull ErrorMessage error) { + super(id); + this.error = error; + } + + public ErrorResponse(@NotNull ErrorMessage error) { + super(NullNode.getInstance()); + this.error = error; + } + + @NotNull + public ErrorMessage getError() { + return error; + } +} diff --git a/src/com/github/arteam/simplejsonrpc/core/domain/Request.java b/src/com/github/arteam/simplejsonrpc/core/domain/Request.java new file mode 100644 index 0000000..9888a8b --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/core/domain/Request.java @@ -0,0 +1,66 @@ +package com.github.arteam.simplejsonrpc.core.domain; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ValueNode; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + + +/** + * Date: 07.06.14 + * Time: 12:24 + *

+ * Representation of a JSON-RPC request + * + * @author Artem Prigoda + */ +public class Request { + + @Nullable + private final String jsonrpc; + + @Nullable + private final String method; + + @NotNull + private final JsonNode params; + + @NotNull + private final ValueNode id; + + public Request(@JsonProperty("jsonrpc") @Nullable String jsonrpc, + @JsonProperty("method") @Nullable String method, + @JsonProperty("params") @NotNull JsonNode params, + @JsonProperty("id") @NotNull ValueNode id) { + this.jsonrpc = jsonrpc; + this.method = method; + this.id = id; + this.params = params; + } + + @Nullable + public String getJsonrpc() { + return jsonrpc; + } + + @Nullable + public String getMethod() { + return method; + } + + @NotNull + public ValueNode getId() { + return id; + } + + @NotNull + public JsonNode getParams() { + return params; + } + + @Override + public String toString() { + return "Request{jsonrpc=" + jsonrpc + ", method=" + method + ", id=" + id + ", params=" + params + "}"; + } +} diff --git a/src/com/github/arteam/simplejsonrpc/core/domain/Response.java b/src/com/github/arteam/simplejsonrpc/core/domain/Response.java new file mode 100644 index 0000000..8dacc41 --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/core/domain/Response.java @@ -0,0 +1,46 @@ +package com.github.arteam.simplejsonrpc.core.domain; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ValueNode; +import org.jetbrains.annotations.NotNull; + +/** + * Date: 07.06.14 + * Time: 12:34 + *

+ * Base representation of a JSON-RPC response (success or error) + * + * @author Artem Prigoda + */ +public class Response { + + private static final String VERSION = "2.0"; + + @NotNull + @JsonProperty("jsonrpc") + private final String jsonrpc; + + @NotNull + @JsonProperty("id") + private final ValueNode id; + + public Response(@NotNull ValueNode id) { + this.id = id; + jsonrpc = VERSION; + } + + public Response(@NotNull ValueNode id, @NotNull String jsonrpc) { + this.id = id; + this.jsonrpc = jsonrpc; + } + + @NotNull + public String getJsonrpc() { + return jsonrpc; + } + + @NotNull + public ValueNode getId() { + return id; + } +} diff --git a/src/com/github/arteam/simplejsonrpc/core/domain/SuccessResponse.java b/src/com/github/arteam/simplejsonrpc/core/domain/SuccessResponse.java new file mode 100644 index 0000000..6023529 --- /dev/null +++ b/src/com/github/arteam/simplejsonrpc/core/domain/SuccessResponse.java @@ -0,0 +1,31 @@ +package com.github.arteam.simplejsonrpc.core.domain; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ValueNode; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Date: 07.06.14 + * Time: 12:31 + * Representation of a successful JSON-RPC response + * + * @author Artem Prigoda + */ +public class SuccessResponse extends Response { + + @Nullable + @JsonProperty("result") + private final Object result; + + public SuccessResponse(@JsonProperty("id") @NotNull ValueNode id, + @JsonProperty("result") @Nullable Object result) { + super(id); + this.result = result; + } + + @Nullable + public Object getResult() { + return result; + } +} diff --git a/src/team492/AdbBridge.java b/src/team492/AdbBridge.java new file mode 100644 index 0000000..c0a2300 --- /dev/null +++ b/src/team492/AdbBridge.java @@ -0,0 +1,98 @@ +package team492; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * AdbBridge interfaces to an Android Debug Bridge (adb) binary, which is needed + * to communicate to Android devices over USB. + * + * adb binary provided by https://github.com/Spectrum3847/RIOdroid + */ +public class AdbBridge +{ + Path bin_location_; + public final static Path DEFAULT_LOCATION = Paths.get("/usr/bin/adb"); + + public AdbBridge() + { + Path adb_location; + String env_val = System.getenv("FRC_ADB_LOCATION"); + if (env_val == null || "".equals(env_val)) + { + adb_location = DEFAULT_LOCATION; + } + else + { + adb_location = Paths.get(env_val); + } + bin_location_ = adb_location; + } + + public AdbBridge(Path location) + { + bin_location_ = location; + } + + private boolean runCommand(String args) + { + Runtime r = Runtime.getRuntime(); + String cmd = bin_location_.toString() + " " + args; + + try + { + Process p = r.exec(cmd); + p.waitFor(); + } + catch (IOException e) + { + System.err.println("AdbBridge: Could not run command " + cmd); + e.printStackTrace(); + return false; + } + catch (InterruptedException e) + { + System.err.println("AdbBridge: Could not run command " + cmd); + e.printStackTrace(); + return false; + } + return true; + } + + public void start() + { + System.out.println("Starting adb"); + runCommand("start"); + } + + public void stop() + { + System.out.println("Stopping adb"); + runCommand("kill-server"); + } + + public void restartAdb() + { + System.out.println("Restarting adb"); + stop(); + start(); + } + + public void portForward(int local_port, int remote_port) + { + runCommand("forward tcp:" + local_port + " tcp:" + remote_port); + } + + public void reversePortForward(int remote_port, int local_port) + { + runCommand("reverse tcp:" + remote_port + " tcp:" + local_port); + } + + public void restartApp() + { + System.out.println("Restarting app"); + runCommand("shell am force-stop com.team254.cheezdroid \\; " + + "am start com.team254.cheezdroid/com.team254.cheezdroid.VisionTrackerActivity"); + } +} diff --git a/src/team492/AwooCommunicator.java b/src/team492/AwooCommunicator.java new file mode 100644 index 0000000..83ad1ab --- /dev/null +++ b/src/team492/AwooCommunicator.java @@ -0,0 +1,103 @@ +package team492; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.Socket; + +import com.github.arteam.simplejsonrpc.client.Transport; + +public class AwooCommunicator implements Transport +{ + private int port; + private BufferedWriter bw; + private Socket kemono; + + public AwooCommunicator(int port) + { + this.port = port; + } + + public void initCommunicator() + { + try + { + System.out.println("Attempting communication to port " + port); + kemono = new Socket("127.0.0.1", port); + bw = new BufferedWriter(new OutputStreamWriter(kemono.getOutputStream())); + } + catch (Exception e) + { + e.printStackTrace(); + } + } + + public void sendMessage(String msg) + { + try + { + if (kemono == null) + { + return; + } + if(!kemono.isClosed() && kemono.isConnected()) + { + try + { + bw.write(msg+"\n"); + bw.flush(); + } + catch (IOException ioe) + { + ioe.printStackTrace(); + } + } + } + catch (Exception e) + { + e.printStackTrace(); + } + } + + public void implodeCommunicator() + { + try + { + bw.close(); + } + catch (IOException e) + { + e.printStackTrace(); + } + bw = null; + try + { + kemono.close(); + } + catch (IOException e) + { + e.printStackTrace(); + } + kemono = null; + port = -1; + } + + @Override + public String pass(String request) throws IOException + { + sendMessage(request); + String toRet = ""; + InputStream istream = kemono.getInputStream(); + BufferedReader receiveRead = new BufferedReader(new InputStreamReader(istream)); + String receiveMessage = ""; + if((receiveMessage = receiveRead.readLine()) != null) + { + toRet = (receiveMessage + "\n"); + } + return toRet; + } +} + diff --git a/src/team492/Robot.java b/src/team492/Robot.java index b5885c0..f3ce923 100644 --- a/src/team492/Robot.java +++ b/src/team492/Robot.java @@ -23,6 +23,8 @@ package team492; import com.ctre.phoenix.motorcontrol.FeedbackDevice; +import com.github.arteam.simplejsonrpc.client.JsonRpcClient; + import edu.wpi.cscore.UsbCamera; import edu.wpi.first.wpilibj.AnalogInput; import edu.wpi.first.wpilibj.CameraServer; @@ -195,6 +197,11 @@ public Robot() super(programName); } //Robot + + public AdbBridge adbBridge; + public AwooCommunicator awooCommunicator; + public JsonRpcClient rpcClient; + /** * This function is run when the robot is first started up and should be used for any initialization code. */ @@ -383,6 +390,24 @@ else if(USE_PIXY_I2C) diagnostics = new OnBoardDiagnostics(this); + + // ayyioasjfasfdjisagoibdsaofisajo;bas;ofjsaiojfasiojgisaofj + adbBridge = new AdbBridge(); + adbBridge.start(); + adbBridge.portForward(13970, 13970); + + awooCommunicator = new AwooCommunicator(13970); + awooCommunicator.initCommunicator(); + + rpcClient = new JsonRpcClient(awooCommunicator); + + // awooCommunicator.sendMessage("Test message from RoboRIO"); + + @SuppressWarnings("unused") + TestRPCClass remoteInitializationInstance = rpcClient.createRequest().method("getMajiraInstance").id(1).returnAs(TestRPCClass.class).execute(); + + System.out.println(remoteInitializationInstance.toString()); + // // Create Robot Modes. // @@ -403,12 +428,14 @@ public void robotStartMode(RunMode runMode, RunMode prevMode) { // Robot is safe. // Note: "disaibled" is not a typo. It forces the speech board to pronounce it correctly. + // awooCommunicator.sendMessage("Robot disabled"); tts.speak("Robot disaibled"); nextTimeToSpeakInSeconds = TrcUtil.getCurrentTime() + IDLE_PERIOD_SECONDS; } else { // Robot is unsafe + // awooCommunicator.sendMessage("Robot enabled, stand clear."); tts.speak("Robot enabled, stand clear"); nextTimeToSpeakInSeconds = TrcUtil.getCurrentTime() + SPEAK_PERIOD_SECONDS; } @@ -873,3 +900,4 @@ public double constrainForcePercentageByElevatorHeight(double desiredForcePercen } } //class Robot + diff --git a/src/team492/TestRPCClass.java b/src/team492/TestRPCClass.java new file mode 100644 index 0000000..cd1e824 --- /dev/null +++ b/src/team492/TestRPCClass.java @@ -0,0 +1,45 @@ +package team492; + +import java.util.Date; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class TestRPCClass +{ + @JsonProperty + private String firstName; + + @JsonProperty + private String lastName; + + @JsonProperty + private long timeInitialized = 0; + + + @JsonCreator + public TestRPCClass() + { + firstName = "Tail"; + lastName = "Fuzzball"; + } + + @JsonCreator + public TestRPCClass(@JsonProperty("firstName") String firstName, @JsonProperty("lastName") String lastName) + { + this.firstName = firstName; + this.lastName = lastName; + } + + public void initTime() + { + Date date = new Date(); + timeInitialized = date.getTime(); + } + + @Override + public String toString() + { + return firstName + " " + lastName + " " + timeInitialized; + } +} diff --git a/src/team492/TestRPCService.java b/src/team492/TestRPCService.java new file mode 100644 index 0000000..e6bd6e6 --- /dev/null +++ b/src/team492/TestRPCService.java @@ -0,0 +1,12 @@ +package team492; + +import com.github.arteam.simplejsonrpc.client.JsonRpcId; +import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcMethod; +import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcService; + +@JsonRpcService +public interface TestRPCService +{ + @JsonRpcMethod("getMajiraInstance") + public TestRPCClass getMajiraInstance(); +}