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