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