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-2016 ForgeRock AS.
015 */
016
017package org.forgerock.audit.util;
018
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collections;
022import java.util.LinkedHashMap;
023import java.util.List;
024import java.util.Locale;
025import java.util.Map;
026import java.util.Set;
027import java.util.TreeMap;
028
029import org.forgerock.json.JsonPointer;
030import org.forgerock.json.JsonValue;
031import org.forgerock.util.query.QueryFilter;
032import org.forgerock.util.query.QueryFilterVisitor;
033import org.slf4j.Logger;
034import org.slf4j.LoggerFactory;
035
036import com.fasterxml.jackson.core.JsonProcessingException;
037import com.fasterxml.jackson.databind.ObjectMapper;
038
039/**
040 * Contains some JsonValue Utility methods.
041 */
042public final class JsonValueUtils {
043
044    private static final Logger LOGGER = LoggerFactory.getLogger(JsonValueUtils.class);
045    private static final ObjectMapper MAPPER = new ObjectMapper();
046
047    private JsonValueUtils() {
048        // utility class
049    }
050
051    /**
052     * Expands a Json Object Map, where the keys of the map are the {@link JsonPointer JsonPointer }s
053     * and the values are the value the {@link JsonPointer JsonPointer } resolves to.
054     *
055     * For Example, the following key-value pairs
056     * <pre>
057     *      /object/array/0 , "test"
058     *      /object/array/1 , "test1"
059     *      /string         , "stringVal"
060     *      /boolean        , false
061     *      /number         , 1
062     *      /array/0        , "value1"
063     *      /array/1        , "value2"
064     * </pre>
065     *
066     * will produce the following json object
067     * <pre>
068     *     {
069     *         "object" : {
070     *             "array" : ["test", "test1"]
071     *         },
072     *         "string" : "stringVal",
073     *         "boolean" : false,
074     *         "number" : 1,
075     *         "array" : ["value1", "value2"]
076     *     }
077     * </pre>
078     * @param object the Json Object Map containing the {@link JsonPointer JsonPointer }s and values
079     * @return the {@link JsonValue JsonValue } expanded from the object map
080     */
081    public static JsonValue expand(final Map<String, Object> object) {
082        //sort the objects so the array objects are in order
083        final Map<String, Object> sortedObjects = new TreeMap<>(object);
084        return buildObject(sortedObjects);
085    }
086
087    /**
088     *
089     * Flattens a {@link JsonValue JsonValue } to a Map, where the keys of the Map are {@link JsonPointer JsonPointer }s
090     * and the values are the value the {@link JsonPointer JsonPointer }s resolve to.
091     *
092     * For Example, the following JsonValue
093     *
094     * <pre>
095     *     {
096     *         "object" : {
097     *             "array" : ["test", "test1"]
098     *         },
099     *         "string" : "stringVal",
100     *         "boolean" : false,
101     *         "number" : 1,
102     *         "array" : ["value1", "value2"]
103     *     }
104     * </pre>
105     *
106     * will produce the following Map key-value pairs
107     *
108     *  <pre>
109     *      /object/array/0 , "test"
110     *      /object/array/1 , "test1"
111     *      /string         , "stringVal"
112     *      /boolean        , false
113     *      /number         , 1
114     *      /array/0        , "value1"
115     *      /array/1        , "value2"
116     * </pre>
117     * @param jsonValue the {@link JsonValue JsonValue } object to flatten
118     * @return a Map representing the flattened {@link JsonValue JsonValue } object
119     */
120    public static Map<String, Object> flatten(final JsonValue jsonValue) {
121        Map<String, Object> flatObject = new LinkedHashMap<>();
122        flatten(new JsonPointer(), jsonValue, flatObject);
123        return flatObject;
124    }
125
126    /**
127     * Extracts String representation of field identified by <code>fieldName</code> from <code>json</code> object.
128     *
129     * @param json the {@link JsonValue} object from which to extract a value.
130     * @param fieldName the field identifier in a form consumable by {@link JsonPointer}.
131     *
132     * @return A non-null String representation of the field's value. If the specified field is not present or has
133     *         a null value, an empty string will be returned.
134     */
135    public static String extractValueAsString(final JsonValue json, final String fieldName) {
136        JsonValue value = json.get(new JsonPointer(fieldName));
137        if (value == null || value.isNull()) {
138            return null;
139        } else if (value.isString()) {
140            return value.asString();
141        } else {
142            String rawStr = null;
143            try {
144                rawStr = MAPPER.writeValueAsString(value.getObject());
145            } catch (JsonProcessingException e) {
146                LOGGER.error("Unable to write the value for field {} as a string.", fieldName);
147            }
148            return rawStr;
149        }
150    }
151
152    private static JsonValue buildObject(Map<String, Object> objectSet) {
153        final JsonValue jsonValue = new JsonValue(new LinkedHashMap<>());
154        for (Map.Entry<String, Object> entry : objectSet.entrySet()) {
155            final String key = entry.getKey();
156            final Object value = entry.getValue();
157            if (jsonValue.get(new JsonPointer(key)) != null
158                    && !jsonValue.get(new JsonPointer(key)).isNull()) {
159                //only build a sub json object for one prefix value
160                continue;
161            }
162            final JsonPointer jsonPointer = new JsonPointer(key);
163            int numberOfIndexTokens = getIndexTokens(jsonPointer);
164            if (numberOfIndexTokens > 1) {
165                //more than one json array must build the sub json object
166                int firstIndexTokenPos = getNextIndexToken(jsonPointer, 0);
167                final JsonPointer prefix = subJsonPointer(jsonPointer, 0, firstIndexTokenPos + 1);
168                jsonValue.putPermissive(
169                        replaceLastIndexToken(prefix),
170                        buildObject(findObjectsThatMatchPrefix(prefix, objectSet)).getObject()
171                );
172            } else {
173                jsonValue.putPermissive(replaceLastIndexToken(jsonPointer), value);
174            }
175        }
176        return jsonValue;
177    }
178
179    private static Map<String, Object> findObjectsThatMatchPrefix(
180            final JsonPointer prefix,
181            Map<String, Object> objectSet) {
182        Map<String, Object> matchingObjects = new LinkedHashMap<>();
183        for (final String key : objectSet.keySet()) {
184            if (key.startsWith(prefix.toString())) {
185                matchingObjects.put(key.substring(prefix.toString().length(), key.length()), objectSet.get(key));
186            }
187        }
188        return matchingObjects;
189    }
190
191    private static boolean isIndexToken(final String token) {
192        if (token.isEmpty()) {
193            return false;
194        } else {
195            for (int i = 0; i < token.length(); i++) {
196                final char c = token.charAt(i);
197                if (!Character.isDigit(c)) {
198                    return false;
199                }
200            }
201            return true;
202        }
203    }
204
205    private static JsonPointer replaceLastIndexToken(final JsonPointer jsonPointer) {
206        final String[] jsonPointerTokens = jsonPointer.toArray();
207        if (getNextIndexToken(jsonPointer, jsonPointer.size() - 1) != -1) {
208            jsonPointerTokens[jsonPointerTokens.length - 1] = "-";
209        }
210        return new JsonPointer(jsonPointerTokens);
211    }
212
213    private static int getNextIndexToken(final JsonPointer jsonPointer, int start) {
214        final String[] jsonPointerTokens = jsonPointer.toArray();
215        for (int i = start; i < jsonPointerTokens.length; i++) {
216            if (isIndexToken(jsonPointerTokens[i])) {
217                return i;
218            }
219        }
220        return -1;
221    }
222
223    private static JsonPointer subJsonPointer(final JsonPointer jsonPointer, final int start, final int end) {
224        final String[] jsonPointerTokens = jsonPointer.toArray();
225        final List<String> newJsonPointerTokens = new ArrayList<>();
226        for (int i = start; i < end; i++) {
227            newJsonPointerTokens.add(jsonPointerTokens[i]);
228        }
229        return new JsonPointer(newJsonPointerTokens.toArray(new String[newJsonPointerTokens.size()]));
230    }
231
232    private static int getIndexTokens(final JsonPointer jsonPointer) {
233        int numberOfIndexTokens = 0;
234        final String[] jsonPointerTokens = jsonPointer.toArray();
235        for (int i = 0; i < jsonPointerTokens.length; i++) {
236            if (isIndexToken(jsonPointerTokens[i])) {
237                numberOfIndexTokens++;
238            }
239        }
240        return numberOfIndexTokens;
241    }
242
243    private static void flatten(
244            final JsonPointer pointer,
245            final JsonValue jsonValue,
246            final Map<String, Object> flatObject) {
247        final Set<String> jsonValueKeys = jsonValue.get(pointer).keys();
248        for (final String key : jsonValueKeys) {
249            final JsonPointer keyPointer = concatJsonPointer(pointer, key);
250            final JsonValue temp = jsonValue.get(keyPointer);
251            if (temp.isMap() || temp.isList()) {
252                flatten(keyPointer, jsonValue, flatObject);
253            } else {
254                flatObject.put(
255                        keyPointer.toString(),
256                        jsonValue.get(keyPointer).getObject());
257            }
258        }
259        return;
260    }
261
262    private static JsonPointer concatJsonPointer(final JsonPointer pointer, final String key) {
263        final String[] pointerTokens = pointer.toArray();
264        final String[] newPointerTokens = Arrays.copyOf(pointerTokens, pointerTokens.length + 1);
265        newPointerTokens[pointerTokens.length] = key;
266        return new JsonPointer(newPointerTokens);
267
268    }
269
270    /**
271     * A generic JsonValue Query Filter Visitor.
272     */
273    public static final QueryFilterVisitor<Boolean, JsonValue, JsonPointer> JSONVALUE_FILTER_VISITOR =
274        new QueryFilterVisitor<Boolean, JsonValue, JsonPointer>() {
275            @Override
276            public Boolean visitAndFilter(final JsonValue p, final List<QueryFilter<JsonPointer>> subFilters) {
277                for (final QueryFilter<JsonPointer> subFilter : subFilters) {
278                    if (!subFilter.accept(this, p)) {
279                        return Boolean.FALSE;
280                    }
281                }
282                return Boolean.TRUE;
283            }
284
285            @Override
286            public Boolean visitBooleanLiteralFilter(final JsonValue p, final boolean value) {
287                return value;
288            }
289
290            @Override
291            public Boolean visitContainsFilter(final JsonValue p, final JsonPointer field,
292                                               final Object valueAssertion) {
293                for (final Object value : getValues(p, field)) {
294                    if (isCompatible(valueAssertion, value)) {
295                        if (valueAssertion instanceof String) {
296                            final String s1 = ((String) valueAssertion).toLowerCase(Locale.ENGLISH);
297                            final String s2 = ((String) value).toLowerCase(Locale.ENGLISH);
298                            if (s2.contains(s1)) {
299                                return Boolean.TRUE;
300                            }
301                        } else {
302                            // Use equality matching for numbers and booleans.
303                            if (compareValues(valueAssertion, value) == 0) {
304                                return Boolean.TRUE;
305                            }
306                        }
307                    }
308                }
309                return Boolean.FALSE;
310            }
311
312            @Override
313            public Boolean visitEqualsFilter(final JsonValue p, final JsonPointer field,
314                                             final Object valueAssertion) {
315                Boolean result = Boolean.TRUE;
316                for (final Object value : getValues(p, field)) {
317                    if (!isCompatible(valueAssertion, value) || compareValues(valueAssertion, value) != 0) {
318                        result = Boolean.FALSE;
319                    }
320                }
321                return result;
322            }
323
324            @Override
325            public Boolean visitExtendedMatchFilter(final JsonValue p, final JsonPointer field,
326                                                    final String matchingRuleId, final Object valueAssertion) {
327                // Extended filters are not supported
328                return Boolean.FALSE;
329            }
330
331            @Override
332            public Boolean visitGreaterThanFilter(final JsonValue p, final JsonPointer field,
333                                                  final Object valueAssertion) {
334                for (final Object value : getValues(p, field)) {
335                    if (isCompatible(valueAssertion, value) && compareValues(valueAssertion, value) < 0) {
336                        return Boolean.TRUE;
337                    }
338                }
339                return Boolean.FALSE;
340            }
341
342            @Override
343            public Boolean visitGreaterThanOrEqualToFilter(final JsonValue p, final JsonPointer field,
344                                                           final Object valueAssertion) {
345                for (final Object value : getValues(p, field)) {
346                    if (isCompatible(valueAssertion, value) && compareValues(valueAssertion, value) <= 0) {
347                        return Boolean.TRUE;
348                    }
349                }
350                return Boolean.FALSE;
351            }
352
353            @Override
354            public Boolean visitLessThanFilter(final JsonValue p, final JsonPointer field,
355                                               final Object valueAssertion) {
356                for (final Object value : getValues(p, field)) {
357                    if (isCompatible(valueAssertion, value) && compareValues(valueAssertion, value) > 0) {
358                        return Boolean.TRUE;
359                    }
360                }
361                return Boolean.FALSE;
362            }
363
364            @Override
365            public Boolean visitLessThanOrEqualToFilter(final JsonValue p, final JsonPointer field,
366                                                        final Object valueAssertion) {
367                for (final Object value : getValues(p, field)) {
368                    if (isCompatible(valueAssertion, value) && compareValues(valueAssertion, value) >= 0) {
369                        return Boolean.TRUE;
370                    }
371                }
372                return Boolean.FALSE;
373            }
374
375            @Override
376            public Boolean visitNotFilter(final JsonValue p, final QueryFilter<JsonPointer> subFilter) {
377                return !subFilter.accept(this, p);
378            }
379
380            @Override
381            public Boolean visitOrFilter(final JsonValue p, final List<QueryFilter<JsonPointer>> subFilters) {
382                for (final QueryFilter<JsonPointer> subFilter : subFilters) {
383                    if (subFilter.accept(this, p)) {
384                        return Boolean.TRUE;
385                    }
386                }
387                return Boolean.FALSE;
388            }
389
390            @Override
391            public Boolean visitPresentFilter(final JsonValue p, final JsonPointer field) {
392                final JsonValue value = p.get(field);
393                return value != null;
394            }
395
396            @Override
397            public Boolean visitStartsWithFilter(final JsonValue p, final JsonPointer field,
398                                                 final Object valueAssertion) {
399                for (final Object value : getValues(p, field)) {
400                    if (isCompatible(valueAssertion, value)) {
401                        if (valueAssertion instanceof String) {
402                            final String s1 = ((String) valueAssertion).toLowerCase(Locale.ENGLISH);
403                            final String s2 = ((String) value).toLowerCase(Locale.ENGLISH);
404                            if (s2.startsWith(s1)) {
405                                return Boolean.TRUE;
406                            }
407                        } else {
408                            // Use equality matching for numbers and booleans.
409                            if (compareValues(valueAssertion, value) == 0) {
410                                return Boolean.TRUE;
411                            }
412                        }
413                    }
414                }
415                return Boolean.FALSE;
416            }
417
418            private List<Object> getValues(final JsonValue resource, final JsonPointer field) {
419                final JsonValue value = resource.get(field);
420                if (value == null) {
421                    return Collections.emptyList();
422                } else if (value.isList()) {
423                    return value.asList();
424                } else {
425                    return Collections.singletonList(value.getObject());
426                }
427            }
428
429            private int compareValues(final Object v1, final Object v2) {
430                if (v1 instanceof String && v2 instanceof String) {
431                    final String s1 = (String) v1;
432                    final String s2 = (String) v2;
433                    return s1.compareToIgnoreCase(s2);
434                } else if (v1 instanceof Number && v2 instanceof Number) {
435                    final Double n1 = ((Number) v1).doubleValue();
436                    final Double n2 = ((Number) v2).doubleValue();
437                    return n1.compareTo(n2);
438                } else if (v1 instanceof Boolean && v2 instanceof Boolean) {
439                    final Boolean b1 = (Boolean) v1;
440                    final Boolean b2 = (Boolean) v2;
441                    return b1.compareTo(b2);
442                } else {
443                    // Different types: we need to ensure predictable ordering,
444                    // so use class name as secondary key.
445                    return v1.getClass().getName().compareTo(v2.getClass().getName());
446                }
447            }
448
449            private boolean isCompatible(final Object v1, final Object v2) {
450                return (v1 instanceof String && v2 instanceof String)
451                        || (v1 instanceof Number && v2 instanceof Number)
452                        || (v1 instanceof Boolean && v2 instanceof Boolean);
453            }
454
455        };
456}