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-2015 ForgeRock AS.
016 */
017
018package org.forgerock.http.util;
019
020import static com.fasterxml.jackson.core.JsonParser.Feature.ALLOW_COMMENTS;
021import static com.fasterxml.jackson.core.JsonParser.Feature.ALLOW_SINGLE_QUOTES;
022import static com.fasterxml.jackson.core.JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS;
023import static java.lang.String.format;
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.List;
032import java.util.Map;
033
034import com.fasterxml.jackson.databind.ObjectMapper;
035
036
037/**
038 * Provides read and write JSON capabilities.
039 * Can check if an object reference is JSON-compatible (expressed as primitive values, list/array and map).
040 */
041public final class Json {
042
043    /** Non strict object mapper / data binder used to read json configuration files/data. */
044    private static final ObjectMapper LENIENT_MAPPER;
045    static {
046        LENIENT_MAPPER = new ObjectMapper();
047        LENIENT_MAPPER.configure(ALLOW_COMMENTS, true);
048        LENIENT_MAPPER.configure(ALLOW_SINGLE_QUOTES, true);
049        LENIENT_MAPPER.configure(ALLOW_UNQUOTED_CONTROL_CHARS, true);
050    }
051
052    /** Strict object mapper / data binder used to read json configuration files/data. */
053    private static final ObjectMapper STRICT_MAPPER = new ObjectMapper();
054
055    /**
056     * Private constructor for utility class.
057     */
058    private Json() { }
059
060    /**
061     * Verify that the given parameter object is of a JSON compatible type (recursively). If no exception is thrown that
062     * means the parameter can be used in the JWT session (that is a JSON value).
063     *
064     * @param trail
065     *         pointer to the verified object
066     * @param value
067     *         object to verify
068     */
069    public static void checkJsonCompatibility(final String trail, final Object value) {
070
071        // Null is OK
072        if (value == null) {
073            return;
074        }
075
076        Class<?> type = value.getClass();
077        Object object = value;
078
079        // JSON supports Boolean
080        if (object instanceof Boolean) {
081            return;
082        }
083
084        // JSON supports Chars (as String)
085        if (object instanceof Character) {
086            return;
087        }
088
089        // JSON supports Numbers (Long, Float, ...)
090        if (object instanceof Number) {
091            return;
092        }
093
094        // JSON supports String
095        if (object instanceof CharSequence) {
096            return;
097        }
098
099        // Consider array like a List
100        if (type.isArray()) {
101            object = Arrays.asList((Object[]) value);
102        }
103
104        if (object instanceof List) {
105            List<?> list = (List<?>) object;
106            for (int i = 0; i < list.size(); i++) {
107                checkJsonCompatibility(format("%s[%d]", trail, i), list.get(i));
108            }
109            return;
110        }
111
112        if (object instanceof Map) {
113            Map<?, ?> map = (Map<?, ?>) object;
114            for (Map.Entry<?, ?> entry : map.entrySet()) {
115                checkJsonCompatibility(format("%s/%s", trail, entry.getKey()), entry.getValue());
116            }
117            return;
118        }
119
120        throw new IllegalArgumentException(format(
121                "The object referenced through '%s' cannot be safely serialized as JSON",
122                trail));
123    }
124
125    /**
126     * Parses to json the provided data.
127     *
128     * @param rawData
129     *            The data as a string to read and parse.
130     * @see Json#readJson(Reader)
131     * @return Any of {@code Map<String, Object>}, {@code List<Object>}, {@code Number}, {@code Boolean}
132     *         or {@code null}.
133     * @throws IOException
134     *             If an exception occurs during parsing the data.
135     */
136    public static Object readJson(final String rawData) throws IOException {
137        if (rawData == null) {
138            return null;
139        }
140        return readJson(new StringReader(rawData));
141    }
142
143    /**
144     * Parses to json the provided reader.
145     *
146     * @param reader
147     *            The data to parse.
148     * @return Any of {@code Map<String, Object>}, {@code List<Object>}, {@code Number}, {@code Boolean}
149     *         or {@code null}.
150     * @throws IOException
151     *             If an exception occurs during parsing the data.
152     */
153    public static Object readJson(final Reader reader) throws IOException {
154        return parse(STRICT_MAPPER, reader);
155    }
156
157    /**
158     * This function it's only used to read our configuration files and allows
159     * JSON files to contain non strict JSON such as comments or single quotes.
160     *
161     * @param reader
162     *            The stream of data to parse.
163     * @return Any of {@code Map<String, Object>}, {@code List<Object>}, {@code Number}, {@code Boolean}
164     *         or {@code null}.
165     * @throws IOException
166     *             If an error occurs during reading/parsing the data.
167     */
168    public static Object readJsonLenient(final Reader reader) throws IOException {
169        return parse(LENIENT_MAPPER, reader);
170    }
171
172    /**
173     * This function it's only used to read our configuration files and allows
174     * JSON files to contain non strict JSON such as comments or single quotes.
175     *
176     * @param in
177     *            The input stream containing the json.
178     * @return Any of {@code Map<String, Object>}, {@code List<Object>}, {@code Number}, {@code Boolean}
179     *         or {@code null}.
180     * @throws IOException
181     *             If an error occurs during reading/parsing the data.
182     */
183    public static Object readJsonLenient(final InputStream in) throws IOException {
184        return parse(LENIENT_MAPPER, new InputStreamReader(in));
185    }
186
187    private static Object parse(ObjectMapper mapper, Reader reader) throws IOException {
188        if (reader == null) {
189            return null;
190        }
191
192        return mapper.readValue(reader, Object.class);
193    }
194
195    /**
196     * Writes the JSON content of the object passed in parameter.
197     *
198     * @param objectToWrite
199     *            The object we want to serialize as JSON output. The
200     * @return the Json output as a byte array.
201     * @throws IOException
202     *             If an error occurs during writing/mapping content.
203     */
204    public static byte[] writeJson(final Object objectToWrite) throws IOException {
205        return STRICT_MAPPER.writeValueAsBytes(objectToWrite);
206    }
207}