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