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