JsonValueUtils.java
/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2015-2016 ForgeRock AS.
*/
package org.forgerock.audit.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
import org.forgerock.util.query.QueryFilter;
import org.forgerock.util.query.QueryFilterVisitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Contains some JsonValue Utility methods.
*/
public final class JsonValueUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(JsonValueUtils.class);
private static final ObjectMapper MAPPER = new ObjectMapper();
private JsonValueUtils() {
// utility class
}
/**
* Expands a Json Object Map, where the keys of the map are the {@link JsonPointer JsonPointer }s
* and the values are the value the {@link JsonPointer JsonPointer } resolves to.
*
* For Example, the following key-value pairs
* <pre>
* /object/array/0 , "test"
* /object/array/1 , "test1"
* /string , "stringVal"
* /boolean , false
* /number , 1
* /array/0 , "value1"
* /array/1 , "value2"
* </pre>
*
* will produce the following json object
* <pre>
* {
* "object" : {
* "array" : ["test", "test1"]
* },
* "string" : "stringVal",
* "boolean" : false,
* "number" : 1,
* "array" : ["value1", "value2"]
* }
* </pre>
* @param object the Json Object Map containing the {@link JsonPointer JsonPointer }s and values
* @return the {@link JsonValue JsonValue } expanded from the object map
*/
public static JsonValue expand(final Map<String, Object> object) {
//sort the objects so the array objects are in order
final Map<String, Object> sortedObjects = new TreeMap<>(object);
return buildObject(sortedObjects);
}
/**
*
* Flattens a {@link JsonValue JsonValue } to a Map, where the keys of the Map are {@link JsonPointer JsonPointer }s
* and the values are the value the {@link JsonPointer JsonPointer }s resolve to.
*
* For Example, the following JsonValue
*
* <pre>
* {
* "object" : {
* "array" : ["test", "test1"]
* },
* "string" : "stringVal",
* "boolean" : false,
* "number" : 1,
* "array" : ["value1", "value2"]
* }
* </pre>
*
* will produce the following Map key-value pairs
*
* <pre>
* /object/array/0 , "test"
* /object/array/1 , "test1"
* /string , "stringVal"
* /boolean , false
* /number , 1
* /array/0 , "value1"
* /array/1 , "value2"
* </pre>
* @param jsonValue the {@link JsonValue JsonValue } object to flatten
* @return a Map representing the flattened {@link JsonValue JsonValue } object
*/
public static Map<String, Object> flatten(final JsonValue jsonValue) {
Map<String, Object> flatObject = new LinkedHashMap<>();
flatten(new JsonPointer(), jsonValue, flatObject);
return flatObject;
}
/**
* Extracts String representation of field identified by <code>fieldName</code> from <code>json</code> object.
* <code>JsonValue</code> transformers are applied up to <code>fieldName</code> but not to its components.
*
* @param json the {@link JsonValue} object from which to extract a value.
* @param fieldName the field identifier in a form consumable by {@link JsonPointer}.
*
* @return A non-null String representation of the field's value. If the specified field is not present or has
* a null value, null will be returned.
*/
public static String extractValueAsString(final JsonValue json, final String fieldName) {
return extractValueAsString(json, new JsonPointer(fieldName));
}
/**
* Extracts String representation of field identified by <code>pointer</code> from <code>json</code> object.
* <code>JsonValue</code> transformers are applied up to <code>fieldName</code> but not to its components.
*
* @param json the {@link JsonValue} object from which to extract a value.
* @param pointer the field identifier as a {@link JsonPointer}.
*
* @return A non-null String representation of the field's value. If the specified field is not present or has
* a null value, null will be returned.
*/
public static String extractValueAsString(final JsonValue json, final JsonPointer pointer) {
Object value = json.getObject();
for (String name : pointer.toArray()) {
if (value instanceof Map<?, ?>) {
value = ((Map<?, ?>) value).get(name);
} else if (value instanceof Collection<?>) {
int valueNum = JsonValue.toIndex(name);
Collection<?> col = (Collection<?>) value;
if (valueNum < 0 || valueNum > col.size()) {
value = null;
break;
}
int cnt = 0;
for (Object o : col) {
if (cnt == valueNum) {
value = o;
break;
}
cnt++;
}
} else {
value = null;
break;
}
}
if (value == null) {
return null;
}
if (value instanceof String || value instanceof Integer || value instanceof Long || value instanceof Boolean) {
return String.valueOf(value);
} else {
return extractComplexString(value, pointer);
}
}
private static String extractComplexString(Object value, JsonPointer name) {
String rawStr = null;
try {
rawStr = MAPPER.writeValueAsString(value);
} catch (JsonProcessingException e) {
LOGGER.error("Unable to write the value for field {} as a string.", name.toString());
}
return rawStr;
}
private static JsonValue buildObject(Map<String, Object> objectSet) {
final JsonValue jsonValue = new JsonValue(new LinkedHashMap<>());
for (Map.Entry<String, Object> entry : objectSet.entrySet()) {
final String key = entry.getKey();
final Object value = entry.getValue();
if (jsonValue.get(new JsonPointer(key)) != null
&& !jsonValue.get(new JsonPointer(key)).isNull()) {
//only build a sub json object for one prefix value
continue;
}
final JsonPointer jsonPointer = new JsonPointer(key);
int numberOfIndexTokens = getIndexTokens(jsonPointer);
if (numberOfIndexTokens > 1) {
//more than one json array must build the sub json object
int firstIndexTokenPos = getNextIndexToken(jsonPointer, 0);
final JsonPointer prefix = subJsonPointer(jsonPointer, 0, firstIndexTokenPos + 1);
jsonValue.putPermissive(
replaceLastIndexToken(prefix),
buildObject(findObjectsThatMatchPrefix(prefix, objectSet)).getObject()
);
} else {
jsonValue.putPermissive(replaceLastIndexToken(jsonPointer), value);
}
}
return jsonValue;
}
private static Map<String, Object> findObjectsThatMatchPrefix(
final JsonPointer prefix,
Map<String, Object> objectSet) {
Map<String, Object> matchingObjects = new LinkedHashMap<>();
for (final String key : objectSet.keySet()) {
if (key.startsWith(prefix.toString())) {
matchingObjects.put(key.substring(prefix.toString().length(), key.length()), objectSet.get(key));
}
}
return matchingObjects;
}
private static boolean isIndexToken(final String token) {
if (token.isEmpty()) {
return false;
} else {
for (int i = 0; i < token.length(); i++) {
final char c = token.charAt(i);
if (!Character.isDigit(c)) {
return false;
}
}
return true;
}
}
private static JsonPointer replaceLastIndexToken(final JsonPointer jsonPointer) {
final String[] jsonPointerTokens = jsonPointer.toArray();
if (getNextIndexToken(jsonPointer, jsonPointer.size() - 1) != -1) {
jsonPointerTokens[jsonPointerTokens.length - 1] = "-";
}
return new JsonPointer(jsonPointerTokens);
}
private static int getNextIndexToken(final JsonPointer jsonPointer, int start) {
final String[] jsonPointerTokens = jsonPointer.toArray();
for (int i = start; i < jsonPointerTokens.length; i++) {
if (isIndexToken(jsonPointerTokens[i])) {
return i;
}
}
return -1;
}
private static JsonPointer subJsonPointer(final JsonPointer jsonPointer, final int start, final int end) {
final String[] jsonPointerTokens = jsonPointer.toArray();
final List<String> newJsonPointerTokens = new ArrayList<>();
for (int i = start; i < end; i++) {
newJsonPointerTokens.add(jsonPointerTokens[i]);
}
return new JsonPointer(newJsonPointerTokens.toArray(new String[newJsonPointerTokens.size()]));
}
private static int getIndexTokens(final JsonPointer jsonPointer) {
int numberOfIndexTokens = 0;
final String[] jsonPointerTokens = jsonPointer.toArray();
for (int i = 0; i < jsonPointerTokens.length; i++) {
if (isIndexToken(jsonPointerTokens[i])) {
numberOfIndexTokens++;
}
}
return numberOfIndexTokens;
}
private static void flatten(
final JsonPointer pointer,
final JsonValue jsonValue,
final Map<String, Object> flatObject) {
final Set<String> jsonValueKeys = jsonValue.get(pointer).keys();
for (final String key : jsonValueKeys) {
final JsonPointer keyPointer = concatJsonPointer(pointer, key);
final JsonValue temp = jsonValue.get(keyPointer);
if (temp.isMap() || temp.isList()) {
flatten(keyPointer, jsonValue, flatObject);
} else {
flatObject.put(
keyPointer.toString(),
jsonValue.get(keyPointer).getObject());
}
}
return;
}
private static JsonPointer concatJsonPointer(final JsonPointer pointer, final String key) {
final String[] pointerTokens = pointer.toArray();
final String[] newPointerTokens = Arrays.copyOf(pointerTokens, pointerTokens.length + 1);
newPointerTokens[pointerTokens.length] = key;
return new JsonPointer(newPointerTokens);
}
/**
* A generic JsonValue Query Filter Visitor.
*/
public static final QueryFilterVisitor<Boolean, JsonValue, JsonPointer> JSONVALUE_FILTER_VISITOR =
new QueryFilterVisitor<Boolean, JsonValue, JsonPointer>() {
@Override
public Boolean visitAndFilter(final JsonValue p, final List<QueryFilter<JsonPointer>> subFilters) {
for (final QueryFilter<JsonPointer> subFilter : subFilters) {
if (!subFilter.accept(this, p)) {
return Boolean.FALSE;
}
}
return Boolean.TRUE;
}
@Override
public Boolean visitBooleanLiteralFilter(final JsonValue p, final boolean value) {
return value;
}
@Override
public Boolean visitContainsFilter(final JsonValue p, final JsonPointer field,
final Object valueAssertion) {
for (final Object value : getValues(p, field)) {
if (isCompatible(valueAssertion, value)) {
if (valueAssertion instanceof String) {
final String s1 = ((String) valueAssertion).toLowerCase(Locale.ENGLISH);
final String s2 = ((String) value).toLowerCase(Locale.ENGLISH);
if (s2.contains(s1)) {
return Boolean.TRUE;
}
} else {
// Use equality matching for numbers and booleans.
if (compareValues(valueAssertion, value) == 0) {
return Boolean.TRUE;
}
}
}
}
return Boolean.FALSE;
}
@Override
public Boolean visitEqualsFilter(final JsonValue p, final JsonPointer field,
final Object valueAssertion) {
Boolean result = Boolean.TRUE;
for (final Object value : getValues(p, field)) {
if (!isCompatible(valueAssertion, value) || compareValues(valueAssertion, value) != 0) {
result = Boolean.FALSE;
}
}
return result;
}
@Override
public Boolean visitExtendedMatchFilter(final JsonValue p, final JsonPointer field,
final String matchingRuleId, final Object valueAssertion) {
// Extended filters are not supported
return Boolean.FALSE;
}
@Override
public Boolean visitGreaterThanFilter(final JsonValue p, final JsonPointer field,
final Object valueAssertion) {
for (final Object value : getValues(p, field)) {
if (isCompatible(valueAssertion, value) && compareValues(valueAssertion, value) < 0) {
return Boolean.TRUE;
}
}
return Boolean.FALSE;
}
@Override
public Boolean visitGreaterThanOrEqualToFilter(final JsonValue p, final JsonPointer field,
final Object valueAssertion) {
for (final Object value : getValues(p, field)) {
if (isCompatible(valueAssertion, value) && compareValues(valueAssertion, value) <= 0) {
return Boolean.TRUE;
}
}
return Boolean.FALSE;
}
@Override
public Boolean visitLessThanFilter(final JsonValue p, final JsonPointer field,
final Object valueAssertion) {
for (final Object value : getValues(p, field)) {
if (isCompatible(valueAssertion, value) && compareValues(valueAssertion, value) > 0) {
return Boolean.TRUE;
}
}
return Boolean.FALSE;
}
@Override
public Boolean visitLessThanOrEqualToFilter(final JsonValue p, final JsonPointer field,
final Object valueAssertion) {
for (final Object value : getValues(p, field)) {
if (isCompatible(valueAssertion, value) && compareValues(valueAssertion, value) >= 0) {
return Boolean.TRUE;
}
}
return Boolean.FALSE;
}
@Override
public Boolean visitNotFilter(final JsonValue p, final QueryFilter<JsonPointer> subFilter) {
return !subFilter.accept(this, p);
}
@Override
public Boolean visitOrFilter(final JsonValue p, final List<QueryFilter<JsonPointer>> subFilters) {
for (final QueryFilter<JsonPointer> subFilter : subFilters) {
if (subFilter.accept(this, p)) {
return Boolean.TRUE;
}
}
return Boolean.FALSE;
}
@Override
public Boolean visitPresentFilter(final JsonValue p, final JsonPointer field) {
final JsonValue value = p.get(field);
return value != null;
}
@Override
public Boolean visitStartsWithFilter(final JsonValue p, final JsonPointer field,
final Object valueAssertion) {
for (final Object value : getValues(p, field)) {
if (isCompatible(valueAssertion, value)) {
if (valueAssertion instanceof String) {
final String s1 = ((String) valueAssertion).toLowerCase(Locale.ENGLISH);
final String s2 = ((String) value).toLowerCase(Locale.ENGLISH);
if (s2.startsWith(s1)) {
return Boolean.TRUE;
}
} else {
// Use equality matching for numbers and booleans.
if (compareValues(valueAssertion, value) == 0) {
return Boolean.TRUE;
}
}
}
}
return Boolean.FALSE;
}
private List<Object> getValues(final JsonValue resource, final JsonPointer field) {
final JsonValue value = resource.get(field);
if (value == null) {
return Collections.emptyList();
} else if (value.isList()) {
return value.asList();
} else {
return Collections.singletonList(value.getObject());
}
}
private int compareValues(final Object v1, final Object v2) {
if (v1 instanceof String && v2 instanceof String) {
final String s1 = (String) v1;
final String s2 = (String) v2;
return s1.compareToIgnoreCase(s2);
} else if (v1 instanceof Number && v2 instanceof Number) {
final Double n1 = ((Number) v1).doubleValue();
final Double n2 = ((Number) v2).doubleValue();
return n1.compareTo(n2);
} else if (v1 instanceof Boolean && v2 instanceof Boolean) {
final Boolean b1 = (Boolean) v1;
final Boolean b2 = (Boolean) v2;
return b1.compareTo(b2);
} else {
// Different types: we need to ensure predictable ordering,
// so use class name as secondary key.
return v1.getClass().getName().compareTo(v2.getClass().getName());
}
}
private boolean isCompatible(final Object v1, final Object v2) {
return (v1 instanceof String && v2 instanceof String)
|| (v1 instanceof Number && v2 instanceof Number)
|| (v1 instanceof Boolean && v2 instanceof Boolean);
}
};
}