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