diff --git a/pom.xml b/pom.xml index d437c401bd..a87e3edddc 100644 --- a/pom.xml +++ b/pom.xml @@ -57,6 +57,7 @@ 1.7.25 3.2.6 1.0.2 + 2.9.6 1.2.3 1.1 4.12 @@ -640,6 +641,12 @@ ${micrometer.version} true + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + true + commons-cli commons-cli diff --git a/src/main/java/com/rabbitmq/tools/json/JSONReader.java b/src/main/java/com/rabbitmq/tools/json/JSONReader.java index 6c9420767e..14c690e6e2 100644 --- a/src/main/java/com/rabbitmq/tools/json/JSONReader.java +++ b/src/main/java/com/rabbitmq/tools/json/JSONReader.java @@ -44,6 +44,11 @@ import java.util.List; import java.util.Map; +/** + * Will be removed in 6.0 + * + * @deprecated Use a third-party JSON library, e.g. Jackson or Gson + */ public class JSONReader { private static final Object OBJECT_END = new Object(); diff --git a/src/main/java/com/rabbitmq/tools/json/JSONSerializable.java b/src/main/java/com/rabbitmq/tools/json/JSONSerializable.java index 1cb1b0a77e..39f72d4ac2 100644 --- a/src/main/java/com/rabbitmq/tools/json/JSONSerializable.java +++ b/src/main/java/com/rabbitmq/tools/json/JSONSerializable.java @@ -18,6 +18,10 @@ /** * Interface for classes that wish to control their own serialization. + * + * Will be removed in 6.0 + * + * @deprecated Use a third-party JSON library, e.g. Jackson or Gson */ public interface JSONSerializable { /** diff --git a/src/main/java/com/rabbitmq/tools/json/JSONUtil.java b/src/main/java/com/rabbitmq/tools/json/JSONUtil.java index 6050482f5e..c024e142c8 100644 --- a/src/main/java/com/rabbitmq/tools/json/JSONUtil.java +++ b/src/main/java/com/rabbitmq/tools/json/JSONUtil.java @@ -13,7 +13,6 @@ // If you have any questions regarding licensing, please contact us at // info@rabbitmq.com. - package com.rabbitmq.tools.json; import org.slf4j.Logger; @@ -34,15 +33,15 @@ */ public class JSONUtil { - private static final Logger LOGGER = LoggerFactory.getLogger(JSONUtil.class); + private static final Logger LOGGER = LoggerFactory.getLogger(JSONUtil.class); + /** * Uses reflection to fill public fields and Bean properties of * the target object from the source Map. */ public static Object fill(Object target, Map source) - throws IntrospectionException, IllegalAccessException, InvocationTargetException - { - return fill(target, source, true); + throws IntrospectionException, IllegalAccessException, InvocationTargetException { + return fill(target, source, true); } /** @@ -50,40 +49,36 @@ public static Object fill(Object target, Map source) * properties of the target object from the source Map. */ public static Object fill(Object target, Map source, boolean useProperties) - throws IntrospectionException, IllegalAccessException, InvocationTargetException - { - if (useProperties) { - BeanInfo info = Introspector.getBeanInfo(target.getClass()); + throws IntrospectionException, IllegalAccessException, InvocationTargetException { + if (useProperties) { + BeanInfo info = Introspector.getBeanInfo(target.getClass()); - PropertyDescriptor[] props = info.getPropertyDescriptors(); - for (int i = 0; i < props.length; ++i) { - PropertyDescriptor prop = props[i]; - String name = prop.getName(); - Method setter = prop.getWriteMethod(); - if (setter != null && !Modifier.isStatic(setter.getModifiers())) { - //System.out.println(target + " " + name + " <- " + source.get(name)); - setter.invoke(target, source.get(name)); - } - } - } + PropertyDescriptor[] props = info.getPropertyDescriptors(); + for (int i = 0; i < props.length; ++i) { + PropertyDescriptor prop = props[i]; + String name = prop.getName(); + Method setter = prop.getWriteMethod(); + if (setter != null && !Modifier.isStatic(setter.getModifiers())) { + setter.invoke(target, source.get(name)); + } + } + } - Field[] ff = target.getClass().getDeclaredFields(); - for (int i = 0; i < ff.length; ++i) { - Field field = ff[i]; + Field[] ff = target.getClass().getDeclaredFields(); + for (int i = 0; i < ff.length; ++i) { + Field field = ff[i]; int fieldMod = field.getModifiers(); - if (Modifier.isPublic(fieldMod) && !(Modifier.isFinal(fieldMod) || - Modifier.isStatic(fieldMod))) - { - //System.out.println(target + " " + field.getName() + " := " + source.get(field.getName())); - try { - field.set(target, source.get(field.getName())); - } catch (IllegalArgumentException iae) { - // no special error processing required + if (Modifier.isPublic(fieldMod) && !(Modifier.isFinal(fieldMod) || + Modifier.isStatic(fieldMod))) { + try { + field.set(target, source.get(field.getName())); + } catch (IllegalArgumentException iae) { + // no special error processing required } - } - } + } + } - return target; + return target; } /** @@ -92,14 +87,14 @@ public static Object fill(Object target, Map source, boolean use * source Map. */ public static void tryFill(Object target, Map source) { - try { - fill(target, source); - } catch (IntrospectionException ie) { - LOGGER.error("Error in tryFill", ie); - } catch (IllegalAccessException iae) { - LOGGER.error("Error in tryFill", iae); - } catch (InvocationTargetException ite) { - LOGGER.error("Error in tryFill", ite); - } + try { + fill(target, source); + } catch (IntrospectionException ie) { + LOGGER.error("Error in tryFill", ie); + } catch (IllegalAccessException iae) { + LOGGER.error("Error in tryFill", iae); + } catch (InvocationTargetException ite) { + LOGGER.error("Error in tryFill", ite); + } } } diff --git a/src/main/java/com/rabbitmq/tools/json/JSONWriter.java b/src/main/java/com/rabbitmq/tools/json/JSONWriter.java index 4a7f78c7f0..7101598040 100644 --- a/src/main/java/com/rabbitmq/tools/json/JSONWriter.java +++ b/src/main/java/com/rabbitmq/tools/json/JSONWriter.java @@ -53,6 +53,10 @@ import java.util.Map; import java.util.Set; +/** + * Will be removed in 6.0 + * @deprecated Use a third-party JSON library, e.g. Jackson or Gson + */ public class JSONWriter { private boolean indentMode = false; private int indentLevel = 0; diff --git a/src/main/java/com/rabbitmq/tools/jsonrpc/DefaultJsonRpcMapper.java b/src/main/java/com/rabbitmq/tools/jsonrpc/DefaultJsonRpcMapper.java new file mode 100644 index 0000000000..b40789e380 --- /dev/null +++ b/src/main/java/com/rabbitmq/tools/jsonrpc/DefaultJsonRpcMapper.java @@ -0,0 +1,72 @@ +// Copyright (c) 2018 Pivotal Software, Inc. All rights reserved. +// +// This software, the RabbitMQ Java client library, is triple-licensed under the +// Mozilla Public License 1.1 ("MPL"), the GNU General Public License version 2 +// ("GPL") and the Apache License version 2 ("ASL"). For the MPL, please see +// LICENSE-MPL-RabbitMQ. For the GPL, please see LICENSE-GPL2. For the ASL, +// please see LICENSE-APACHE2. +// +// This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, +// either express or implied. See the LICENSE file for specific language governing +// rights and limitations of this software. +// +// If you have any questions regarding licensing, please contact us at +// info@rabbitmq.com. + +package com.rabbitmq.tools.jsonrpc; + +import com.rabbitmq.tools.json.JSONReader; +import com.rabbitmq.tools.json.JSONWriter; + +import java.util.List; +import java.util.Map; + +/** + * Simple {@link JsonRpcMapper} based on homegrown JSON utilities. + * Handles integers, doubles, strings, booleans, and arrays of those types. + *

+ * For a more comprehensive set of features, use {@link JacksonJsonRpcMapper}. + *

+ * Will be removed in 6.0 + * + * @see JsonRpcMapper + * @see JacksonJsonRpcMapper + * @since 5.4.0 + * @deprecated use {@link JacksonJsonRpcMapper} instead + */ +public class DefaultJsonRpcMapper implements JsonRpcMapper { + + @Override + public JsonRpcRequest parse(String requestBody, ServiceDescription description) { + @SuppressWarnings("unchecked") + Map request = (Map) new JSONReader().read(requestBody); + return new JsonRpcRequest( + request.get("id"), request.get("version").toString(), request.get("method").toString(), + ((List) request.get("params")).toArray() + ); + } + + @Override + public JsonRpcResponse parse(String responseBody, Class expectedType) { + @SuppressWarnings("unchecked") + Map map = (Map) (new JSONReader().read(responseBody)); + Map error; + JsonRpcException exception = null; + if (map.containsKey("error")) { + error = (Map) map.get("error"); + exception = new JsonRpcException( + new JSONWriter().write(error), + (String) error.get("name"), + error.get("code") == null ? 0 : (Integer) error.get("code"), + (String) error.get("message"), + error + ); + } + return new JsonRpcResponse(map.get("result"), map.get("error"), exception); + } + + @Override + public String write(Object input) { + return new JSONWriter().write(input); + } +} diff --git a/src/main/java/com/rabbitmq/tools/jsonrpc/JacksonJsonRpcMapper.java b/src/main/java/com/rabbitmq/tools/jsonrpc/JacksonJsonRpcMapper.java new file mode 100644 index 0000000000..9cb5411b25 --- /dev/null +++ b/src/main/java/com/rabbitmq/tools/jsonrpc/JacksonJsonRpcMapper.java @@ -0,0 +1,200 @@ +// Copyright (c) 2018 Pivotal Software, Inc. All rights reserved. +// +// This software, the RabbitMQ Java client library, is triple-licensed under the +// Mozilla Public License 1.1 ("MPL"), the GNU General Public License version 2 +// ("GPL") and the Apache License version 2 ("ASL"). For the MPL, please see +// LICENSE-MPL-RabbitMQ. For the GPL, please see LICENSE-GPL2. For the ASL, +// please see LICENSE-APACHE2. +// +// This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, +// either express or implied. See the LICENSE file for specific language governing +// rights and limitations of this software. +// +// If you have any questions regarding licensing, please contact us at +// info@rabbitmq.com. + +package com.rabbitmq.tools.jsonrpc; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.MappingJsonFactory; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ValueNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * {@link JsonRpcMapper} based on Jackson. + * Uses the streaming and databind modules. + * + * @see JsonRpcMapper + * @since 5.4.0 + */ +public class JacksonJsonRpcMapper implements JsonRpcMapper { + + private static final Logger LOGGER = LoggerFactory.getLogger(JacksonJsonRpcMapper.class); + + private final ObjectMapper mapper; + + public JacksonJsonRpcMapper(ObjectMapper mapper) { + this.mapper = mapper; + } + + public JacksonJsonRpcMapper() { + this(new ObjectMapper()); + } + + @Override + public JsonRpcRequest parse(String requestBody, ServiceDescription description) { + JsonFactory jsonFactory = new MappingJsonFactory(); + String method = null, version = null; + final List parameters = new ArrayList<>(); + Object id = null; + try (JsonParser parser = jsonFactory.createParser(requestBody)) { + while (parser.nextToken() != null) { + JsonToken token = parser.currentToken(); + if (token == JsonToken.FIELD_NAME) { + String name = parser.currentName(); + token = parser.nextToken(); + if ("method".equals(name)) { + method = parser.getValueAsString(); + } else if ("id".equals(name)) { + TreeNode node = parser.readValueAsTree(); + if (node instanceof ValueNode) { + ValueNode idNode = (ValueNode) node; + if (idNode.isNull()) { + id = null; + } else if (idNode.isTextual()) { + id = idNode.asText(); + } else if (idNode.isNumber()) { + id = Long.valueOf(idNode.asLong()); + } else { + LOGGER.warn("ID type not null, text, or number {}, ignoring", idNode); + } + } else { + LOGGER.warn("ID not a scalar value {}, ignoring", node); + } + } else if ("version".equals(name)) { + version = parser.getValueAsString(); + } else if ("params".equals(name)) { + if (token == JsonToken.START_ARRAY) { + while (parser.nextToken() != JsonToken.END_ARRAY) { + parameters.add(parser.readValueAsTree()); + } + } else { + throw new IllegalStateException("Field params must be an array"); + } + } + } + } + } catch (IOException e) { + throw new JsonRpcMappingException("Error during JSON parsing", e); + } + + if (method == null) { + throw new IllegalArgumentException("Could not find method to invoke in request"); + } + + List convertedParameters = new ArrayList<>(parameters.size()); + if (!parameters.isEmpty()) { + ProcedureDescription proc = description.getProcedure(method, parameters.size()); + Method internalMethod = proc.internal_getMethod(); + for (int i = 0; i < internalMethod.getParameterCount(); i++) { + TreeNode parameterNode = parameters.get(i); + try { + Class parameterType = internalMethod.getParameterTypes()[i]; + Object value = convert(parameterNode, parameterType); + convertedParameters.add(value); + } catch (IOException e) { + throw new JsonRpcMappingException("Error during parameter conversion", e); + } + } + } + + return new JsonRpcRequest( + id, version, method, + convertedParameters.toArray() + ); + } + + @Override + public JsonRpcResponse parse(String responseBody, Class expectedReturnType) { + JsonFactory jsonFactory = new MappingJsonFactory(); + Object result = null; + JsonRpcException exception = null; + Map errorMap = null; + try (JsonParser parser = jsonFactory.createParser(responseBody)) { + while (parser.nextToken() != null) { + JsonToken token = parser.currentToken(); + if (token == JsonToken.FIELD_NAME) { + String name = parser.currentName(); + if ("result".equals(name)) { + parser.nextToken(); + if (expectedReturnType == Void.TYPE) { + result = null; + } else { + result = convert(parser.readValueAsTree(), expectedReturnType); + } + } else if ("error".equals(name)) { + errorMap = (Map) convert(parser.readValueAsTree(), Map.class); + exception = new JsonRpcException( + errorMap.toString(), + (String) errorMap.get("name"), + errorMap.get("code") == null ? 0 : (Integer) errorMap.get("code"), + (String) errorMap.get("message"), + errorMap + ); + } + } + } + } catch (IOException e) { + throw new JsonRpcMappingException("Error during JSON parsing", e); + } + return new JsonRpcResponse(result, errorMap, exception); + } + + @Override + public String write(Object input) { + try { + return mapper.writeValueAsString(input); + } catch (JsonProcessingException e) { + throw new JsonRpcMappingException("Error during JSON serialization", e); + } + } + + protected Object convert(TreeNode node, Class expectedType) throws IOException { + Object value; + if (expectedType.isPrimitive()) { + ValueNode valueNode = (ValueNode) node; + if (expectedType == Boolean.TYPE) { + value = valueNode.booleanValue(); + } else if (expectedType == Character.TYPE) { + value = valueNode.textValue().charAt(0); + } else if (expectedType == Short.TYPE) { + value = valueNode.shortValue(); + } else if (expectedType == Integer.TYPE) { + value = valueNode.intValue(); + } else if (expectedType == Long.TYPE) { + value = valueNode.longValue(); + } else if (expectedType == Float.TYPE) { + value = valueNode.floatValue(); + } else if (expectedType == Double.TYPE) { + value = valueNode.doubleValue(); + } else { + throw new IllegalArgumentException("Primitive type not supported: " + expectedType); + } + } else { + value = mapper.readValue(node.traverse(), expectedType); + } + return value; + } +} diff --git a/src/main/java/com/rabbitmq/tools/jsonrpc/JsonRpcClient.java b/src/main/java/com/rabbitmq/tools/jsonrpc/JsonRpcClient.java index 31822da6d0..f64b9f47f0 100644 --- a/src/main/java/com/rabbitmq/tools/jsonrpc/JsonRpcClient.java +++ b/src/main/java/com/rabbitmq/tools/jsonrpc/JsonRpcClient.java @@ -15,6 +15,13 @@ package com.rabbitmq.tools.jsonrpc; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.RpcClient; +import com.rabbitmq.client.ShutdownSignalException; +import com.rabbitmq.tools.json.JSONReader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.IOException; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; @@ -23,111 +30,149 @@ import java.util.Map; import java.util.concurrent.TimeoutException; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.RpcClient; -import com.rabbitmq.client.ShutdownSignalException; -import com.rabbitmq.tools.json.JSONReader; -import com.rabbitmq.tools.json.JSONWriter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** - JSON-RPC is a lightweight - RPC mechanism using JSON - as a data language for request and reply messages. It is - rapidly becoming a standard in web development, where it is - used to make RPC requests over HTTP. RabbitMQ provides an - AMQP transport binding for JSON-RPC in the form of the - JsonRpcClient class. - - JSON-RPC services are self-describing - each service is able - to list its supported procedures, and each procedure - describes its parameters and types. An instance of - JsonRpcClient retrieves its service description using the - standard system.describe procedure when it is - constructed, and uses the information to coerce parameter - types appropriately. A JSON service description is parsed - into instances of ServiceDescription. Client - code can access the service description by reading the - serviceDescription field of - JsonRpcClient instances. - - @see #call(String, Object[]) - @see #call(String[]) + * JSON-RPC is a lightweight + * RPC mechanism using JSON + * as a data language for request and reply messages. It is + * rapidly becoming a standard in web development, where it is + * used to make RPC requests over HTTP. RabbitMQ provides an + * AMQP transport binding for JSON-RPC in the form of the + * JsonRpcClient class. + *

+ * JSON-RPC services are self-describing - each service is able + * to list its supported procedures, and each procedure + * describes its parameters and types. An instance of + * JsonRpcClient retrieves its service description using the + * standard system.describe procedure when it is + * constructed, and uses the information to coerce parameter + * types appropriately. A JSON service description is parsed + * into instances of ServiceDescription. Client + * code can access the service description by reading the + * serviceDescription field of + * JsonRpcClient instances. + *

+ * {@link JsonRpcClient} delegates JSON parsing and generating to + * a {@link JsonRpcMapper}. + * + * @see #call(String, Object[]) + * @see #call(String[]) + * @see JsonRpcMapper + * @see JacksonJsonRpcMapper */ public class JsonRpcClient extends RpcClient implements InvocationHandler { private static final Logger LOGGER = LoggerFactory.getLogger(JsonRpcClient.class); - - /** Holds the JSON-RPC service description for this client. */ + private final JsonRpcMapper mapper; + /** + * Holds the JSON-RPC service description for this client. + */ private ServiceDescription serviceDescription; /** * Construct a new JsonRpcClient, passing the parameters through * to RpcClient's constructor. The service description record is * retrieved from the server during construction. + * + * @throws TimeoutException if a response is not received within the timeout specified, if any + */ + public JsonRpcClient(Channel channel, String exchange, String routingKey, int timeout, JsonRpcMapper mapper) + throws IOException, JsonRpcException, TimeoutException { + super(channel, exchange, routingKey, timeout); + this.mapper = mapper; + retrieveServiceDescription(); + } + + /** + * Construct a new JsonRpcClient, passing the parameters through + * to RpcClient's constructor. The service description record is + * retrieved from the server during construction. + * * @throws TimeoutException if a response is not received within the timeout specified, if any */ public JsonRpcClient(Channel channel, String exchange, String routingKey, int timeout) - throws IOException, JsonRpcException, TimeoutException - { - super(channel, exchange, routingKey, timeout); - retrieveServiceDescription(); + throws IOException, JsonRpcException, TimeoutException { + this(channel, exchange, routingKey, timeout, new DefaultJsonRpcMapper()); } public JsonRpcClient(Channel channel, String exchange, String routingKey) - throws IOException, JsonRpcException, TimeoutException - { + throws IOException, JsonRpcException, TimeoutException { this(channel, exchange, routingKey, RpcClient.NO_TIMEOUT); } + /** + * Private API - used by {@link #call(String[])} to ad-hoc convert + * strings into the required data types for a call. + */ + public static Object coerce(String val, String type) + throws NumberFormatException { + if ("bit".equals(type)) { + return Boolean.getBoolean(val) ? Boolean.TRUE : Boolean.FALSE; + } else if ("num".equals(type)) { + try { + return Integer.valueOf(val); + } catch (NumberFormatException nfe) { + return Double.valueOf(val); + } + } else if ("str".equals(type)) { + return val; + } else if ("arr".equals(type) || "obj".equals(type) || "any".equals(type)) { + return new JSONReader().read(val); + } else if ("nil".equals(type)) { + return null; + } else { + throw new IllegalArgumentException("Bad type: " + type); + } + } + /** * Private API - parses a JSON-RPC reply object, checking it for exceptions. + * * @return the result contained within the reply, if no exception is found * Throws JsonRpcException if the reply object contained an exception */ - public static Object checkReply(Map reply) - throws JsonRpcException - { - if (reply.containsKey("error")) { - @SuppressWarnings("unchecked") - Map map = (Map) reply.get("error"); - // actually a Map - throw new JsonRpcException(map); + private Object checkReply(JsonRpcMapper.JsonRpcResponse reply) + throws JsonRpcException { + if (reply.getError() != null) { + throw reply.getException(); } - Object result = reply.get("result"); - return result; + return reply.getResult(); } /** * Public API - builds, encodes and sends a JSON-RPC request, and * waits for the response. + * * @return the result contained within the reply, if no exception is found * @throws JsonRpcException if the reply object contained an exception * @throws TimeoutException if a response is not received within the timeout specified, if any */ - public Object call(String method, Object[] params) throws IOException, JsonRpcException, TimeoutException - { - HashMap request = new HashMap(); + public Object call(String method, Object[] params) throws IOException, JsonRpcException, TimeoutException { + Map request = new HashMap(); request.put("id", null); request.put("method", method); request.put("version", ServiceDescription.JSON_RPC_VERSION); - request.put("params", (params == null) ? new Object[0] : params); - String requestStr = new JSONWriter().write(request); + params = (params == null) ? new Object[0] : params; + request.put("params", params); + String requestStr = mapper.write(request); try { String replyStr = this.stringCall(requestStr); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Reply string: {}", replyStr); } - @SuppressWarnings("unchecked") - Map map = (Map) (new JSONReader().read(replyStr)); + Class expectedType; + if ("system.describe".equals(method) && params.length == 0) { + expectedType = Map.class; + } else { + ProcedureDescription proc = serviceDescription.getProcedure(method, params.length); + expectedType = proc.getReturnType(); + } + JsonRpcMapper.JsonRpcResponse reply = mapper.parse(replyStr, expectedType); - return checkReply(map); - } catch(ShutdownSignalException ex) { + return checkReply(reply); + } catch (ShutdownSignalException ex) { throw new IOException(ex.getMessage()); // wrap, re-throw } - } /** @@ -137,8 +182,7 @@ public Object call(String method, Object[] params) throws IOException, JsonRpcEx */ @Override public Object invoke(Object proxy, Method method, Object[] args) - throws Throwable - { + throws Throwable { return call(method.getName(), args); } @@ -147,69 +191,42 @@ public Object invoke(Object proxy, Method method, Object[] args) */ @SuppressWarnings("unchecked") public T createProxy(Class klass) - throws IllegalArgumentException - { + throws IllegalArgumentException { return (T) Proxy.newProxyInstance(klass.getClassLoader(), - new Class[] { klass }, - this); + new Class[] { klass }, + this); } /** - * Private API - used by {@link #call(String[])} to ad-hoc convert - * strings into the required data types for a call. - */ - public static Object coerce(String val, String type) - throws NumberFormatException - { - if ("bit".equals(type)) { - return Boolean.getBoolean(val) ? Boolean.TRUE : Boolean.FALSE; - } else if ("num".equals(type)) { - try { - return Integer.valueOf(val); - } catch (NumberFormatException nfe) { - return Double.valueOf(val); - } - } else if ("str".equals(type)) { - return val; - } else if ("arr".equals(type) || "obj".equals(type) || "any".equals(type)) { - return new JSONReader().read(val); - } else if ("nil".equals(type)) { - return null; - } else { - throw new IllegalArgumentException("Bad type: " + type); - } - } - - /** - * Public API - as {@link #call(String,Object[])}, but takes the + * Public API - as {@link #call(String, Object[])}, but takes the * method name from the first entry in args, and the * parameters from subsequent entries. All parameter values are * passed through coerce() to attempt to make them the types the * server is expecting. + * * @return the result contained within the reply, if no exception is found - * @throws JsonRpcException if the reply object contained an exception + * @throws JsonRpcException if the reply object contained an exception * @throws NumberFormatException if a coercion failed - * @throws TimeoutException if a response is not received within the timeout specified, if any + * @throws TimeoutException if a response is not received within the timeout specified, if any * @see #coerce */ public Object call(String[] args) - throws NumberFormatException, IOException, JsonRpcException, TimeoutException - { - if (args.length == 0) { - throw new IllegalArgumentException("First string argument must be method name"); - } + throws NumberFormatException, IOException, JsonRpcException, TimeoutException { + if (args.length == 0) { + throw new IllegalArgumentException("First string argument must be method name"); + } - String method = args[0]; + String method = args[0]; int arity = args.length - 1; - ProcedureDescription proc = serviceDescription.getProcedure(method, arity); - ParameterDescription[] params = proc.getParams(); + ProcedureDescription proc = serviceDescription.getProcedure(method, arity); + ParameterDescription[] params = proc.getParams(); - Object[] actuals = new Object[arity]; - for (int count = 0; count < params.length; count++) { - actuals[count] = coerce(args[count + 1], params[count].type); - } + Object[] actuals = new Object[arity]; + for (int count = 0; count < params.length; count++) { + actuals[count] = coerce(args[count + 1], params[count].type); + } - return call(method, actuals); + return call(method, actuals); } /** @@ -217,7 +234,7 @@ public Object call(String[] args) * service loaded from the server itself at construction time. */ public ServiceDescription getServiceDescription() { - return serviceDescription; + return serviceDescription; } /** @@ -225,10 +242,10 @@ public ServiceDescription getServiceDescription() { * server, and parses and stores the resulting service description * in this object. * TODO: Avoid calling this from the constructor. + * * @throws TimeoutException if a response is not received within the timeout specified, if any */ - private void retrieveServiceDescription() throws IOException, JsonRpcException, TimeoutException - { + private void retrieveServiceDescription() throws IOException, JsonRpcException, TimeoutException { @SuppressWarnings("unchecked") Map rawServiceDescription = (Map) call("system.describe", null); serviceDescription = new ServiceDescription(rawServiceDescription); diff --git a/src/main/java/com/rabbitmq/tools/jsonrpc/JsonRpcException.java b/src/main/java/com/rabbitmq/tools/jsonrpc/JsonRpcException.java index cbefc4fb21..fafb9457d0 100644 --- a/src/main/java/com/rabbitmq/tools/jsonrpc/JsonRpcException.java +++ b/src/main/java/com/rabbitmq/tools/jsonrpc/JsonRpcException.java @@ -13,40 +13,43 @@ // If you have any questions regarding licensing, please contact us at // info@rabbitmq.com. - package com.rabbitmq.tools.jsonrpc; -import java.util.Map; - -import com.rabbitmq.tools.json.JSONWriter; - /** * Thrown when a JSON-RPC service indicates an error occurred during a call. */ public class JsonRpcException extends Exception { + /** * Default serialized version ID */ private static final long serialVersionUID = 1L; - /** Usually the constant string, "JSONRPCError" */ + /** + * Usually the constant string, "JSONRPCError" + */ public String name; - /** Error code */ + /** + * Error code + */ public int code; - /** Error message */ + /** + * Error message + */ public String message; - /** Error detail object - may not always be present or meaningful */ + /** + * Error detail object - may not always be present or meaningful + */ public Object error; public JsonRpcException() { // no work needed in default no-arg constructor } - public JsonRpcException(Map errorMap) { - super(new JSONWriter().write(errorMap)); - name = (String) errorMap.get("name"); - code = 0; - if (errorMap.get("code") != null) { code = ((Integer) errorMap.get("code")); } - message = (String) errorMap.get("message"); - error = errorMap.get("error"); + public JsonRpcException(String detailMessage, String name, int code, String message, Object error) { + super(detailMessage); + this.name = name; + this.code = code; + this.message = message; + this.error = error; } } diff --git a/src/main/java/com/rabbitmq/tools/jsonrpc/JsonRpcMapper.java b/src/main/java/com/rabbitmq/tools/jsonrpc/JsonRpcMapper.java new file mode 100644 index 0000000000..fdad5e1960 --- /dev/null +++ b/src/main/java/com/rabbitmq/tools/jsonrpc/JsonRpcMapper.java @@ -0,0 +1,115 @@ +// Copyright (c) 2018 Pivotal Software, Inc. All rights reserved. +// +// This software, the RabbitMQ Java client library, is triple-licensed under the +// Mozilla Public License 1.1 ("MPL"), the GNU General Public License version 2 +// ("GPL") and the Apache License version 2 ("ASL"). For the MPL, please see +// LICENSE-MPL-RabbitMQ. For the GPL, please see LICENSE-GPL2. For the ASL, +// please see LICENSE-APACHE2. +// +// This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, +// either express or implied. See the LICENSE file for specific language governing +// rights and limitations of this software. +// +// If you have any questions regarding licensing, please contact us at +// info@rabbitmq.com. + +package com.rabbitmq.tools.jsonrpc; + +/** + * Abstraction to handle JSON parsing and generation. + * Used by {@link JsonRpcServer} and {@link JsonRpcClient}. + * + * @since 5.4.0 + */ +public interface JsonRpcMapper { + + /** + * Parses a JSON RPC request. + * The {@link ServiceDescription} can be used + * to look up the invoked procedure and learn about + * its signature. + * @param requestBody + * @param description + * @return + */ + JsonRpcRequest parse(String requestBody, ServiceDescription description); + + /** + * Parses a JSON RPC response. + * @param responseBody + * @param expectedType + * @return + */ + JsonRpcResponse parse(String responseBody, Class expectedType); + + /** + * Serialize an object into JSON. + * @param input + * @return + */ + String write(Object input); + + class JsonRpcRequest { + + private final Object id; + private final String version; + private final String method; + private final Object[] parameters; + + public JsonRpcRequest(Object id, String version, String method, Object[] parameters) { + this.id = id; + this.version = version; + this.method = method; + this.parameters = parameters; + } + + public Object getId() { + return id; + } + + public String getVersion() { + return version; + } + + public String getMethod() { + return method; + } + + public Object[] getParameters() { + return parameters; + } + + public boolean isSystem() { + return method.startsWith("system."); + } + + public boolean isSystemDescribe() { + return "system.describe".equals(method); + } + } + + class JsonRpcResponse { + + private final Object result; + private final Object error; + private final JsonRpcException exception; + + public JsonRpcResponse(Object result, Object error, JsonRpcException exception) { + this.result = result; + this.error = error; + this.exception = exception; + } + + public Object getError() { + return error; + } + + public Object getResult() { + return result; + } + + public JsonRpcException getException() { + return exception; + } + } +} diff --git a/src/main/java/com/rabbitmq/tools/jsonrpc/JsonRpcMappingException.java b/src/main/java/com/rabbitmq/tools/jsonrpc/JsonRpcMappingException.java new file mode 100644 index 0000000000..03a7d12b91 --- /dev/null +++ b/src/main/java/com/rabbitmq/tools/jsonrpc/JsonRpcMappingException.java @@ -0,0 +1,27 @@ +// Copyright (c) 2018 Pivotal Software, Inc. All rights reserved. +// +// This software, the RabbitMQ Java client library, is triple-licensed under the +// Mozilla Public License 1.1 ("MPL"), the GNU General Public License version 2 +// ("GPL") and the Apache License version 2 ("ASL"). For the MPL, please see +// LICENSE-MPL-RabbitMQ. For the GPL, please see LICENSE-GPL2. For the ASL, +// please see LICENSE-APACHE2. +// +// This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, +// either express or implied. See the LICENSE file for specific language governing +// rights and limitations of this software. +// +// If you have any questions regarding licensing, please contact us at +// info@rabbitmq.com. + +package com.rabbitmq.tools.jsonrpc; + +/** + * + * @since 5.4.0 + */ +public class JsonRpcMappingException extends RuntimeException { + + public JsonRpcMappingException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/rabbitmq/tools/jsonrpc/JsonRpcServer.java b/src/main/java/com/rabbitmq/tools/jsonrpc/JsonRpcServer.java index 138f654b12..723664c3b5 100644 --- a/src/main/java/com/rabbitmq/tools/jsonrpc/JsonRpcServer.java +++ b/src/main/java/com/rabbitmq/tools/jsonrpc/JsonRpcServer.java @@ -13,71 +13,89 @@ // If you have any questions regarding licensing, please contact us at // info@rabbitmq.com. - package com.rabbitmq.tools.jsonrpc; +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.StringRpcServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.IOException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; -import java.util.List; import java.util.Map; -import com.rabbitmq.client.AMQP; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.StringRpcServer; -import com.rabbitmq.tools.json.JSONReader; -import com.rabbitmq.tools.json.JSONWriter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** * JSON-RPC Server class. - * + *

* Given a Java {@link Class}, representing an interface, and an * implementation of that interface, JsonRpcServer will reflect on the * class to construct the {@link ServiceDescription}, and will route * incoming requests for methods on the interface to the * implementation object while the mainloop() is running. + *

+ * {@link JsonRpcServer} delegates JSON parsing and generating to + * a {@link JsonRpcMapper}. * * @see com.rabbitmq.client.RpcServer * @see JsonRpcClient + * @see JsonRpcMapper + * @see JacksonJsonRpcMapper */ public class JsonRpcServer extends StringRpcServer { private static final Logger LOGGER = LoggerFactory.getLogger(JsonRpcServer.class); - - /** Holds the JSON-RPC service description for this client. */ + private final JsonRpcMapper mapper; + /** + * Holds the JSON-RPC service description for this client. + */ public ServiceDescription serviceDescription; - /** The interface this server implements. */ + /** + * The interface this server implements. + */ public Class interfaceClass; - /** The instance backing this server. */ + /** + * The instance backing this server. + */ public Object interfaceInstance; + public JsonRpcServer(Channel channel, + Class interfaceClass, + Object interfaceInstance, JsonRpcMapper mapper) + throws IOException { + super(channel); + this.mapper = mapper; + init(interfaceClass, interfaceInstance); + } + /** * Construct a server that talks to the outside world using the * given channel, and constructs a fresh temporary * queue. Use getQueueName() to discover the created queue name. - * @param channel AMQP channel to use - * @param interfaceClass Java interface that this server is exposing to the world + * + * @param channel AMQP channel to use + * @param interfaceClass Java interface that this server is exposing to the world * @param interfaceInstance Java instance (of interfaceClass) that is being exposed * @throws IOException if something goes wrong during an AMQP operation */ public JsonRpcServer(Channel channel, - Class interfaceClass, - Object interfaceInstance) - throws IOException - { - super(channel); - init(interfaceClass, interfaceInstance); + Class interfaceClass, + Object interfaceInstance) + throws IOException { + this(channel, interfaceClass, interfaceInstance, new DefaultJsonRpcMapper()); } - private void init(Class interfaceClass, Object interfaceInstance) - { - this.interfaceClass = interfaceClass; - this.interfaceInstance = interfaceInstance; - this.serviceDescription = new ServiceDescription(interfaceClass); + public JsonRpcServer(Channel channel, + String queueName, + Class interfaceClass, + Object interfaceInstance, JsonRpcMapper mapper) + throws IOException { + super(channel, queueName); + this.mapper = mapper; + init(interfaceClass, interfaceInstance); } /** @@ -85,39 +103,43 @@ private void init(Class interfaceClass, Object interfaceInstance) * given channel and queue name. Our superclass, * RpcServer, expects the queue to exist at the time of * construction. - * @param channel AMQP channel to use - * @param queueName AMQP queue name to listen for requests on - * @param interfaceClass Java interface that this server is exposing to the world + * + * @param channel AMQP channel to use + * @param queueName AMQP queue name to listen for requests on + * @param interfaceClass Java interface that this server is exposing to the world * @param interfaceInstance Java instance (of interfaceClass) that is being exposed * @throws IOException if something goes wrong during an AMQP operation */ public JsonRpcServer(Channel channel, - String queueName, - Class interfaceClass, - Object interfaceInstance) - throws IOException - { - super(channel, queueName); - init(interfaceClass, interfaceInstance); + String queueName, + Class interfaceClass, + Object interfaceInstance) + throws IOException { + this(channel, queueName, interfaceClass, interfaceInstance, new DefaultJsonRpcMapper()); + } + + private void init(Class interfaceClass, Object interfaceInstance) { + this.interfaceClass = interfaceClass; + this.interfaceInstance = interfaceInstance; + this.serviceDescription = new ServiceDescription(interfaceClass); } /** * Override our superclass' method, dispatching to doCall. */ @Override - public String handleStringCall(String requestBody, AMQP.BasicProperties replyProperties) - { + public String handleStringCall(String requestBody, AMQP.BasicProperties replyProperties) { String replyBody = doCall(requestBody); return replyBody; } /** * Runs a single JSON-RPC request. + * * @param requestBody the JSON-RPC request string (a JSON encoded value) * @return a JSON-RPC response string (a JSON encoded value) */ - public String doCall(String requestBody) - { + public String doCall(String requestBody) { Object id; String method; Object[] params; @@ -126,20 +148,18 @@ public String doCall(String requestBody) LOGGER.debug("Request: {}", requestBody); } try { - @SuppressWarnings("unchecked") - Map request = (Map) new JSONReader().read(requestBody); + JsonRpcMapper.JsonRpcRequest request = mapper.parse(requestBody, serviceDescription); if (request == null) { response = errorResponse(null, 400, "Bad Request", null); - } else if (!ServiceDescription.JSON_RPC_VERSION.equals(request.get("version"))) { + } else if (!ServiceDescription.JSON_RPC_VERSION.equals(request.getVersion())) { response = errorResponse(null, 505, "JSONRPC version not supported", null); } else { - id = request.get("id"); - method = (String) request.get("method"); - List parmList = (List) request.get("params"); - params = parmList.toArray(); - if (method.equals("system.describe")) { + id = request.getId(); + method = request.getMethod(); + params = request.getParameters(); + if (request.isSystemDescribe()) { response = resultResponse(id, serviceDescription); - } else if (method.startsWith("system.")) { + } else if (request.isSystem()) { response = errorResponse(id, 403, "System methods forbidden", null); } else { Object result; @@ -169,7 +189,7 @@ public String doCall(String requestBody) } } catch (ClassCastException cce) { // Bogus request! - response = errorResponse(null, 400, "Bad Request", null); + response = errorResponse(null, 400, "Bad Request", null); } if (LOGGER.isDebugEnabled()) { @@ -181,13 +201,12 @@ public String doCall(String requestBody) /** * Retrieves the best matching method for the given method name and parameters. - * + *

* Subclasses may override this if they have specialised * dispatching requirements, so long as they continue to honour * their ServiceDescription. */ - public Method matchingMethod(String methodName, Object[] params) - { + public Method matchingMethod(String methodName, Object[] params) { ProcedureDescription proc = serviceDescription.getProcedure(methodName, params.length); return proc.internal_getMethod(); } @@ -197,7 +216,7 @@ public Method matchingMethod(String methodName, Object[] params) * ID given, using the code, message, and possible * (JSON-encodable) argument passed in. */ - public static String errorResponse(Object id, int code, String message, Object errorArg) { + private String errorResponse(Object id, int code, String message, Object errorArg) { Map err = new HashMap(); err.put("name", "JSONRPCError"); err.put("code", code); @@ -210,22 +229,21 @@ public static String errorResponse(Object id, int code, String message, Object e * Construct and encode a JSON-RPC success response for the * request ID given, using the result value passed in. */ - public static String resultResponse(Object id, Object result) { + private String resultResponse(Object id, Object result) { return response(id, "result", result); } /** * Private API - used by errorResponse and resultResponse. */ - public static String response(Object id, String label, Object value) { + private String response(Object id, String label, Object value) { Map resp = new HashMap(); resp.put("version", ServiceDescription.JSON_RPC_VERSION); if (id != null) { resp.put("id", id); } resp.put(label, value); - String respStr = new JSONWriter().write(resp); - //System.err.println(respStr); + String respStr = mapper.write(resp); return respStr; } diff --git a/src/main/java/com/rabbitmq/tools/jsonrpc/ProcedureDescription.java b/src/main/java/com/rabbitmq/tools/jsonrpc/ProcedureDescription.java index 57dcc39b05..714db1da96 100644 --- a/src/main/java/com/rabbitmq/tools/jsonrpc/ProcedureDescription.java +++ b/src/main/java/com/rabbitmq/tools/jsonrpc/ProcedureDescription.java @@ -39,6 +39,8 @@ public class ProcedureDescription { private ParameterDescription[] params; /** Return type for this procedure */ private String returnType; + private String javaReturnType; + private Class _javaReturnTypeAsClass; /** Reflected method object, used for service invocation */ private Method method; @@ -68,6 +70,7 @@ public ProcedureDescription(Method m) { params[i] = new ParameterDescription(i, parameterTypes[i]); } this.returnType = ParameterDescription.lookup(m.getReturnType()); + this.javaReturnType = m.getReturnType().getName(); } public ProcedureDescription() { @@ -82,6 +85,47 @@ public ProcedureDescription() { /** Private API - used to get the reflected method object, for servers */ public Method internal_getMethod() { return method; } + public String getJavaReturnType() { + return javaReturnType; + } + + public void setJavaReturnType(String javaReturnType) { + this.javaReturnType = javaReturnType; + this._javaReturnTypeAsClass = computeReturnTypeAsJavaClass(); + } + + public Class getReturnType() { + return _javaReturnTypeAsClass; + } + + private Class computeReturnTypeAsJavaClass() { + try { + if ("int".equals(javaReturnType)) { + return Integer.TYPE; + } else if ("double".equals(javaReturnType)) { + return Double.TYPE; + } else if ("long".equals(javaReturnType)) { + return Long.TYPE; + } else if ("boolean".equals(javaReturnType)) { + return Boolean.TYPE; + } else if ("char".equals(javaReturnType)) { + return Character.TYPE; + } else if ("byte".equals(javaReturnType)) { + return Byte.TYPE; + } else if ("short".equals(javaReturnType)) { + return Short.TYPE; + } else if ("float".equals(javaReturnType)) { + return Float.TYPE; + } else if ("void".equals(javaReturnType)) { + return Void.TYPE; + } else { + return Class.forName(javaReturnType); + } + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Unable to load class: " + javaReturnType, e); + } + } + /** Gets an array of parameter descriptions for all this procedure's parameters */ public ParameterDescription[] internal_getParams() { return params; diff --git a/src/main/java/com/rabbitmq/tools/jsonrpc/ServiceDescription.java b/src/main/java/com/rabbitmq/tools/jsonrpc/ServiceDescription.java index 79d750278b..4cb6d3bf66 100644 --- a/src/main/java/com/rabbitmq/tools/jsonrpc/ServiceDescription.java +++ b/src/main/java/com/rabbitmq/tools/jsonrpc/ServiceDescription.java @@ -48,7 +48,7 @@ public ServiceDescription(Map rawServiceDescription) { } public ServiceDescription(Class klass) { - this.procedures = new HashMap(); + this.procedures = new HashMap<>(); for (Method m: klass.getMethods()) { ProcedureDescription proc = new ProcedureDescription(m); addProcedure(proc); @@ -66,7 +66,7 @@ public Collection getProcs() { /** Private API - used via reflection during parsing/loading */ public void setProcs(Collection> p) { - procedures = new HashMap(); + procedures = new HashMap<>(); for (Map pm: p) { ProcedureDescription proc = new ProcedureDescription(pm); addProcedure(proc); diff --git a/src/test/java/com/rabbitmq/client/JsonRpcTest.java b/src/test/java/com/rabbitmq/client/AbstractJsonRpcTest.java similarity index 63% rename from src/test/java/com/rabbitmq/client/JsonRpcTest.java rename to src/test/java/com/rabbitmq/client/AbstractJsonRpcTest.java index b6f73c824e..079dcf00a7 100644 --- a/src/test/java/com/rabbitmq/client/JsonRpcTest.java +++ b/src/test/java/com/rabbitmq/client/AbstractJsonRpcTest.java @@ -17,17 +17,14 @@ import com.rabbitmq.client.test.TestUtils; import com.rabbitmq.tools.jsonrpc.JsonRpcClient; +import com.rabbitmq.tools.jsonrpc.JsonRpcMapper; import com.rabbitmq.tools.jsonrpc.JsonRpcServer; import org.junit.After; import org.junit.Before; -import org.junit.Test; -import java.lang.reflect.UndeclaredThrowableException; +import java.util.Date; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -public class JsonRpcTest { +public abstract class AbstractJsonRpcTest { Connection clientConnection, serverConnection; Channel clientChannel, serverChannel; @@ -36,6 +33,8 @@ public class JsonRpcTest { JsonRpcClient client; RpcService service; + abstract JsonRpcMapper createMapper(); + @Before public void init() throws Exception { clientConnection = TestUtils.connectionFactory().newConnection(); @@ -43,19 +42,15 @@ public void init() throws Exception { serverConnection = TestUtils.connectionFactory().newConnection(); serverChannel = serverConnection.createChannel(); serverChannel.queueDeclare(queue, false, false, false, null); - server = new JsonRpcServer(serverChannel, queue, RpcService.class, new DefaultRpcservice()); - new Thread(new Runnable() { - - @Override - public void run() { - try { - server.mainloop(); - } catch (Exception e) { - // safe to ignore when loops ends/server is canceled - } + server = new JsonRpcServer(serverChannel, queue, RpcService.class, new DefaultRpcservice(), createMapper()); + new Thread(() -> { + try { + server.mainloop(); + } catch (Exception e) { + // safe to ignore when loops ends/server is canceled } }).start(); - client = new JsonRpcClient(clientChannel, "", queue, 1000); + client = new JsonRpcClient(clientChannel, "", queue, 1000, createMapper()); service = client.createProxy(RpcService.class); } @@ -74,41 +69,16 @@ public void tearDown() throws Exception { serverConnection.close(); } - @Test - public void rpc() { - assertEquals("hello1", service.procedureString("hello")); - assertEquals(2, service.procedureInteger(1).intValue()); - assertEquals(2, service.procedurePrimitiveInteger(1)); - assertEquals(2, service.procedureDouble(1.0).intValue()); - assertEquals(2, (int) service.procedurePrimitiveDouble(1.0)); - - try { - assertEquals(2, (int) service.procedureLongToInteger(1L)); - fail("Long argument isn't supported"); - } catch (UndeclaredThrowableException e) { - // OK - } - assertEquals(2, service.procedurePrimitiveLongToInteger(1L)); - - try { - assertEquals(2, service.procedurePrimitiveLong(1L)); - fail("Long return type not supported"); - } catch (ClassCastException e) { - // OK - } + public interface RpcService { - try { - assertEquals(2, service.procedureLong(1L).longValue()); - fail("Long argument isn't supported"); - } catch (UndeclaredThrowableException e) { - // OK - } - } + boolean procedurePrimitiveBoolean(boolean input); - public interface RpcService { + Boolean procedureBoolean(Boolean input); String procedureString(String input); + String procedureStringString(String input1, String input2); + int procedurePrimitiveInteger(int input); Integer procedureInteger(Integer input); @@ -124,15 +94,40 @@ public interface RpcService { Long procedureLong(Long input); long procedurePrimitiveLong(long input); + + Pojo procedureIntegerToPojo(Integer id); + + String procedurePojoToString(Pojo pojo); + + void procedureException(); + + void procedureNoArgumentVoid(); + + Date procedureDateDate(Date date); } - public class DefaultRpcservice implements RpcService { + public static class DefaultRpcservice implements RpcService { + + @Override + public boolean procedurePrimitiveBoolean(boolean input) { + return !input; + } + + @Override + public Boolean procedureBoolean(Boolean input) { + return Boolean.valueOf(!input.booleanValue()); + } @Override public String procedureString(String input) { return input + 1; } + @Override + public String procedureStringString(String input1, String input2) { + return input1 + input2; + } + @Override public int procedurePrimitiveInteger(int input) { return input + 1; @@ -172,5 +167,45 @@ public Integer procedureLongToInteger(Long input) { public int procedurePrimitiveLongToInteger(long input) { return (int) input + 1; } + + @Override + public Pojo procedureIntegerToPojo(Integer id) { + Pojo pojo = new Pojo(); + pojo.setStringProperty(id.toString()); + return pojo; + } + + @Override + public String procedurePojoToString(Pojo pojo) { + return pojo.getStringProperty(); + } + + @Override + public void procedureException() { + throw new RuntimeException(); + } + + @Override + public void procedureNoArgumentVoid() { + + } + + @Override + public Date procedureDateDate(Date date) { + return date; + } + } + + public static class Pojo { + + private String stringProperty; + + public String getStringProperty() { + return stringProperty; + } + + public void setStringProperty(String stringProperty) { + this.stringProperty = stringProperty; + } } } diff --git a/src/test/java/com/rabbitmq/client/BlockingCellBenchmark.java b/src/test/java/com/rabbitmq/client/BlockingCellBenchmark.java deleted file mode 100644 index d65d01f956..0000000000 --- a/src/test/java/com/rabbitmq/client/BlockingCellBenchmark.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.rabbitmq.client; - -import com.rabbitmq.utility.BlockingCell; - -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -/** - * - */ -public class BlockingCellBenchmark { - - public static void main(String[] args) { - - } - - public void legacyBlockingCell() { - ExecutorService executorService = Executors.newFixedThreadPool(10); - - for (int i = 0; i < 1000; i++) { - final BlockingCell cell = new BlockingCell(); - executorService.submit(new Callable() { - - @Override - public Void call() throws Exception { - cell.set("whatever"); - return null; - } - }); - - cell.uninterruptibleGet(); - - } - - } - -} diff --git a/src/test/java/com/rabbitmq/client/DefaultJsonRpcTest.java b/src/test/java/com/rabbitmq/client/DefaultJsonRpcTest.java new file mode 100644 index 0000000000..049554d1d2 --- /dev/null +++ b/src/test/java/com/rabbitmq/client/DefaultJsonRpcTest.java @@ -0,0 +1,93 @@ +// Copyright (c) 2018 Pivotal Software, Inc. All rights reserved. +// +// This software, the RabbitMQ Java client library, is triple-licensed under the +// Mozilla Public License 1.1 ("MPL"), the GNU General Public License version 2 +// ("GPL") and the Apache License version 2 ("ASL"). For the MPL, please see +// LICENSE-MPL-RabbitMQ. For the GPL, please see LICENSE-GPL2. For the ASL, +// please see LICENSE-APACHE2. +// +// This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, +// either express or implied. See the LICENSE file for specific language governing +// rights and limitations of this software. +// +// If you have any questions regarding licensing, please contact us at +// info@rabbitmq.com. + +package com.rabbitmq.client; + +import com.rabbitmq.tools.jsonrpc.DefaultJsonRpcMapper; +import com.rabbitmq.tools.jsonrpc.JsonRpcException; +import com.rabbitmq.tools.jsonrpc.JsonRpcMapper; +import org.junit.Test; + +import java.lang.reflect.UndeclaredThrowableException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class DefaultJsonRpcTest extends AbstractJsonRpcTest { + + @Override + JsonRpcMapper createMapper() { + return new DefaultJsonRpcMapper(); + } + + @Test + public void rpc() { + assertFalse(service.procedurePrimitiveBoolean(true)); + assertFalse(service.procedureBoolean(Boolean.TRUE).booleanValue()); + assertEquals("hello1", service.procedureString("hello")); + assertEquals(2, service.procedureInteger(1).intValue()); + assertEquals(2, service.procedurePrimitiveInteger(1)); + assertEquals(2, service.procedureDouble(1.0).intValue()); + assertEquals(2, (int) service.procedurePrimitiveDouble(1.0)); + service.procedureNoArgumentVoid(); + + try { + service.procedureException(); + fail("Remote procedure throwing exception, an exception should have been thrown"); + } catch (UndeclaredThrowableException e) { + assertTrue(e.getCause() instanceof JsonRpcException); + } + + try { + assertEquals(2, (int) service.procedureLongToInteger(1L)); + fail("Long argument isn't supported"); + } catch (UndeclaredThrowableException e) { + // OK + } + assertEquals(2, service.procedurePrimitiveLongToInteger(1L)); + + try { + assertEquals(2, service.procedurePrimitiveLong(1L)); + fail("Long return type not supported"); + } catch (ClassCastException e) { + // OK + } + + try { + assertEquals(2, service.procedureLong(1L).longValue()); + fail("Long argument isn't supported"); + } catch (UndeclaredThrowableException e) { + // OK + } + + try { + assertEquals("123", service.procedureIntegerToPojo(123).getStringProperty()); + fail("Complex return type not supported"); + } catch (ClassCastException e) { + // OK + } + + try { + Pojo pojo = new Pojo(); + pojo.setStringProperty("hello"); + assertEquals("hello", service.procedurePojoToString(pojo)); + fail("Complex type argument not supported"); + } catch (UndeclaredThrowableException e) { + // OK + } + } +} \ No newline at end of file diff --git a/src/test/java/com/rabbitmq/client/JacksonJsonRpcTest.java b/src/test/java/com/rabbitmq/client/JacksonJsonRpcTest.java new file mode 100644 index 0000000000..091ce44680 --- /dev/null +++ b/src/test/java/com/rabbitmq/client/JacksonJsonRpcTest.java @@ -0,0 +1,72 @@ +// Copyright (c) 2018 Pivotal Software, Inc. All rights reserved. +// +// This software, the RabbitMQ Java client library, is triple-licensed under the +// Mozilla Public License 1.1 ("MPL"), the GNU General Public License version 2 +// ("GPL") and the Apache License version 2 ("ASL"). For the MPL, please see +// LICENSE-MPL-RabbitMQ. For the GPL, please see LICENSE-GPL2. For the ASL, +// please see LICENSE-APACHE2. +// +// This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, +// either express or implied. See the LICENSE file for specific language governing +// rights and limitations of this software. +// +// If you have any questions regarding licensing, please contact us at +// info@rabbitmq.com. + +package com.rabbitmq.client; + +import com.rabbitmq.tools.jsonrpc.JacksonJsonRpcMapper; +import com.rabbitmq.tools.jsonrpc.JsonRpcException; +import com.rabbitmq.tools.jsonrpc.JsonRpcMapper; +import org.junit.Test; + +import java.lang.reflect.UndeclaredThrowableException; +import java.util.Calendar; +import java.util.Date; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class JacksonJsonRpcTest extends AbstractJsonRpcTest { + + @Override + JsonRpcMapper createMapper() { + return new JacksonJsonRpcMapper(); + } + + @Test + public void rpc() { + assertFalse(service.procedurePrimitiveBoolean(true)); + assertFalse(service.procedureBoolean(Boolean.TRUE).booleanValue()); + assertEquals("hello1", service.procedureString("hello")); + assertEquals("hello1hello2", service.procedureStringString("hello1", "hello2")); + assertEquals(2, service.procedureInteger(1).intValue()); + assertEquals(2, service.procedurePrimitiveInteger(1)); + assertEquals(2, service.procedureDouble(1.0).intValue()); + assertEquals(2, (int) service.procedurePrimitiveDouble(1.0)); + assertEquals(2, (int) service.procedureLongToInteger(1L)); + assertEquals(2, service.procedurePrimitiveLongToInteger(1L)); + assertEquals(2, service.procedurePrimitiveLong(1L)); + assertEquals(2, service.procedureLong(1L).longValue()); + assertEquals("123", service.procedureIntegerToPojo(123).getStringProperty()); + service.procedureNoArgumentVoid(); + + Calendar calendar = Calendar.getInstance(); + Date date = calendar.getTime(); + Date returnedDate = service.procedureDateDate(date); + assertEquals(date.getTime(), returnedDate.getTime()); + + Pojo pojo = new Pojo(); + pojo.setStringProperty("hello"); + assertEquals("hello", service.procedurePojoToString(pojo)); + + try { + service.procedureException(); + fail("Remote procedure throwing exception, an exception should have been thrown"); + } catch (UndeclaredThrowableException e) { + assertTrue(e.getCause() instanceof JsonRpcException); + } + } +} diff --git a/src/test/java/com/rabbitmq/client/test/ClientTests.java b/src/test/java/com/rabbitmq/client/test/ClientTests.java index 9c2994b671..e79c40df73 100644 --- a/src/test/java/com/rabbitmq/client/test/ClientTests.java +++ b/src/test/java/com/rabbitmq/client/test/ClientTests.java @@ -16,7 +16,8 @@ package com.rabbitmq.client.test; -import com.rabbitmq.client.JsonRpcTest; +import com.rabbitmq.client.JacksonJsonRpcTest; +import com.rabbitmq.client.DefaultJsonRpcTest; import com.rabbitmq.utility.IntAllocatorTests; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -61,7 +62,8 @@ TestUtilsTest.class, StrictExceptionHandlerTest.class, NoAutoRecoveryWhenTcpWindowIsFullTest.class, - JsonRpcTest.class, + DefaultJsonRpcTest.class, + JacksonJsonRpcTest.class, AddressTest.class, DefaultRetryHandlerTest.class })