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}