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