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    }