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.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
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}