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