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