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