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