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