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 }