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