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