All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.thetransactioncompany.jsonrpc2.JSONRPC2Parser Maven / Gradle / Ivy

Go to download

Java classes to represent, parse and serialise JSON-RPC 2.0 requests, notifications and responses.

There is a newer version: 2.2
Show newest version
package com.thetransactioncompany.jsonrpc2;


import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import net.minidev.json.parser.ContainerFactory;
import net.minidev.json.parser.JSONParser;
import net.minidev.json.parser.ParseException;


/**
 * Parses JSON-RPC 2.0 request, notification and response messages. 
 *
 * 

Parsing of batched requests / notifications is not supported. * *

This class is not thread-safe. A parser instance should not be used by * more than one thread unless properly synchronised. Alternatively, you may * use the thread-safe {@link JSONRPC2Message#parse} and its sister methods. * *

Example: * *

 * String jsonString = "{\"method\":\"makePayment\"," +
 *                      "\"params\":{\"recipient\":\"Penny Adams\",\"amount\":175.05}," +
 *                      "\"id\":\"0001\","+
 *                      "\"jsonrpc\":\"2.0\"}";
 *  
 *  JSONRPC2Request req = null;
 *
 * JSONRPC2Parser parser = new JSONRPC2Parser();
 *  
 *  try {
 *          req = parser.parseJSONRPC2Request(jsonString);
 * 
 *  } catch (JSONRPC2ParseException e) {
 *          // handle exception
 *  }
 *
 * 
* *

The mapping between JSON and Java entities (as defined by the * underlying JSON Smart library): * *

 *     true|false  <--->  java.lang.Boolean
 *     number      <--->  java.lang.Number
 *     string      <--->  java.lang.String
 *     array       <--->  java.util.List
 *     object      <--->  java.util.Map
 *     null        <--->  null
 * 
* *

The JSON-RPC 2.0 specification and user group forum can be found * here. * * @author Vladimir Dzhuvinov * @version 1.34.3 (2013-02-19) */ public class JSONRPC2Parser { /** * Reusable JSON parser. Not thread-safe! */ private JSONParser parser; /** * If {@code true} the order of the parsed JSON object members must be * preserved. */ private boolean preserveOrder; /** * If {@code true} the {@code "jsonrpc":"2.0"} version attribute in the * JSON-RPC 2.0 message must be ignored during parsing. */ private boolean ignoreVersion; /** * If {@code true} non-standard JSON-RPC 2.0 message attributes must be * parsed too. */ private boolean parseNonStdAttributes; /** * Creates a new JSON-RPC 2.0 message parser. * *

The member order of parsed JSON objects in parameters and results * will not be preserved; strict checking of the 2.0 JSON-RPC version * attribute will be enforced; non-standard message attributes will be * ignored. Check the other constructors if you want to specify * different behaviour. */ public JSONRPC2Parser() { this(false, false, false); } /** * Creates a new JSON-RPC 2.0 message parser. * *

Strict checking of the 2.0 JSON-RPC version attribute will be * enforced; non-standard message attributes will be ignored. Check the * other constructors if you want to specify different behaviour. * * @param preserveOrder If {@code true} the member order of JSON objects * in parameters and results will be preserved. */ public JSONRPC2Parser(final boolean preserveOrder) { this(preserveOrder, false, false); } /** * Creates a new JSON-RPC 2.0 message parser. * *

Non-standard message attributes will be ignored. Check the other * constructors if you want to specify different behaviour. * * @param preserveOrder If {@code true} the member order of JSON objects * in parameters and results will be preserved. * @param ignoreVersion If {@code true} the {@code "jsonrpc":"2.0"} * version attribute in the JSON-RPC 2.0 message * will not be checked. */ public JSONRPC2Parser(final boolean preserveOrder, final boolean ignoreVersion) { this(preserveOrder, ignoreVersion, false); } /** * Creates a new JSON-RPC 2.0 message parser. * *

This constructor allows full specification of the available * JSON-RPC message parsing properties. * * @param preserveOrder If {@code true} the member order of JSON * objects in parameters and results will * be preserved. * @param ignoreVersion If {@code true} the * {@code "jsonrpc":"2.0"} version * attribute in the JSON-RPC 2.0 message * will not be checked. * @param parseNonStdAttributes If {@code true} non-standard attributes * found in the JSON-RPC 2.0 messages will * be parsed too. */ public JSONRPC2Parser(final boolean preserveOrder, final boolean ignoreVersion, final boolean parseNonStdAttributes) { // Numbers parsed as long/double, requires JSON Smart 1.0.9+ parser = new JSONParser(JSONParser.MODE_JSON_SIMPLE); this.preserveOrder = preserveOrder; this.ignoreVersion = ignoreVersion; this.parseNonStdAttributes = parseNonStdAttributes; } /** * Parses a JSON object string. Provides the initial parsing of * JSON-RPC 2.0 messages. The member order of JSON objects will be * preserved if {@link #preserveOrder} is set to {@code true}. * * @param jsonString The JSON string to parse. Must not be * {@code null}. * * @return The parsed JSON object. * * @throws JSONRPC2ParseException With detailed message if parsing * failed. */ @SuppressWarnings("unchecked") private Map parseJSONObject(final String jsonString) throws JSONRPC2ParseException { if (jsonString.trim().length()==0) throw new JSONRPC2ParseException("Invalid JSON: Empty string", JSONRPC2ParseException.JSON, jsonString); Object json; // Parse the JSON string try { if (preserveOrder) json = parser.parse(jsonString, ContainerFactory.FACTORY_ORDERED); else json = parser.parse(jsonString); } catch (ParseException e) { // Terse message, do not include full parse exception message throw new JSONRPC2ParseException("Invalid JSON", JSONRPC2ParseException.JSON, jsonString); } if (json instanceof List) throw new JSONRPC2ParseException("JSON-RPC 2.0 batch requests/notifications not supported", jsonString); if (! (json instanceof Map)) throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 message: Message must be a JSON object", jsonString); return (Map)json; } /** * Ensures the specified parameter is a {@code String} object set to * "2.0". This method is intended to check the "jsonrpc" attribute * during parsing of JSON-RPC messages. * * @param version The version parameter. Must not be {@code null}. * @param jsonString The original JSON string. * * @throws JSONRPC2Exception If the parameter is not a string that * equals "2.0". */ private static void ensureVersion2(final Object version, final String jsonString) throws JSONRPC2ParseException { if (version == null) throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0: Version string missing", jsonString); else if (! (version instanceof String)) throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0: Version not a JSON string", jsonString); else if (! version.equals("2.0")) throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0: Version must be \"2.0\"", jsonString); } /** * Provides common parsing of JSON-RPC 2.0 requests, notifications * and responses. Use this method if you don't know which type of * JSON-RPC message the input string represents. * *

If a particular message type is expected use the dedicated * {@link #parseJSONRPC2Request}, {@link #parseJSONRPC2Notification} * and {@link #parseJSONRPC2Response} methods. They are more efficient * and would provide you with more detailed parse error reporting. * * @param jsonString A JSON string representing a JSON-RPC 2.0 request, * notification or response, UTF-8 encoded. Must not * be {@code null}. * * @return An instance of {@link JSONRPC2Request}, * {@link JSONRPC2Notification} or {@link JSONRPC2Response}. * * @throws JSONRPC2ParseException With detailed message if the parsing * failed. */ public JSONRPC2Message parseJSONRPC2Message(final String jsonString) throws JSONRPC2ParseException { // Try each of the parsers until one succeeds (or all fail) try { return parseJSONRPC2Request(jsonString); } catch (JSONRPC2ParseException e) { // throw on JSON error, ignore on protocol error if (e.getCauseType() == JSONRPC2ParseException.JSON) throw e; } try { return parseJSONRPC2Notification(jsonString); } catch (JSONRPC2ParseException e) { // throw on JSON error, ignore on protocol error if (e.getCauseType() == JSONRPC2ParseException.JSON) throw e; } try { return parseJSONRPC2Response(jsonString); } catch (JSONRPC2ParseException e) { // throw on JSON error, ignore on protocol error if (e.getCauseType() == JSONRPC2ParseException.JSON) throw e; } throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 message", JSONRPC2ParseException.PROTOCOL, jsonString); } /** * Parses a JSON-RPC 2.0 request string. * * @param jsonString The JSON-RPC 2.0 request string, UTF-8 encoded. * Must not be {@code null}. * * @return The corresponding JSON-RPC 2.0 request object. * * @throws JSONRPC2ParseException With detailed message if parsing * failed. */ @SuppressWarnings("unchecked") public JSONRPC2Request parseJSONRPC2Request(final String jsonString) throws JSONRPC2ParseException { // Initial JSON object parsing Map jsonObject = parseJSONObject(jsonString); // Check for JSON-RPC version "2.0" Object version = jsonObject.remove("jsonrpc"); if (! ignoreVersion) ensureVersion2(version, jsonString); // Extract method name Object method = jsonObject.remove("method"); if (method == null) throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 request: Method name missing", jsonString); else if (! (method instanceof String)) throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 request: Method name not a JSON string", jsonString); else if (((String)method).length() == 0) throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 request: Method name is an empty string", jsonString); // Extract ID if (! jsonObject.containsKey("id")) throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 request: Missing identifier", jsonString); Object id = jsonObject.remove("id"); if ( id != null && !(id instanceof Number ) && !(id instanceof Boolean) && !(id instanceof String ) ) throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 request: Identifier not a JSON scalar", jsonString); // Extract params Object params = jsonObject.remove("params"); JSONRPC2Request request = null; if (params == null) request = new JSONRPC2Request((String)method, id); else if (params instanceof List) request = new JSONRPC2Request((String)method, (List)params, id); else if (params instanceof Map) request = new JSONRPC2Request((String)method, (Map)params, id); else throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 request: Method parameters have unexpected JSON type", jsonString); // Extract remaining non-std params? if (parseNonStdAttributes) { for (Map.Entry entry: jsonObject.entrySet()) { request.appendNonStdAttribute(entry.getKey(), entry.getValue()); } } return request; } /** * Parses a JSON-RPC 2.0 notification string. * * @param jsonString The JSON-RPC 2.0 notification string, UTF-8 * encoded. Must not be {@code null}. * * @return The corresponding JSON-RPC 2.0 notification object. * * @throws JSONRPC2ParseException With detailed message if parsing * failed. */ @SuppressWarnings("unchecked") public JSONRPC2Notification parseJSONRPC2Notification(final String jsonString) throws JSONRPC2ParseException { // Initial JSON object parsing Map jsonObject = parseJSONObject(jsonString); // Check for JSON-RPC version "2.0" Object version = jsonObject.remove("jsonrpc"); if (! ignoreVersion) ensureVersion2(version, jsonString); // Extract method name Object method = jsonObject.remove("method"); if (method == null) throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 notification: Method name missing", jsonString); else if (! (method instanceof String)) throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 notification: Method name not a JSON string", jsonString); else if (((String)method).length() == 0) throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 notification: Method name is an empty string", jsonString); // Extract params Object params = jsonObject.get("params"); JSONRPC2Notification notification = null; if (params == null) notification = new JSONRPC2Notification((String)method); else if (params instanceof List) notification = new JSONRPC2Notification((String)method, (List)params); else if (params instanceof Map) notification = new JSONRPC2Notification((String)method, (Map)params); else throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 notification: Method parameters have unexpected JSON type", jsonString); // Extract remaining non-std params? if (parseNonStdAttributes) { for (Map.Entry entry: jsonObject.entrySet()) { notification.appendNonStdAttribute(entry.getKey(), entry.getValue()); } } return notification; } /** * Parses a JSON-RPC 2.0 response string. * * @param jsonString The JSON-RPC 2.0 response string, UTF-8 encoded. * Must not be {@code null}. * * @return The corresponding JSON-RPC 2.0 response object. * * @throws JSONRPC2ParseException With detailed message if parsing * failed. */ @SuppressWarnings("unchecked") public JSONRPC2Response parseJSONRPC2Response(final String jsonString) throws JSONRPC2ParseException { // Initial JSON object parsing Map jsonObject = parseJSONObject(jsonString); // Check for JSON-RPC version "2.0" Object version = jsonObject.remove("jsonrpc"); if (! ignoreVersion) ensureVersion2(version, jsonString); // Extract request ID Object id = jsonObject.remove("id"); if ( id != null && ! (id instanceof Boolean) && ! (id instanceof Number ) && ! (id instanceof String ) ) throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: Identifier not a JSON scalar", jsonString); // Extract result/error and create response object // Note: result and error are mutually exclusive JSONRPC2Response response = null; if (jsonObject.containsKey("result") && ! jsonObject.containsKey("error")) { // Success Object res = jsonObject.remove("result"); response = new JSONRPC2Response(res, id); } else if (! jsonObject.containsKey("result") && jsonObject.containsKey("error")) { // Error JSON object Object errorJSON = jsonObject.remove("error"); if (errorJSON == null) throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: Missing error object", jsonString); if (! (errorJSON instanceof Map)) throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: Error object not a JSON object"); Map error = (Map)errorJSON; int errorCode; try { errorCode = ((Long)error.get("code")).intValue(); } catch (Exception e) { throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: Error code missing or not an integer", jsonString); } String errorMessage = null; try { errorMessage = (String)error.get("message"); } catch (Exception e) { throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: Error message missing or not a string", jsonString); } Object errorData = error.get("data"); response = new JSONRPC2Response(new JSONRPC2Error(errorCode, errorMessage, errorData), id); } else if (jsonObject.containsKey("result") && jsonObject.containsKey("error")) { // Invalid response throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: You cannot have result and error at the same time", jsonString); } else if (! jsonObject.containsKey("result") && ! jsonObject.containsKey("error")){ // Invalid response throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: Neither result nor error specified", jsonString); } else { throw new AssertionError(); } // Extract remaining non-std params? if (parseNonStdAttributes) { for (Map.Entry entry: jsonObject.entrySet()) { response.appendNonStdAttribute(entry.getKey(), entry.getValue()); } } return response; } /** * Controls the preservation of JSON object member order in parsed * JSON-RPC 2.0 messages. * * @param preserveOrder {@code true} to preserve the order of JSON * object members, else {@code false}. */ public void preserveOrder(final boolean preserveOrder) { this.preserveOrder = preserveOrder; } /** * Returns {@code true} if the order of JSON object members in parsed * JSON-RPC 2.0 messages is preserved, else {@code false}. * * @return {@code true} if order is preserved, else {@code false}. */ public boolean preservesOrder() { return preserveOrder; } /** * Specifies whether to ignore the {@code "jsonrpc":"2.0"} version * attribute during parsing of JSON-RPC 2.0 messages. * *

You may with to disable strict 2.0 version checking if the parsed * JSON-RPC 2.0 messages don't include a version attribute or if you * wish to achieve limited compatibility with older JSON-RPC protocol * versions. * * @param ignore {@code true} to skip checks of the * {@code "jsonrpc":"2.0"} version attribute in parsed * JSON-RPC 2.0 messages, else {@code false}. */ public void ignoreVersion(final boolean ignore) { ignoreVersion = ignore; } /** * Returns {@code true} if the {@code "jsonrpc":"2.0"} version * attribute in parsed JSON-RPC 2.0 messages is ignored, else * {@code false}. * * @return {@code true} if the {@code "jsonrpc":"2.0"} version * attribute in parsed JSON-RPC 2.0 messages is ignored, else * {@code false}. */ public boolean ignoresVersion() { return ignoreVersion; } /** * Specifies whether to parse non-standard attributes found in JSON-RPC * 2.0 messages. * * @param enable {@code true} to parse non-standard attributes, else * {@code false}. */ public void parseNonStdAttributes(final boolean enable) { parseNonStdAttributes = enable; } /** * Returns {@code true} if non-standard attributes in JSON-RPC 2.0 * messages are parsed. * * @return {@code true} if non-standard attributes are parsed, else * {@code false}. */ public boolean parsesNonStdAttributes() { return parseNonStdAttributes; } }