001 package com.thetransactioncompany.jsonrpc2;
002
003
004 import java.util.LinkedHashMap;
005 import java.util.LinkedList;
006 import java.util.List;
007 import java.util.Map;
008
009 import net.minidev.json.parser.ContainerFactory;
010 import net.minidev.json.parser.JSONParser;
011 import net.minidev.json.parser.ParseException;
012
013
014 /**
015 * Parses JSON-RPC 2.0 request, notification and response messages.
016 *
017 * <p>Parsing of batched requests / notifications is not supported.
018 *
019 * <p>This class is not thread-safe. A parser instance should not be used by
020 * more than one thread unless properly synchronised. Alternatively, you may
021 * use the thread-safe {@link JSONRPC2Message#parse} and its sister methods.
022 *
023 * <p>Example:
024 *
025 * <pre>
026 * String jsonString = "{\"method\":\"makePayment\"," +
027 * "\"params\":{\"recipient\":\"Penny Adams\",\"amount\":175.05}," +
028 * "\"id\":\"0001\","+
029 * "\"jsonrpc\":\"2.0\"}";
030 *
031 * JSONRPC2Request req = null;
032 *
033 * JSONRPC2Parser parser = new JSONRPC2Parser();
034 *
035 * try {
036 * req = parser.parseJSONRPC2Request(jsonString);
037 *
038 * } catch (JSONRPC2ParseException e) {
039 * // handle exception
040 * }
041 *
042 * </pre>
043 *
044 * <p id="map">The mapping between JSON and Java entities (as defined by the
045 * underlying JSON Smart library):
046 *
047 * <pre>
048 * true|false <---> java.lang.Boolean
049 * number <---> java.lang.Number
050 * string <---> java.lang.String
051 * array <---> java.util.List
052 * object <---> java.util.Map
053 * null <---> null
054 * </pre>
055 *
056 * <p>The JSON-RPC 2.0 specification and user group forum can be found
057 * <a href="http://groups.google.com/group/json-rpc">here</a>.
058 *
059 * @author Vladimir Dzhuvinov
060 */
061 public class JSONRPC2Parser {
062
063
064 /**
065 * Reusable JSON parser. Not thread-safe!
066 */
067 private JSONParser parser;
068
069
070 /**
071 * If {@code true} the order of the parsed JSON object members must be
072 * preserved.
073 */
074 private boolean preserveOrder;
075
076
077 /**
078 * If {@code true} the {@code "jsonrpc":"2.0"} version attribute in the
079 * JSON-RPC 2.0 message must be ignored during parsing.
080 */
081 private boolean ignoreVersion;
082
083
084 /**
085 * If {@code true} non-standard JSON-RPC 2.0 message attributes must be
086 * parsed too.
087 */
088 private boolean parseNonStdAttributes;
089
090
091 /**
092 * Creates a new JSON-RPC 2.0 message parser.
093 *
094 * <p>The member order of parsed JSON objects in parameters and results
095 * will not be preserved; strict checking of the 2.0 JSON-RPC version
096 * attribute will be enforced; non-standard message attributes will be
097 * ignored. Check the other constructors if you want to specify
098 * different behaviour.
099 */
100 public JSONRPC2Parser() {
101
102 this(false, false, false);
103 }
104
105
106 /**
107 * Creates a new JSON-RPC 2.0 message parser.
108 *
109 * <p>Strict checking of the 2.0 JSON-RPC version attribute will be
110 * enforced; non-standard message attributes will be ignored. Check the
111 * other constructors if you want to specify different behaviour.
112 *
113 * @param preserveOrder If {@code true} the member order of JSON objects
114 * in parameters and results will be preserved.
115 */
116 public JSONRPC2Parser(final boolean preserveOrder) {
117
118 this(preserveOrder, false, false);
119 }
120
121
122 /**
123 * Creates a new JSON-RPC 2.0 message parser.
124 *
125 * <p>Non-standard message attributes will be ignored. Check the other
126 * constructors if you want to specify different behaviour.
127 *
128 * @param preserveOrder If {@code true} the member order of JSON objects
129 * in parameters and results will be preserved.
130 * @param ignoreVersion If {@code true} the {@code "jsonrpc":"2.0"}
131 * version attribute in the JSON-RPC 2.0 message
132 * will not be checked.
133 */
134 public JSONRPC2Parser(final boolean preserveOrder,
135 final boolean ignoreVersion) {
136
137 this(preserveOrder, ignoreVersion, false);
138 }
139
140
141 /**
142 * Creates a new JSON-RPC 2.0 message parser.
143 *
144 * <p>This constructor allows full specification of the available
145 * JSON-RPC message parsing properties.
146 *
147 * @param preserveOrder If {@code true} the member order of JSON
148 * objects in parameters and results will
149 * be preserved.
150 * @param ignoreVersion If {@code true} the
151 * {@code "jsonrpc":"2.0"} version
152 * attribute in the JSON-RPC 2.0 message
153 * will not be checked.
154 * @param parseNonStdAttributes If {@code true} non-standard attributes
155 * found in the JSON-RPC 2.0 messages will
156 * be parsed too.
157 */
158 public JSONRPC2Parser(final boolean preserveOrder,
159 final boolean ignoreVersion,
160 final boolean parseNonStdAttributes) {
161
162 // Numbers parsed as long/double, requires JSON Smart 1.0.9+
163 parser = new JSONParser(JSONParser.MODE_JSON_SIMPLE);
164
165 this.preserveOrder = preserveOrder;
166 this.ignoreVersion = ignoreVersion;
167 this.parseNonStdAttributes = parseNonStdAttributes;
168 }
169
170
171 /**
172 * Parses a JSON object string. Provides the initial parsing of
173 * JSON-RPC 2.0 messages. The member order of JSON objects will be
174 * preserved if {@link #preserveOrder} is set to {@code true}.
175 *
176 * @param jsonString The JSON string to parse. Must not be
177 * {@code null}.
178 *
179 * @return The parsed JSON object.
180 *
181 * @throws JSONRPC2ParseException With detailed message if parsing
182 * failed.
183 */
184 @SuppressWarnings("unchecked")
185 private Map<String,Object> parseJSONObject(final String jsonString)
186 throws JSONRPC2ParseException {
187
188 if (jsonString.trim().length()==0)
189 throw new JSONRPC2ParseException("Invalid JSON: Empty string",
190 JSONRPC2ParseException.JSON,
191 jsonString);
192
193 Object json;
194
195 // Parse the JSON string
196 try {
197 if (preserveOrder)
198 json = parser.parse(jsonString, ContainerFactory.FACTORY_ORDERED);
199
200 else
201 json = parser.parse(jsonString);
202
203 } catch (ParseException e) {
204
205 // Terse message, do not include full parse exception message
206 throw new JSONRPC2ParseException("Invalid JSON",
207 JSONRPC2ParseException.JSON,
208 jsonString);
209 }
210
211 if (json instanceof List)
212 throw new JSONRPC2ParseException("JSON-RPC 2.0 batch requests/notifications not supported", jsonString);
213
214 if (! (json instanceof Map))
215 throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 message: Message must be a JSON object", jsonString);
216
217 return (Map<String,Object>)json;
218 }
219
220
221 /**
222 * Ensures the specified parameter is a {@code String} object set to
223 * "2.0". This method is intended to check the "jsonrpc" attribute
224 * during parsing of JSON-RPC messages.
225 *
226 * @param version The version parameter. Must not be {@code null}.
227 * @param jsonString The original JSON string.
228 *
229 * @throws JSONRPC2Exception If the parameter is not a string that
230 * equals "2.0".
231 */
232 private static void ensureVersion2(final Object version, final String jsonString)
233 throws JSONRPC2ParseException {
234
235 if (version == null)
236 throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0: Version string missing", jsonString);
237
238 else if (! (version instanceof String))
239 throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0: Version not a JSON string", jsonString);
240
241 else if (! version.equals("2.0"))
242 throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0: Version must be \"2.0\"", jsonString);
243 }
244
245
246 /**
247 * Provides common parsing of JSON-RPC 2.0 requests, notifications
248 * and responses. Use this method if you don't know which type of
249 * JSON-RPC message the input string represents.
250 *
251 * <p>If a particular message type is expected use the dedicated
252 * {@link #parseJSONRPC2Request}, {@link #parseJSONRPC2Notification}
253 * and {@link #parseJSONRPC2Response} methods. They are more efficient
254 * and would provide you with more detailed parse error reporting.
255 *
256 * @param jsonString A JSON string representing a JSON-RPC 2.0 request,
257 * notification or response, UTF-8 encoded. Must not
258 * be {@code null}.
259 *
260 * @return An instance of {@link JSONRPC2Request},
261 * {@link JSONRPC2Notification} or {@link JSONRPC2Response}.
262 *
263 * @throws JSONRPC2ParseException With detailed message if the parsing
264 * failed.
265 */
266 public JSONRPC2Message parseJSONRPC2Message(final String jsonString)
267 throws JSONRPC2ParseException {
268
269 // Try each of the parsers until one succeeds (or all fail)
270
271 try {
272 return parseJSONRPC2Request(jsonString);
273
274 } catch (JSONRPC2ParseException e) {
275
276 // throw on JSON error, ignore on protocol error
277 if (e.getCauseType() == JSONRPC2ParseException.JSON)
278 throw e;
279 }
280
281 try {
282 return parseJSONRPC2Notification(jsonString);
283
284 } catch (JSONRPC2ParseException e) {
285
286 // throw on JSON error, ignore on protocol error
287 if (e.getCauseType() == JSONRPC2ParseException.JSON)
288 throw e;
289 }
290
291 try {
292 return parseJSONRPC2Response(jsonString);
293
294 } catch (JSONRPC2ParseException e) {
295
296 // throw on JSON error, ignore on protocol error
297 if (e.getCauseType() == JSONRPC2ParseException.JSON)
298 throw e;
299 }
300
301 throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 message",
302 JSONRPC2ParseException.PROTOCOL,
303 jsonString);
304 }
305
306
307 /**
308 * Parses a JSON-RPC 2.0 request string.
309 *
310 * @param jsonString The JSON-RPC 2.0 request string, UTF-8 encoded.
311 * Must not be {@code null}.
312 *
313 * @return The corresponding JSON-RPC 2.0 request object.
314 *
315 * @throws JSONRPC2ParseException With detailed message if parsing
316 * failed.
317 */
318 @SuppressWarnings("unchecked")
319 public JSONRPC2Request parseJSONRPC2Request(final String jsonString)
320 throws JSONRPC2ParseException {
321
322 // Initial JSON object parsing
323 Map<String,Object> jsonObject = parseJSONObject(jsonString);
324
325
326 // Check for JSON-RPC version "2.0"
327 Object version = jsonObject.remove("jsonrpc");
328
329 if (! ignoreVersion)
330 ensureVersion2(version, jsonString);
331
332
333 // Extract method name
334 Object method = jsonObject.remove("method");
335
336 if (method == null)
337 throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 request: Method name missing", jsonString);
338
339 else if (! (method instanceof String))
340 throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 request: Method name not a JSON string", jsonString);
341
342 else if (((String)method).length() == 0)
343 throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 request: Method name is an empty string", jsonString);
344
345
346 // Extract ID
347 if (! jsonObject.containsKey("id"))
348 throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 request: Missing identifier", jsonString);
349
350 Object id = jsonObject.remove("id");
351
352 if ( id != null &&
353 !(id instanceof Number ) &&
354 !(id instanceof Boolean) &&
355 !(id instanceof String ) )
356 throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 request: Identifier not a JSON scalar", jsonString);
357
358
359 // Extract params
360 Object params = jsonObject.remove("params");
361
362
363 JSONRPC2Request request = null;
364
365 if (params == null)
366 request = new JSONRPC2Request((String)method, id);
367
368 else if (params instanceof List)
369 request = new JSONRPC2Request((String)method, (List<Object>)params, id);
370
371 else if (params instanceof Map)
372 request = new JSONRPC2Request((String)method, (Map<String,Object>)params, id);
373
374 else
375 throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 request: Method parameters have unexpected JSON type", jsonString);
376
377
378 // Extract remaining non-std params?
379 if (parseNonStdAttributes) {
380
381 for (Map.Entry<String,Object> entry: jsonObject.entrySet()) {
382
383 request.appendNonStdAttribute(entry.getKey(), entry.getValue());
384 }
385 }
386
387 return request;
388 }
389
390
391 /**
392 * Parses a JSON-RPC 2.0 notification string.
393 *
394 * @param jsonString The JSON-RPC 2.0 notification string, UTF-8
395 * encoded. Must not be {@code null}.
396 *
397 * @return The corresponding JSON-RPC 2.0 notification object.
398 *
399 * @throws JSONRPC2ParseException With detailed message if parsing
400 * failed.
401 */
402 @SuppressWarnings("unchecked")
403 public JSONRPC2Notification parseJSONRPC2Notification(final String jsonString)
404 throws JSONRPC2ParseException {
405
406 // Initial JSON object parsing
407 Map<String,Object> jsonObject = parseJSONObject(jsonString);
408
409
410 // Check for JSON-RPC version "2.0"
411 Object version = jsonObject.remove("jsonrpc");
412
413 if (! ignoreVersion)
414 ensureVersion2(version, jsonString);
415
416
417 // Extract method name
418 Object method = jsonObject.remove("method");
419
420 if (method == null)
421 throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 notification: Method name missing", jsonString);
422
423 else if (! (method instanceof String))
424 throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 notification: Method name not a JSON string", jsonString);
425
426 else if (((String)method).length() == 0)
427 throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 notification: Method name is an empty string", jsonString);
428
429
430 // Extract params
431 Object params = jsonObject.get("params");
432
433 JSONRPC2Notification notification = null;
434
435 if (params == null)
436 notification = new JSONRPC2Notification((String)method);
437
438 else if (params instanceof List)
439 notification = new JSONRPC2Notification((String)method, (List<Object>)params);
440
441 else if (params instanceof Map)
442 notification = new JSONRPC2Notification((String)method, (Map<String,Object>)params);
443 else
444 throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 notification: Method parameters have unexpected JSON type", jsonString);
445
446 // Extract remaining non-std params?
447 if (parseNonStdAttributes) {
448
449 for (Map.Entry<String,Object> entry: jsonObject.entrySet()) {
450
451 notification.appendNonStdAttribute(entry.getKey(), entry.getValue());
452 }
453 }
454
455 return notification;
456 }
457
458
459 /**
460 * Parses a JSON-RPC 2.0 response string.
461 *
462 * @param jsonString The JSON-RPC 2.0 response string, UTF-8 encoded.
463 * Must not be {@code null}.
464 *
465 * @return The corresponding JSON-RPC 2.0 response object.
466 *
467 * @throws JSONRPC2ParseException With detailed message if parsing
468 * failed.
469 */
470 @SuppressWarnings("unchecked")
471 public JSONRPC2Response parseJSONRPC2Response(final String jsonString)
472 throws JSONRPC2ParseException {
473
474 // Initial JSON object parsing
475 Map<String,Object> jsonObject = parseJSONObject(jsonString);
476
477 // Check for JSON-RPC version "2.0"
478 Object version = jsonObject.remove("jsonrpc");
479
480 if (! ignoreVersion)
481 ensureVersion2(version, jsonString);
482
483
484 // Extract request ID
485 Object id = jsonObject.remove("id");
486
487 if ( id != null &&
488 ! (id instanceof Boolean) &&
489 ! (id instanceof Number ) &&
490 ! (id instanceof String ) )
491 throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: Identifier not a JSON scalar", jsonString);
492
493
494 // Extract result/error and create response object
495 // Note: result and error are mutually exclusive
496
497 JSONRPC2Response response = null;
498
499 if (jsonObject.containsKey("result") && ! jsonObject.containsKey("error")) {
500
501 // Success
502 Object res = jsonObject.remove("result");
503
504 response = new JSONRPC2Response(res, id);
505
506 }
507 else if (! jsonObject.containsKey("result") && jsonObject.containsKey("error")) {
508
509 // Error JSON object
510 Object errorJSON = jsonObject.remove("error");
511
512 if (errorJSON == null)
513 throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: Missing error object", jsonString);
514
515
516 if (! (errorJSON instanceof Map))
517 throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: Error object not a JSON object");
518
519
520 Map<String,Object> error = (Map<String,Object>)errorJSON;
521
522
523 int errorCode;
524
525 try {
526 errorCode = ((Long)error.get("code")).intValue();
527
528 } catch (Exception e) {
529
530 throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: Error code missing or not an integer", jsonString);
531 }
532
533 String errorMessage = null;
534
535 try {
536 errorMessage = (String)error.get("message");
537
538 } catch (Exception e) {
539
540 throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: Error message missing or not a string", jsonString);
541 }
542
543 Object errorData = error.get("data");
544
545 response = new JSONRPC2Response(new JSONRPC2Error(errorCode, errorMessage, errorData), id);
546
547 }
548 else if (jsonObject.containsKey("result") && jsonObject.containsKey("error")) {
549
550 // Invalid response
551 throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: You cannot have result and error at the same time", jsonString);
552 }
553 else if (! jsonObject.containsKey("result") && ! jsonObject.containsKey("error")){
554
555 // Invalid response
556 throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: Neither result nor error specified", jsonString);
557 }
558 else {
559 throw new AssertionError();
560 }
561
562
563 // Extract remaining non-std params?
564 if (parseNonStdAttributes) {
565
566 for (Map.Entry<String,Object> entry: jsonObject.entrySet()) {
567
568 response.appendNonStdAttribute(entry.getKey(), entry.getValue());
569 }
570 }
571
572 return response;
573 }
574
575
576 /**
577 * Controls the preservation of JSON object member order in parsed
578 * JSON-RPC 2.0 messages.
579 *
580 * @param preserveOrder {@code true} to preserve the order of JSON
581 * object members, else {@code false}.
582 */
583 public void preserveOrder(final boolean preserveOrder) {
584
585 this.preserveOrder = preserveOrder;
586 }
587
588
589 /**
590 * Returns {@code true} if the order of JSON object members in parsed
591 * JSON-RPC 2.0 messages is preserved, else {@code false}.
592 *
593 * @return {@code true} if order is preserved, else {@code false}.
594 */
595 public boolean preservesOrder() {
596
597 return preserveOrder;
598 }
599
600
601 /**
602 * Specifies whether to ignore the {@code "jsonrpc":"2.0"} version
603 * attribute during parsing of JSON-RPC 2.0 messages.
604 *
605 * <p>You may with to disable strict 2.0 version checking if the parsed
606 * JSON-RPC 2.0 messages don't include a version attribute or if you
607 * wish to achieve limited compatibility with older JSON-RPC protocol
608 * versions.
609 *
610 * @param ignore {@code true} to skip checks of the
611 * {@code "jsonrpc":"2.0"} version attribute in parsed
612 * JSON-RPC 2.0 messages, else {@code false}.
613 */
614 public void ignoreVersion(final boolean ignore) {
615
616 ignoreVersion = ignore;
617 }
618
619
620 /**
621 * Returns {@code true} if the {@code "jsonrpc":"2.0"} version
622 * attribute in parsed JSON-RPC 2.0 messages is ignored, else
623 * {@code false}.
624 *
625 * @return {@code true} if the {@code "jsonrpc":"2.0"} version
626 * attribute in parsed JSON-RPC 2.0 messages is ignored, else
627 * {@code false}.
628 */
629 public boolean ignoresVersion() {
630
631 return ignoreVersion;
632 }
633
634
635 /**
636 * Specifies whether to parse non-standard attributes found in JSON-RPC
637 * 2.0 messages.
638 *
639 * @param enable {@code true} to parse non-standard attributes, else
640 * {@code false}.
641 */
642 public void parseNonStdAttributes(final boolean enable) {
643
644 parseNonStdAttributes = enable;
645 }
646
647
648 /**
649 * Returns {@code true} if non-standard attributes in JSON-RPC 2.0
650 * messages are parsed.
651 *
652 * @return {@code true} if non-standard attributes are parsed, else
653 * {@code false}.
654 */
655 public boolean parsesNonStdAttributes() {
656
657 return parseNonStdAttributes;
658 }
659 }