001/*
002 * nimbus-jose-jwt
003 *
004 * Copyright 2012-2016, Connect2id Ltd.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
007 * this file except in compliance with the License. You may obtain a copy of the
008 * License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software distributed
013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
015 * specific language governing permissions and limitations under the License.
016 */
017
018package com.nimbusds.jose.util;
019
020
021import java.net.URI;
022import java.net.URISyntaxException;
023import java.text.ParseException;
024import java.util.Arrays;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028
029import net.minidev.json.JSONObject;
030import net.minidev.json.parser.JSONParser;
031
032
033/**
034 * JSON object helper methods.
035 *
036 * @author Vladimir Dzhuvinov
037 * @version 2021-10-08
038 */
039public class JSONObjectUtils {
040
041
042        /**
043         * Parses a JSON object.
044         *
045         * <p>Specific JSON to Java entity mapping (as per JSON Smart):
046         *
047         * <ul>
048         *     <li>JSON true|false map to {@code java.lang.Boolean}.
049         *     <li>JSON numbers map to {@code java.lang.Number}.
050         *         <ul>
051         *             <li>JSON integer numbers map to {@code long}.
052         *             <li>JSON fraction numbers map to {@code double}.
053         *         </ul>
054         *     <li>JSON strings map to {@code java.lang.String}.
055         *     <li>JSON arrays map to {@code java.util.List<Object>}.
056         *     <li>JSON objects map to {@code java.util.Map<String,Object>}.
057         * </ul>
058         *
059         * @param s The JSON object string to parse. Must not be {@code null}.
060         *
061         * @return The JSON object.
062         *
063         * @throws ParseException If the string cannot be parsed to a valid JSON 
064         *                        object.
065         */
066        public static Map<String, Object> parse(final String s)
067                throws ParseException {
068
069                return parse(s, -1);
070        }
071
072
073        /**
074         * Parses a JSON object with the option to limit the input string size.
075         *
076         * <p>Specific JSON to Java entity mapping (as per JSON Smart):
077         *
078         * <ul>
079         *     <li>JSON true|false map to {@code java.lang.Boolean}.
080         *     <li>JSON numbers map to {@code java.lang.Number}.
081         *         <ul>
082         *             <li>JSON integer numbers map to {@code long}.
083         *             <li>JSON fraction numbers map to {@code double}.
084         *         </ul>
085         *     <li>JSON strings map to {@code java.lang.String}.
086         *     <li>JSON arrays map to {@code java.util.List<Object>}.
087         *     <li>JSON objects map to {@code java.util.Map<String,Object>}.
088         * </ul>
089         *
090         * @param s         The JSON object string to parse. Must not be
091         *                  {@code null}.
092         * @param sizeLimit The max allowed size of the string to parse. A
093         *                  negative integer means no limit.
094         *
095         * @return The JSON object.
096         *
097         * @throws ParseException If the string cannot be parsed to a valid JSON
098         *                        object.
099         */
100        public static Map<String, Object> parse(final String s, final int sizeLimit)
101                throws ParseException {
102
103                if (sizeLimit >= 0 && s.length() > sizeLimit) {
104                        throw new ParseException("The parsed string is longer than the max accepted size of " + sizeLimit + " characters", 0);
105                }
106                
107                Object o;
108                try {
109                        o = new JSONParser(JSONParser.USE_HI_PRECISION_FLOAT | JSONParser.ACCEPT_TAILLING_SPACE).parse(s);
110                } catch (net.minidev.json.parser.ParseException e) {
111                        throw new ParseException("Invalid JSON: " + e.getMessage(), 0);
112                } catch (Exception e) {
113                        throw new ParseException("Unexpected exception: " + e.getMessage(), 0);
114                } catch (StackOverflowError e) {
115                        throw new ParseException("Excessive JSON object and / or array nesting", 0);
116                }
117
118                if (o instanceof JSONObject) {
119                        return (JSONObject)o;
120                } else {
121                        throw new ParseException("JSON entity is not an object", 0);
122                }
123        }
124
125
126        /**
127         * Use {@link #parse(String)} instead.
128         *
129         * @param s The JSON object string to parse. Must not be {@code null}.
130         *
131         * @return The JSON object.
132         *
133         * @throws ParseException If the string cannot be parsed to a valid JSON
134         *                        object.
135         */
136        @Deprecated
137        public static Map<String, Object> parseJSONObject(final String s)
138                throws ParseException {
139
140                return parse(s);
141        }
142
143
144        /**
145         * Gets a generic member of a JSON object.
146         *
147         * @param o     The JSON object. Must not be {@code null}.
148         * @param key   The JSON object member key. Must not be {@code null}.
149         * @param clazz The expected class of the JSON object member value. Must
150         *              not be {@code null}.
151         *
152         * @return The JSON object member value, may be {@code null}.
153         *
154         * @throws ParseException If the value is not of the expected type.
155         */
156        private static <T> T getGeneric(final Map<String, Object> o, final String key, final Class<T> clazz)
157                throws ParseException {
158
159                if (o.get(key) == null) {
160                        return null;
161                }
162
163                Object value = o.get(key);
164
165                if (! clazz.isAssignableFrom(value.getClass())) {
166                        throw new ParseException("Unexpected type of JSON object member with key " + key + "", 0);
167                }
168                
169                @SuppressWarnings("unchecked")
170                T castValue = (T)value;
171                return castValue;
172        }
173
174
175        /**
176         * Gets a boolean member of a JSON object.
177         *
178         * @param o   The JSON object. Must not be {@code null}.
179         * @param key The JSON object member key. Must not be {@code null}.
180         *
181         * @return The JSON object member value.
182         *
183         * @throws ParseException If the member is missing, the value is
184         *                        {@code null} or not of the expected type.
185         */
186        public static boolean getBoolean(final Map<String, Object> o, final String key)
187                throws ParseException {
188
189                Boolean value = getGeneric(o, key, Boolean.class);
190                
191                if (value == null) {
192                        throw new ParseException("JSON object member with key " + key + " is missing or null", 0);
193                }
194                
195                return value;
196        }
197
198
199        /**
200         * Gets an number member of a JSON object as {@code int}.
201         *
202         * @param o   The JSON object. Must not be {@code null}.
203         * @param key The JSON object member key. Must not be {@code null}.
204         *
205         * @return The JSON object member value.
206         *
207         * @throws ParseException If the member is missing, the value is
208         *                        {@code null} or not of the expected type.
209         */
210        public static int getInt(final Map<String, Object> o, final String key)
211                throws ParseException {
212
213                Number value = getGeneric(o, key, Number.class);
214                
215                if (value == null) {
216                        throw new ParseException("JSON object member with key " + key + " is missing or null", 0);
217                }
218                
219                return value.intValue();
220        }
221
222
223        /**
224         * Gets a number member of a JSON object as {@code long}.
225         *
226         * @param o   The JSON object. Must not be {@code null}.
227         * @param key The JSON object member key. Must not be {@code null}.
228         *
229         * @return The JSON object member value.
230         *
231         * @throws ParseException If the member is missing, the value is
232         *                        {@code null} or not of the expected type.
233         */
234        public static long getLong(final Map<String, Object> o, final String key)
235                throws ParseException {
236
237                Number value = getGeneric(o, key, Number.class);
238                
239                if (value == null) {
240                        throw new ParseException("JSON object member with key " + key + " is missing or null", 0);
241                }
242                
243                return value.longValue();
244        }
245
246
247        /**
248         * Gets a number member of a JSON object {@code float}.
249         *
250         * @param o   The JSON object. Must not be {@code null}.
251         * @param key The JSON object member key. Must not be {@code null}.
252         *
253         * @return The JSON object member value, may be {@code null}.
254         *
255         * @throws ParseException If the member is missing, the value is
256         *                        {@code null} or not of the expected type.
257         */
258        public static float getFloat(final Map<String, Object> o, final String key)
259                throws ParseException {
260
261                Number value = getGeneric(o, key, Number.class);
262                
263                if (value == null) {
264                        throw new ParseException("JSON object member with key " + key + " is missing or null", 0);
265                }
266                
267                return value.floatValue();
268        }
269
270
271        /**
272         * Gets a number member of a JSON object as {@code double}.
273         *
274         * @param o   The JSON object. Must not be {@code null}.
275         * @param key The JSON object member key. Must not be {@code null}.
276         *
277         * @return The JSON object member value, may be {@code null}.
278         *
279         * @throws ParseException If the member is missing, the value is
280         *                        {@code null} or not of the expected type.
281         */
282        public static double getDouble(final Map<String, Object> o, final String key)
283                throws ParseException {
284
285                Number value = getGeneric(o, key, Number.class);
286                
287                if (value == null) {
288                        throw new ParseException("JSON object member with key " + key + " is missing or null", 0);
289                }
290                
291                return value.doubleValue();
292        }
293
294
295        /**
296         * Gets a string member of a JSON object.
297         *
298         * @param o   The JSON object. Must not be {@code null}.
299         * @param key The JSON object member key. Must not be {@code null}.
300         *
301         * @return The JSON object member value, may be {@code null}.
302         *
303         * @throws ParseException If the value is not of the expected type.
304         */
305        public static String getString(final Map<String, Object> o, final String key)
306                throws ParseException {
307
308                return getGeneric(o, key, String.class);
309        }
310
311
312        /**
313         * Gets a string member of a JSON object as {@code java.net.URI}.
314         *
315         * @param o   The JSON object. Must not be {@code null}.
316         * @param key The JSON object member key. Must not be {@code null}.
317         *
318         * @return The JSON object member value, may be {@code null}.
319         *
320         * @throws ParseException If the value is not of the expected type.
321         */
322        public static URI getURI(final Map<String, Object> o, final String key)
323                throws ParseException {
324
325                String value = getString(o, key);
326                
327                if (value == null) {
328                        return null;
329                }
330                
331                try {
332                        return new URI(value);
333
334                } catch (URISyntaxException e) {
335
336                        throw new ParseException(e.getMessage(), 0);
337                }
338        }
339
340
341        /**
342         * Gets a JSON array member of a JSON object.
343         *
344         * @param o   The JSON object. Must not be {@code null}.
345         * @param key The JSON object member key. Must not be {@code null}.
346         *
347         * @return The JSON object member value, may be {@code null}.
348         *
349         * @throws ParseException If the value is not of the expected type.
350         */
351        public static List<Object> getJSONArray(final Map<String, Object> o, final String key)
352                throws ParseException {
353                
354                @SuppressWarnings("unchecked")
355                List<Object> jsonArray = getGeneric(o, key, List.class);
356                return jsonArray;
357        }
358
359
360        /**
361         * Gets a string array member of a JSON object.
362         *
363         * @param o   The JSON object. Must not be {@code null}.
364         * @param key The JSON object member key. Must not be {@code null}.
365         *
366         * @return The JSON object member value, may be {@code null}.
367         *
368         * @throws ParseException If the value is not of the expected type.
369         */
370        public static String[] getStringArray(final Map<String, Object> o, final String key)
371                throws ParseException {
372
373                List<Object> jsonArray = getJSONArray(o, key);
374                
375                if (jsonArray == null) {
376                        return null;
377                }
378
379                try {
380                        return jsonArray.toArray(new String[0]);
381                } catch (ArrayStoreException e) {
382                        throw new ParseException("JSON object member with key \"" + key + "\" is not an array of strings", 0);
383                }
384        }
385
386        /**
387         * Gets a JSON objects array member of a JSON object.
388         *
389         * @param o   The JSON object. Must not be {@code null}.
390         * @param key The JSON object member key. Must not be {@code null}.
391         *
392         * @return The JSON object member value, may be {@code null}.
393         *
394         * @throws ParseException If the value is not of the expected type.
395         */
396        public static Map<String, Object>[] getJSONObjectArray(final Map<String, Object> o, final String key)
397                throws ParseException {
398
399                List<Object> jsonArray = getJSONArray(o, key);
400
401                if (jsonArray == null) {
402                        return null;
403                }
404
405                try {
406                        return jsonArray.toArray(new HashMap[0]);
407                } catch (ArrayStoreException e) {
408                        throw new ParseException("JSON object member with key \"" + key + "\" is not an array of JSON objects", 0);
409                }
410        }
411        
412        /**
413         * Gets a string list member of a JSON object
414         * 
415         * @param o   The JSON object. Must not be {@code null}.
416         * @param key The JSON object member key. Must not be {@code null}.
417         *
418         * @return The JSON object member value, may be {@code null}.
419         *
420         * @throws ParseException If the value is not of the expected type.
421         */
422        public static List<String> getStringList(final Map<String, Object> o, final String key) throws ParseException {
423
424                String[] array = getStringArray(o, key);
425                
426                if (array == null) {
427                        return null;
428                }
429
430                return Arrays.asList(array);
431        }
432        
433
434        /**
435         * Gets a JSON object member of a JSON object.
436         *
437         * @param o   The JSON object. Must not be {@code null}.
438         * @param key The JSON object member key. Must not be {@code null}.
439         *
440         * @return The JSON object member value, may be {@code null}.
441         *
442         * @throws ParseException If the value is not of the expected type.
443         */
444        public static Map<String, Object> getJSONObject(final Map<String, Object> o, final String key)
445                throws ParseException {
446                
447                Map<?,?> jsonObject = getGeneric(o, key, Map.class);
448                
449                if (jsonObject == null) {
450                        return null;
451                }
452                
453                // Verify keys are String
454                for (Object oKey: jsonObject.keySet()) {
455                        if (! (oKey instanceof String)) {
456                                throw new ParseException("JSON object member with key " + key + " not a JSON object", 0);
457                        }
458                }
459                @SuppressWarnings("unchecked")
460                Map<String, Object> castJSONObject = (Map<String, Object>)jsonObject;
461                return castJSONObject;
462        }
463        
464        
465        /**
466         * Gets a string member of a JSON object as {@link Base64URL}.
467         *
468         * @param o   The JSON object. Must not be {@code null}.
469         * @param key The JSON object member key. Must not be {@code null}.
470         *
471         * @return The JSON object member value, may be {@code null}.
472         *
473         * @throws ParseException If the value is not of the expected type.
474         */
475        public static Base64URL getBase64URL(final Map<String, Object> o, final String key)
476                throws ParseException {
477                
478                String value = getString(o, key);
479                
480                if (value == null) {
481                        return null;
482                }
483                
484                return new Base64URL(value);
485        }
486        
487        
488        /**
489         * Serialises the specified map to a JSON object using the entity
490         * mapping specified in {@link #parse(String)}.
491         *
492         * @param o The map. Must not be {@code null}.
493         *
494         * @return The JSON object as string.
495         */
496        public static String toJSONString(final Map<String, ?> o) {
497                return JSONObject.toJSONString(o);
498        }
499
500        /**
501         * Creates a new JSON object (unordered).
502         *
503         * @return The new empty JSON object.
504         */
505        public static Map<String, Object> newJSONObject() {
506                return new HashMap<>();
507        }
508        
509        
510        /**
511         * Prevents public instantiation.
512         */
513        private JSONObjectUtils() { }
514}
515