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