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}