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 2015 ForgeRock AS. 015 */ 016 017package org.forgerock.openig.util; 018 019import static java.lang.String.*; 020import static java.util.Collections.*; 021import static org.forgerock.http.util.Loader.*; 022 023import java.util.List; 024 025import org.forgerock.json.JsonException; 026import org.forgerock.json.JsonTransformer; 027import org.forgerock.json.JsonValue; 028import org.forgerock.json.JsonValueException; 029import org.forgerock.openig.alias.ClassAliasResolver; 030import org.forgerock.openig.el.Expression; 031import org.forgerock.openig.el.ExpressionException; 032import org.forgerock.openig.heap.Heap; 033import org.forgerock.openig.heap.HeapException; 034import org.forgerock.openig.log.Logger; 035import org.forgerock.util.Function; 036import org.forgerock.util.Utils; 037 038/** 039 * Provides additional functionality to {@link JsonValue}. 040 */ 041public final class JsonValues { 042 043 /** List of alias service providers found at initialization time. */ 044 private static final List<ClassAliasResolver> CLASS_ALIAS_RESOLVERS = 045 unmodifiableList(loadList(ClassAliasResolver.class)); 046 047 private static final Function<JsonValue, Expression<String>, HeapException> OF_EXPRESSION = 048 new Function<JsonValue, Expression<String>, HeapException>() { 049 @Override 050 public Expression<String> apply(final JsonValue value) throws HeapException { 051 return asExpression(value, String.class); 052 } 053 }; 054 055 /** 056 * Private constructor for utility class. 057 */ 058 private JsonValues() { } 059 060 /** 061 * Resolves a String-based {@link JsonValue} instance that may contains an {@link Expression}. 062 */ 063 private static final JsonTransformer EXPRESSION_TRANSFORMER = new JsonTransformer() { 064 @Override 065 public void transform(final JsonValue value) { 066 if (value.isString()) { 067 try { 068 Expression<Object> expression = Expression.valueOf(value.asString(), Object.class); 069 value.setObject(expression.eval()); 070 } catch (ExpressionException e) { 071 throw new JsonException(format("Expression '%s' (in %s) is not syntactically correct", 072 value.asString(), 073 value.getPointer()), e); 074 } 075 } 076 } 077 }; 078 079 private static Class<?> classForName(JsonValue value) { 080 String name = value.asString(); 081 // Looks for registered aliases first 082 Class<?> type = resolveAlias(name); 083 if (type != null) { 084 return type; 085 } 086 // No alias found, consider the value as a fully qualified class name 087 try { 088 return Class.forName(name, true, Thread.currentThread().getContextClassLoader()); 089 } catch (ClassNotFoundException cnfe) { 090 throw new JsonValueException(value, cnfe); 091 } 092 } 093 094 /** 095 * Resolve a given alias against the known aliases service providers. 096 * The first {@literal non-null} resolved type is returned. 097 */ 098 private static Class<?> resolveAlias(final String alias) { 099 for (ClassAliasResolver service : CLASS_ALIAS_RESOLVERS) { 100 Class<?> type = service.resolve(alias); 101 if (type != null) { 102 return type; 103 } 104 } 105 return null; 106 } 107 108 /** 109 * Returns the class object associated with a named class or interface, using the thread 110 * context class loader. If the value is {@code null}, this method returns {@code null}. 111 * 112 * @param value the value containing the class name string. 113 * @return the class object with the specified name. 114 * @throws JsonValueException if value is not a string or the named class could not be found. 115 */ 116 public static Class<?> asClass(JsonValue value) { 117 return (value == null || value.isNull() ? null : classForName(value)); 118 } 119 120 /** 121 * Creates a new instance of a named class. The class is instantiated as if by a 122 * {@code new} expression with an empty argument list. The class is initialized if it has 123 * not already been initialized. If the value is {@code null}, this method returns 124 * {@code null}. 125 * 126 * @param <T> the type of the new instance. 127 * @param value the value containing the class name string. 128 * @param type the type that the instantiated class should to resolve to. 129 * @return a new instance of the requested class. 130 * @throws JsonValueException if the requested class could not be instantiated. 131 */ 132 @SuppressWarnings("unchecked") 133 public static <T> T asNewInstance(JsonValue value, Class<T> type) { 134 if (value == null || value.isNull()) { 135 return null; 136 } 137 Class<?> c = asClass(value); 138 if (!type.isAssignableFrom(c)) { 139 throw new JsonValueException(value, "expecting " + type.getName()); 140 } 141 try { 142 return (T) c.newInstance(); 143 } catch (ExceptionInInitializerError | InstantiationException | IllegalAccessException e) { 144 throw new JsonValueException(value, e); 145 } 146 } 147 148 /** 149 * Returns a JSON value string value as an expression. If the value is {@code null}, this 150 * method returns {@code null}. 151 * 152 * @param <T> expected result type 153 * @param value the JSON value containing the expression string. 154 * @param expectedType The expected result type of the expression. 155 * @return the expression represented by the string value. 156 * @throws JsonValueException if the value is not a string or the value is not a valid expression. 157 */ 158 public static <T> Expression<T> asExpression(JsonValue value, Class<T> expectedType) { 159 try { 160 return (value == null || value.isNull() ? null : Expression.valueOf(value.asString(), expectedType)); 161 } catch (ExpressionException ee) { 162 throw new JsonValueException(value, ee); 163 } 164 } 165 166 /** 167 * Evaluates the given JSON value string as an {@link Expression}. 168 * 169 * @param value 170 * the JSON value containing the expression string. 171 * @return the String that resulted of the Expression evaluation. 172 * @throws JsonValueException 173 * if the value is not a string or the value is not a valid string typed expression. 174 */ 175 public static String evaluate(JsonValue value) { 176 Expression<String> expression = asExpression(value, String.class); 177 if (expression != null) { 178 return expression.eval(); 179 } 180 return null; 181 } 182 183 /** 184 * Evaluates the given JSON value object, applying a {@link JsonTransformer} 185 * that will evaluate all String nodes. Transformation is applied 186 * recursively. <p>Malformed expressions are ignored e.g: <tt>"$$$${{"</tt> 187 * and their values are not changed. <p>When an error occurs during the 188 * evaluation of an expression, the value is set to {@code null} because we 189 * cannot differentiate successful evaluations or failed ones. 190 * 191 * @param value 192 * The JSON value object to evaluate. 193 * @param logger 194 * The logger which should be used for warnings. 195 * @return A JSON value object which all expressions string are evaluated. 196 * @throws JsonException 197 * If the value not a valid expression. 198 */ 199 public static JsonValue evaluate(final JsonValue value, final Logger logger) { 200 return new JsonValue(value.getObject(), singleton(new JsonTransformer() { 201 @Override 202 public void transform(final JsonValue value) { 203 if (value.isString()) { 204 try { 205 // Malformed expressions are ignored 206 final Expression<Object> expression = asExpression(value, Object.class); 207 if (expression != null) { 208 final Object evaluated = expression.eval(); 209 // Errors during evaluation are represented with a null result 210 if (evaluated == null) { 211 logger.warning(format("The expression '%s' (in %s) cannot be evaluated", 212 expression, value.getPointer())); 213 } 214 // We should only replace the value for successful evaluations, 215 // but we cannot differentiate successful evaluations or failed ones 216 value.setObject(evaluated); 217 218 } 219 } catch (JsonValueException jve) { 220 logger.warning(format("The value %s (in %s) is a malformed expression", 221 value.asString(), value.getPointer())); 222 } 223 } 224 } 225 })); 226 } 227 228 /** 229 * Evaluates the given JSON value using an Expression and wraps the returned value as a new JsonValue. This only 230 * change value of String types JsonValues, other types are ignored. This mechanism only perform change on the given 231 * JsonValue object (child nodes are left unchanged). 232 * 233 * @param value 234 * the JSON value to be evaluated. 235 * @return a new JsonValue instance containing the resolved expression (or the original wrapped value if it was not 236 * changed) 237 * @throws JsonException 238 * if the expression cannot be evaluated (syntax error or resolution error). 239 */ 240 public static JsonValue evaluateJsonStaticExpression(final JsonValue value) { 241 // Returned a transformed, deep object copy 242 return new JsonValue(value, singleton(EXPRESSION_TRANSFORMER)); 243 } 244 245 /** 246 * Returns, if the given JSON value contains one of the names, the first 247 * defined JSON value, otherwise if the given JSON value does not match any 248 * of the names, then a JsonValue encapsulating null is returned. 249 * Example of use: 250 * 251 * <pre>{@code 252 * Uri uri = firstOf(config, "authorizeEndpoint", "authorize_endpoint").required().asURI(); 253 * }</pre> 254 * 255 * @param config 256 * The JSON value where one of the selected names can be found. 257 * Usually in a heaplet configuration for example. 258 * @param names 259 * Names of the attributes that you are looking for. 260 * @return the specified item JSON value or JsonValue encapsulating null if 261 * none were found. 262 */ 263 public static JsonValue firstOf(final JsonValue config, final String... names) { 264 if (names != null) { 265 for (final String name : names) { 266 if (config.isDefined(name)) { 267 return config.get(name); 268 } 269 } 270 } 271 return new JsonValue(null); 272 } 273 274 /** 275 * Returns a function for transforming JsonValues to expressions. 276 * 277 * @return A function for transforming JsonValues to expressions. 278 */ 279 public static Function<JsonValue, Expression<String>, HeapException> ofExpression() { 280 return OF_EXPRESSION; 281 } 282 283 /** 284 * Returns a {@link Function} to transform a list of String-based {@link JsonValue}s into a list of required heap 285 * objects. 286 * 287 * @param heap 288 * the heap to query for references resolution 289 * @param type 290 * expected object type 291 * @param <T> 292 * expected object type 293 * @return a {@link Function} to transform a list of String-based {@link JsonValue}s into a list of required heap 294 * objects. 295 */ 296 public static <T> Function<JsonValue, T, HeapException> ofRequiredHeapObject(final Heap heap, 297 final Class<T> type) { 298 return new Function<JsonValue, T, HeapException>() { 299 @Override 300 public T apply(final JsonValue value) throws HeapException { 301 return heap.resolve(value, type); 302 } 303 }; 304 } 305 306 /** 307 * Returns a {@link Function} to transform a list of String-based {@link JsonValue}s into a list of enum 308 * constant values of the given type. 309 * 310 * @param enumType expected type of the enum 311 * @param <T> Enumeration type 312 * @return a {@link Function} to transform a list of String-based {@link JsonValue}s into a list of enum 313 * constant values of the given type. 314 */ 315 public static <T extends Enum<T>> Function<JsonValue, T, HeapException> ofEnum(final Class<T> enumType) { 316 return new Function<JsonValue, T, HeapException>() { 317 @Override 318 public T apply(final JsonValue value) throws HeapException { 319 return Utils.asEnum(value.asString(), enumType); 320 } 321 }; 322 } 323 324 /** 325 * Returns the named property from the provided JSON object, falling back to 326 * zero or more deprecated property names. This method will log a warning if 327 * only a deprecated property is found or if two equivalent property names 328 * are found. 329 * 330 * @param config 331 * The configuration object. 332 * @param logger 333 * The logger which should be used for deprecation warnings. 334 * @param name 335 * The non-deprecated property name. 336 * @param deprecatedNames 337 * The deprecated property names ordered from newest to oldest. 338 * @return The request property. 339 */ 340 public static JsonValue getWithDeprecation(JsonValue config, Logger logger, String name, 341 String... deprecatedNames) { 342 String found = config.isDefined(name) ? name : null; 343 for (String deprecatedName : deprecatedNames) { 344 if (config.isDefined(deprecatedName)) { 345 if (found == null) { 346 found = deprecatedName; 347 warnForDeprecation(config, logger, name, found); 348 } else { 349 logger.warning("Cannot use both '" + deprecatedName + "' and '" + found 350 + "' attributes, " + "will use configuration from '" + found 351 + "' attribute"); 352 break; 353 } 354 } 355 } 356 return found == null ? config.get(name) : config.get(found); 357 } 358 359 /** 360 * Issues a warning that the configuration property {@code oldName} is 361 * deprecated and that the property {@code newName} should be used instead. 362 * 363 * @param config 364 * The configuration object. 365 * @param logger 366 * The logger which should be used for deprecation warnings. 367 * @param name 368 * The non-deprecated property name. 369 * @param deprecatedName 370 * The deprecated property name. 371 */ 372 public static void warnForDeprecation(final JsonValue config, final Logger logger, 373 final String name, final String deprecatedName) { 374 logger.warning(format("[%s] The '%s' attribute is deprecated, please use '%s' instead", 375 config.getPointer(), deprecatedName, name)); 376 } 377}