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