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