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