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