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}