JsonToFlatMapConverter.java
/*
* Copyright 2019 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gringlobal.spring;
import java.lang.reflect.Field;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.BooleanNode;
import com.fasterxml.jackson.databind.node.MissingNode;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.NumericNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.ValueNode;
import org.springframework.util.ReflectionUtils;
/**
* Converts a JSON to a map of keys (full JSON paths) and values
*
* @author Maxym Borodenko
* @author Matija Obreza
*/
@Slf4j
public class JsonToFlatMapConverter {
private final Map<String, Object> map = new LinkedHashMap<>();
private final Set<String> initialPathsOfArrays = new HashSet<>();
private final List<String> excludedFields;
private final Map<String, Map<Number, ObjectNode>> cachedObjects;
private JsonToFlatMapConverter(List<String> excludedFields, Map<String, Map<Number, ObjectNode>> cachedObjects) {
this.excludedFields = excludedFields;
this.cachedObjects = cachedObjects;
}
public Set<String> getInitialPathsOfArrays() {
return this.initialPathsOfArrays;
}
public Map<String, Object> getMap() {
return this.map;
}
public static JsonToFlatMapConverter fromJson(Class<?> clazz, final JsonNode jsonNode, List<String> include, List<String> exclude, Map<String, Map<Number, ObjectNode>> cachedObjects) {
JsonToFlatMapConverter jtm = new JsonToFlatMapConverter(exclude, cachedObjects);
if (jsonNode == null) {
return jtm;
}
if (include.isEmpty()) {
jtm.readObject(jsonNode, "", clazz);
} else {
for (String field : include) {
String point = Paths.get("/", field.split("\\.")).toString();
JsonNode node = jsonNode.at(point);
if (!node.isMissingNode()) {
// Find type of property at node point
Class<?> objectClazz = clazz;
for (var prop : field.split("\\.")) {
Field objectField = null;
if (objectClazz != null) {
objectField = ReflectionUtils.findField(objectClazz, prop);
}
if (objectField == null) {
log.trace("Could not find field {} in {} for path={}", prop, objectClazz, field);
objectClazz = null;
break;
}
objectClazz = objectField.getType();
}
// Read node value
jtm.readValue(node, field, objectClazz);
}
}
}
return jtm;
}
private void readObject(final JsonNode object, final String jsonPath, Class<?> clazz) {
if (object == null) {
log.trace("Object at path={} is null.", jsonPath);
return;
}
Iterator<String> keysItr = object.fieldNames();
while (keysItr.hasNext()) {
String key = keysItr.next();
String parentPath = StringUtils.isEmpty(jsonPath) ? key : jsonPath + "." + key;
// MAYBE use cache for excluded paths instead of .anyMatch
if (CollectionUtils.isNotEmpty(this.excludedFields) && this.excludedFields.stream().anyMatch(parentPath::startsWith))
continue;
Field objectField = null;
if (clazz != null) {
objectField = ReflectionUtils.findField(clazz, key);
}
readValue(object.get(key), parentPath, objectField == null ? null : objectField.getType());
}
}
private void readArray(final ArrayNode array, final String jsonPath) {
this.initialPathsOfArrays.add(jsonPath);
for (int i = 0; i < array.size(); i++) {
readValue(array.get(i), jsonPath + (i + 1), null);
}
}
private void readValue(final Object value, final String jsonPath, Class<?> clazz) {
if (value instanceof ArrayNode) {
// TODO find type of element in collection
readArray((ArrayNode) value, jsonPath);
} else if (value instanceof ObjectNode) {
cacheAndReadObject((ObjectNode) value, jsonPath, clazz);
} else if (value instanceof ValueNode) {
ValueNode valueNode = (ValueNode) value;
if (valueNode == null || valueNode instanceof NullNode || valueNode instanceof MissingNode) {
} else if (valueNode instanceof NumericNode) {
readCachedObjectOrPutNumber((NumericNode)valueNode, jsonPath, clazz);
} else if (valueNode instanceof BooleanNode) {
this.map.put(jsonPath, valueNode.booleanValue() ? 1 : 0);
} else {
this.map.put(jsonPath, valueNode.asText());
}
}
}
private void readCachedObjectOrPutNumber(NumericNode value, String jsonPath, Class<?> clazz) {
var cacheName = clazz == null ? jsonPath : clazz.getName();
var cache = this.cachedObjects.get(cacheName);
if (cache != null) {
log.trace("Looking up cached object for path={} type={} key={}", jsonPath, clazz, value);
ObjectNode cachedObject = this.cachedObjects.get(cacheName).get(value.numberValue());
readObject(cachedObject, jsonPath, clazz);
} else {
this.map.put(jsonPath, value.numberValue());
}
}
private void cacheAndReadObject(final ObjectNode object, final String jsonPath, Class<?> clazz) {
if (object.get("id") != null && object.get("id").isNumber()) {
log.trace("Caching object path={} type={} obj={}", jsonPath, clazz, object);
var cacheName = clazz == null ? jsonPath : clazz.getName();
var cache = this.cachedObjects.get(cacheName);
if (cache != null) {
cache.put(object.get("id").numberValue(), object);
} else {
Map<Number, ObjectNode> objectsByPath = new HashMap<>();
objectsByPath.put(object.get("id").numberValue(), object);
this.cachedObjects.put(cacheName, objectsByPath);
}
}
readObject(object, jsonPath, clazz);
}
}