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