001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2010–2011 ApexIdentity Inc. 015 * Portions Copyright 2011-2014 ForgeRock AS. 016 */ 017 018package org.forgerock.openig.util; 019 020import static com.fasterxml.jackson.core.JsonParser.Feature.*; 021import static java.lang.String.*; 022import static java.util.Collections.*; 023import static org.forgerock.openig.util.Loader.*; 024 025import java.io.IOException; 026import java.io.InputStream; 027import java.io.InputStreamReader; 028import java.io.Reader; 029import java.io.StringReader; 030import java.util.Arrays; 031import java.util.LinkedHashMap; 032import java.util.LinkedList; 033import java.util.List; 034import java.util.Map; 035 036import org.forgerock.json.fluent.JsonException; 037import org.forgerock.json.fluent.JsonTransformer; 038import org.forgerock.json.fluent.JsonValue; 039import org.forgerock.json.fluent.JsonValueException; 040import org.forgerock.openig.alias.ClassAliasResolver; 041import org.forgerock.openig.el.Expression; 042import org.forgerock.openig.el.ExpressionException; 043import org.forgerock.openig.heap.Heap; 044import org.forgerock.openig.heap.HeapException; 045import org.forgerock.openig.log.Logger; 046import org.forgerock.util.Utils; 047import org.forgerock.util.promise.Function; 048 049import com.fasterxml.jackson.core.JsonParser; 050import com.fasterxml.jackson.core.JsonToken; 051import com.fasterxml.jackson.core.type.TypeReference; 052import com.fasterxml.jackson.databind.ObjectMapper; 053 054 055/** 056 * Provides additional functionality to JsonValue. 057 */ 058public final class Json { 059 060 /** Non strict object mapper / data binder used to read json configuration files/data. */ 061 private static final ObjectMapper LENIENT_MAPPER; 062 static { 063 LENIENT_MAPPER = new ObjectMapper(); 064 LENIENT_MAPPER.configure(ALLOW_COMMENTS, true); 065 LENIENT_MAPPER.configure(ALLOW_SINGLE_QUOTES, true); 066 LENIENT_MAPPER.configure(ALLOW_UNQUOTED_CONTROL_CHARS, true); 067 } 068 069 /** Strict object mapper / data binder used to read json configuration files/data. */ 070 private static final ObjectMapper STRICT_MAPPER = new ObjectMapper(); 071 072 /** List of alias service providers found at initialization time. */ 073 private static final List<ClassAliasResolver> CLASS_ALIAS_RESOLVERS = 074 unmodifiableList(loadList(ClassAliasResolver.class)); 075 076 private static final Function<JsonValue, Expression, HeapException> OF_EXPRESSION = 077 new Function<JsonValue, Expression, HeapException>() { 078 @Override 079 public Expression apply(final JsonValue value) throws HeapException { 080 return asExpression(value); 081 } 082 }; 083 084 /** 085 * Resolves a String-based {@link JsonValue} instance that may contains an {@link Expression}. 086 */ 087 private static final JsonTransformer EXPRESSION_TRANSFORMER = new JsonTransformer() { 088 @Override 089 public void transform(final JsonValue value) { 090 if (value.isString()) { 091 try { 092 Expression expression = new Expression(value.asString()); 093 value.setObject(expression.eval(null, String.class)); 094 } catch (ExpressionException e) { 095 throw new JsonException(format("Expression '%s' (in %s) is not syntactically correct", 096 value.asString(), 097 value.getPointer()), e); 098 } 099 } 100 } 101 }; 102 103 /** 104 * Private constructor for utility class. 105 */ 106 private Json() { } 107 108 private static Class<?> classForName(JsonValue value) { 109 String name = value.asString(); 110 // Looks for registered aliases first 111 Class<?> type = resolveAlias(name); 112 if (type != null) { 113 return type; 114 } 115 // No alias found, consider the value as a fully qualified class name 116 try { 117 return Class.forName(name, true, Thread.currentThread().getContextClassLoader()); 118 } catch (ClassNotFoundException cnfe) { 119 throw new JsonValueException(value, cnfe); 120 } 121 } 122 123 /** 124 * Resolve a given alias against the known aliases service providers. 125 * The first {@literal non-null} resolved type is returned. 126 */ 127 private static Class<?> resolveAlias(final String alias) { 128 for (ClassAliasResolver service : CLASS_ALIAS_RESOLVERS) { 129 Class<?> type = service.resolve(alias); 130 if (type != null) { 131 return type; 132 } 133 } 134 return null; 135 } 136 137 /** 138 * Returns the class object associated with a named class or interface, using the thread 139 * context class loader. If the value is {@code null}, this method returns {@code null}. 140 * 141 * @param value the value containing the class name string. 142 * @return the class object with the specified name. 143 * @throws JsonValueException if value is not a string or the named class could not be found. 144 */ 145 public static Class<?> asClass(JsonValue value) { 146 return (value == null || value.isNull() ? null : classForName(value)); 147 } 148 149 /** 150 * Creates a new instance of a named class. The class is instantiated as if by a 151 * {@code new} expression with an empty argument list. The class is initialized if it has 152 * not already been initialized. If the value is {@code null}, this method returns 153 * {@code null}. 154 * 155 * @param <T> the type of the new instance. 156 * @param value the value containing the class name string. 157 * @param type the type that the instantiated class should to resolve to. 158 * @return a new instance of the requested class. 159 * @throws JsonValueException if the requested class could not be instantiated. 160 */ 161 @SuppressWarnings("unchecked") 162 public static <T> T asNewInstance(JsonValue value, Class<T> type) { 163 if (value == null || value.isNull()) { 164 return null; 165 } 166 Class<?> c = asClass(value); 167 if (!type.isAssignableFrom(c)) { 168 throw new JsonValueException(value, "expecting " + type.getName()); 169 } 170 try { 171 return (T) c.newInstance(); 172 } catch (ExceptionInInitializerError eiie) { 173 throw new JsonValueException(value, eiie); 174 } catch (IllegalAccessException iae) { 175 throw new JsonValueException(value, iae); 176 } catch (InstantiationException ie) { 177 throw new JsonValueException(value, ie); 178 } 179 } 180 181 /** 182 * Returns a JSON value string value as an expression. If the value is {@code null}, this 183 * method returns {@code null}. 184 * 185 * @param value the JSON value containing the expression string. 186 * @return the expression represented by the string value. 187 * @throws JsonValueException if the value is not a string or the value is not a valid expression. 188 */ 189 public static Expression asExpression(JsonValue value) { 190 try { 191 return (value == null || value.isNull() ? null : new Expression(value.asString())); 192 } catch (ExpressionException ee) { 193 throw new JsonValueException(value, ee); 194 } 195 } 196 197 /** 198 * Evaluates the given JSON value string as an {@link Expression}. 199 * 200 * @param value 201 * the JSON value containing the expression string. 202 * @return the String that resulted of the Expression evaluation. 203 * @throws JsonValueException 204 * if the value is not a string or the value is not a valid string typed expression. 205 */ 206 public static String evaluate(JsonValue value) { 207 Expression expression = asExpression(value); 208 if (expression != null) { 209 return expression.eval(null, String.class); 210 } 211 return null; 212 } 213 214 /** 215 * Evaluates the given JSON value using an Expression and wraps the returned value as a new JsonValue. This only 216 * change value of String types JsonValues, other types are ignored. This mechanism only perform change on the given 217 * JsonValue object (child nodes are left unchanged). 218 * 219 * @param value 220 * the JSON value to be evaluated. 221 * @return a new JsonValue instance containing the resolved expression (or the original wrapped value if it was not 222 * changed) 223 * @throws JsonException 224 * if the expression cannot be evaluated (syntax error or resolution error). 225 */ 226 public static JsonValue evaluateJsonStaticExpression(final JsonValue value) { 227 // Returned a transformed, deep object copy 228 return new JsonValue(value, singleton(EXPRESSION_TRANSFORMER)); 229 } 230 231 /** 232 * Returns a function for transforming JsonValues to expressions. 233 * 234 * @return A function for transforming JsonValues to expressions. 235 */ 236 public static Function<JsonValue, Expression, HeapException> ofExpression() { 237 return OF_EXPRESSION; 238 } 239 240 /** 241 * Returns a {@link Function} to transform a list of String-based {@link JsonValue}s into a list of required heap 242 * objects. 243 * 244 * @param heap 245 * the heap to query for references resolution 246 * @param type 247 * expected object type 248 * @param <T> 249 * expected object type 250 * @return a {@link Function} to transform a list of String-based {@link JsonValue}s into a list of required heap 251 * objects. 252 */ 253 public static <T> Function<JsonValue, T, HeapException> ofRequiredHeapObject(final Heap heap, 254 final Class<T> type) { 255 return new Function<JsonValue, T, HeapException>() { 256 @Override 257 public T apply(final JsonValue value) throws HeapException { 258 return heap.resolve(value, type); 259 } 260 }; 261 } 262 263 /** 264 * Returns a {@link Function} to transform a list of String-based {@link JsonValue}s into a list of enum 265 * constant values of the given type. 266 * 267 * @param enumType expected type of the enum 268 * @param <T> Enumeration type 269 * @return a {@link Function} to transform a list of String-based {@link JsonValue}s into a list of enum 270 * constant values of the given type. 271 */ 272 public static <T extends Enum<T>> Function<JsonValue, T, HeapException> ofEnum(final Class<T> enumType) { 273 return new Function<JsonValue, T, HeapException>() { 274 @Override 275 public T apply(final JsonValue value) throws HeapException { 276 return Utils.asEnum(value.asString(), enumType); 277 } 278 }; 279 } 280 281 /** 282 * Verify that the given parameter object is of a JSON compatible type (recursively). If no exception is thrown that 283 * means the parameter can be used in the JWT session (that is a JSON value). 284 * 285 * @param trail 286 * pointer to the verified object 287 * @param value 288 * object to verify 289 */ 290 public static void checkJsonCompatibility(final String trail, final Object value) { 291 292 // Null is OK 293 if (value == null) { 294 return; 295 } 296 297 Class<?> type = value.getClass(); 298 Object object = value; 299 300 // JSON supports Boolean 301 if (object instanceof Boolean) { 302 return; 303 } 304 305 // JSON supports Chars (as String) 306 if (object instanceof Character) { 307 return; 308 } 309 310 // JSON supports Numbers (Long, Float, ...) 311 if (object instanceof Number) { 312 return; 313 } 314 315 // JSON supports String 316 if (object instanceof CharSequence) { 317 return; 318 } 319 320 // Consider array like a List 321 if (type.isArray()) { 322 object = Arrays.asList((Object[]) value); 323 } 324 325 if (object instanceof List) { 326 List<?> list = (List<?>) object; 327 for (int i = 0; i < list.size(); i++) { 328 checkJsonCompatibility(format("%s[%d]", trail, i), list.get(i)); 329 } 330 return; 331 } 332 333 if (object instanceof Map) { 334 Map<?, ?> map = (Map<?, ?>) object; 335 for (Map.Entry<?, ?> entry : map.entrySet()) { 336 checkJsonCompatibility(format("%s/%s", trail, entry.getKey()), entry.getValue()); 337 } 338 return; 339 } 340 341 throw new IllegalArgumentException(format( 342 "The object referenced through '%s' cannot be safely serialized as JSON", 343 trail)); 344 } 345 346 /** 347 * Returns the named property from the provided JSON object, falling back to 348 * zero or more deprecated property names. This method will log a warning if 349 * only a deprecated property is found or if two equivalent property names 350 * are found. 351 * 352 * @param config 353 * The configuration object. 354 * @param logger 355 * The logger which should be used for deprecation warnings. 356 * @param name 357 * The non-deprecated property name. 358 * @param deprecatedNames 359 * The deprecated property names ordered from newest to oldest. 360 * @return The request property. 361 */ 362 public static JsonValue getWithDeprecation(JsonValue config, Logger logger, String name, 363 String... deprecatedNames) { 364 String found = config.isDefined(name) ? name : null; 365 for (String deprecatedName : deprecatedNames) { 366 if (config.isDefined(deprecatedName)) { 367 if (found == null) { 368 found = deprecatedName; 369 warnForDeprecation(config, logger, name, found); 370 } else { 371 logger.warning("Cannot use both '" + deprecatedName + "' and '" + found 372 + "' attributes, " + "will use configuration from '" + found 373 + "' attribute"); 374 break; 375 } 376 } 377 } 378 return found == null ? config.get(name) : config.get(found); 379 } 380 381 /** 382 * Issues a warning that the configuration property {@code oldName} is 383 * deprecated and that the property {@code newName} should be used instead. 384 * 385 * @param config 386 * The configuration object. 387 * @param logger 388 * The logger which should be used for deprecation warnings. 389 * @param name 390 * The non-deprecated property name. 391 * @param deprecatedName 392 * The deprecated property name. 393 */ 394 public static void warnForDeprecation(final JsonValue config, final Logger logger, 395 final String name, final String deprecatedName) { 396 logger.warning(format("[%s] The '%s' attribute is deprecated, please use '%s' instead", 397 config.getPointer(), deprecatedName, name)); 398 } 399 400 /** 401 * Parses to json the provided data. 402 * 403 * @param rawData 404 * The data as a string to read and parse. 405 * @param <T> 406 * The parsing should be as specified in doc. e.g: 407 * @see Json#readJson(Reader) 408 * @return According to its type, a cast must be necessary to extract the 409 * value. 410 * @throws IOException 411 * If an exception occurs during parsing the data. 412 */ 413 public static <T> T readJson(final String rawData) throws IOException { 414 if (rawData == null) { 415 return null; 416 } 417 return readJson(new StringReader(rawData)); 418 } 419 420 /** 421 * Parses to json the provided reader. 422 * 423 * @param reader 424 * The data to parse. 425 * @param <T> 426 * The parsing should be as specified in doc. e.g: 427 * 428 * <pre> 429 * <b>JSON | Type Java Type</b> 430 * ------------------------------------ 431 * object | LinkedHashMap<String,?> 432 * array | LinkedList<?> 433 * string | String 434 * number | Integer 435 * float | Float 436 * true|false | Boolean 437 * null | null 438 * </pre> 439 * @return The parsed JSON into its corresponding java type. 440 * @throws IOException 441 * If an exception occurs during parsing the data. 442 */ 443 public static <T> T readJson(final Reader reader) throws IOException { 444 return parse(STRICT_MAPPER, reader); 445 } 446 447 /** 448 * This function it's only used to read our configuration files and allows 449 * JSON files to contain non strict JSON such as comments or single quotes. 450 * 451 * @param reader 452 * The stream of data to parse. 453 * @return A map containing the parsed configuration. 454 * @throws IOException 455 * If an error occurs during reading/parsing the data. 456 */ 457 public static Map<String, Object> readJsonLenient(final Reader reader) throws IOException { 458 return parse(LENIENT_MAPPER, reader); 459 } 460 461 /** 462 * This function it's only used to read our configuration files and allows 463 * JSON files to contain non strict JSON such as comments or single quotes. 464 * 465 * @param in 466 * The input stream containing the json configuration. 467 * @return A map containing the parsed configuration. 468 * @throws IOException 469 * If an error occurs during reading/parsing the data. 470 */ 471 public static Map<String, Object> readJsonLenient(final InputStream in) throws IOException { 472 return parse(LENIENT_MAPPER, new InputStreamReader(in)); 473 } 474 475 private static <T> T parse(ObjectMapper mapper, Reader reader) throws IOException { 476 if (reader == null) { 477 return null; 478 } 479 480 final JsonParser jp = mapper.getFactory().createParser(reader); 481 final JsonToken jToken = jp.nextToken(); 482 if (jToken != null) { 483 switch (jToken) { 484 case START_ARRAY: 485 return mapper.readValue(jp, new TypeReference<LinkedList<?>>() { 486 }); 487 case START_OBJECT: 488 return mapper.readValue(jp, new TypeReference<LinkedHashMap<String, ?>>() { 489 }); 490 case VALUE_FALSE: 491 case VALUE_TRUE: 492 return mapper.readValue(jp, new TypeReference<Boolean>() { 493 }); 494 case VALUE_NUMBER_INT: 495 return mapper.readValue(jp, new TypeReference<Integer>() { 496 }); 497 case VALUE_NUMBER_FLOAT: 498 return mapper.readValue(jp, new TypeReference<Float>() { 499 }); 500 case VALUE_NULL: 501 return null; 502 default: 503 // This is very unlikely to happen. 504 throw new IOException("Invalid JSON content"); 505 } 506 } 507 return null; 508 } 509 510 /** 511 * Writes the JSON content of the object passed in parameter. 512 * 513 * @param objectToWrite 514 * The object we want to serialize as JSON output. The 515 * @return the Json output as a string. 516 * @throws IOException 517 * If an error occurs during writing/mapping content. 518 */ 519 public static byte[] writeJson(final Object objectToWrite) throws IOException { 520 return STRICT_MAPPER.writeValueAsBytes(objectToWrite); 521 } 522}