DocumentValueJson.java
package org.hammer.audio.experiment.document;
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.DecimalNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
import org.hammer.audio.plugin.document.DocumentValue;
/** Converts host-owned JSON trees to the framework-neutral plugin value model and back. */
@SuppressWarnings("PMD.LooseCoupling")
final class DocumentValueJson {
private DocumentValueJson() {
// utility class
}
static DocumentValue fromJson(JsonNode node) throws ExperimentDocumentException {
return fromJson(node, "/", 0);
}
private static DocumentValue fromJson(JsonNode node, String pointer, int depth)
throws ExperimentDocumentException {
requireDepth(depth, pointer);
if (node == null || node.isNull()) {
return DocumentValue.nullValue();
}
if (node.isObject()) {
requireSize(node.size(), pointer);
TreeMap<String, DocumentValue> fields = new TreeMap<>();
Iterator<Map.Entry<String, JsonNode>> entries = node.fields();
while (entries.hasNext()) {
Map.Entry<String, JsonNode> entry = entries.next();
String childPointer = pointer(pointer, entry.getKey());
requireString(entry.getKey(), childPointer);
fields.put(entry.getKey(), fromJson(entry.getValue(), childPointer, depth + 1));
}
return DocumentValue.object(fields);
}
if (node.isArray()) {
requireSize(node.size(), pointer);
ArrayList<DocumentValue> values = new ArrayList<>(node.size());
for (int index = 0; index < node.size(); index++) {
values.add(fromJson(node.get(index), pointer + "/" + index, depth + 1));
}
return DocumentValue.array(values);
}
if (node.isTextual()) {
return DocumentValue.string(requireString(node.textValue(), pointer));
}
if (node.isNumber()) {
return DocumentValue.number(node.decimalValue());
}
if (node.isBoolean()) {
return DocumentValue.bool(node.booleanValue());
}
throw new ExperimentDocumentException(
pointer, "unsupported-value", "Unsupported JSON value type at " + pointer);
}
static JsonNode toJson(DocumentValue value) {
return switch (value) {
case DocumentValue.ObjectValue objectValue -> objectNode(objectValue.fields());
case DocumentValue.ArrayValue arrayValue -> arrayNode(arrayValue);
case DocumentValue.StringValue stringValue -> new TextNode(stringValue.value());
case DocumentValue.NumberValue numberValue -> new DecimalNode(numberValue.value());
case DocumentValue.BooleanValue booleanValue -> BooleanNode.valueOf(booleanValue.value());
case DocumentValue.NullValue ignored -> NullNode.getInstance();
};
}
private static ObjectNode objectNode(Map<String, DocumentValue> values) {
ObjectNode result = JsonNodeFactory.instance.objectNode();
new TreeMap<>(values).forEach((key, value) -> result.set(key, toJson(value)));
return result;
}
private static ArrayNode arrayNode(DocumentValue.ArrayValue value) {
ArrayNode result = JsonNodeFactory.instance.arrayNode();
value.values().forEach(item -> result.add(toJson(item)));
return result;
}
static String pointer(String parent, String key) {
String escaped = key.replace("~", "~0").replace("/", "~1");
return "/".equals(parent) ? "/" + escaped : parent + "/" + escaped;
}
private static void requireDepth(int depth, String pointer) throws ExperimentDocumentException {
if (depth > ExperimentDocumentFormat.MAX_NESTING_DEPTH) {
throw new ExperimentDocumentException(
pointer, "max-depth", "Document nesting exceeds the configured limit");
}
}
private static void requireSize(int size, String pointer) throws ExperimentDocumentException {
if (size > ExperimentDocumentFormat.MAX_COLLECTION_SIZE) {
throw new ExperimentDocumentException(
pointer, "max-collection", "Document collection exceeds the configured limit");
}
}
private static String requireString(String value, String pointer)
throws ExperimentDocumentException {
if (value.length() > ExperimentDocumentFormat.MAX_STRING_LENGTH) {
throw new ExperimentDocumentException(
pointer, "max-string", "Document string exceeds the configured limit");
}
return value;
}
}