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}