ValueTransformer.java
package io.github.pojotools.flat2pojo.core.engine;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.BooleanNode;
import com.fasterxml.jackson.databind.node.DoubleNode;
import com.fasterxml.jackson.databind.node.IntNode;
import com.fasterxml.jackson.databind.node.LongNode;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.TextNode;
import io.github.pojotools.flat2pojo.core.config.MappingConfig;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import lombok.AccessLevel;
import lombok.experimental.FieldDefaults;
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public final class ValueTransformer {
ObjectMapper objectMapper;
Map<String, MappingConfig.PrimitiveSplitRule> splitRulesCache;
boolean blanksAsNulls;
public ValueTransformer(final ObjectMapper objectMapper, final MappingConfig config) {
this.objectMapper = objectMapper;
this.blanksAsNulls = config.nullPolicy() != null && config.nullPolicy().blanksAsNulls();
this.splitRulesCache = buildSplitRulesCache(config);
}
private static Map<String, MappingConfig.PrimitiveSplitRule> buildSplitRulesCache(
final MappingConfig config) {
final Map<String, MappingConfig.PrimitiveSplitRule> cache = new HashMap<>();
for (final MappingConfig.PrimitiveSplitRule rule : config.primitives()) {
cache.put(rule.path(), rule);
}
return cache;
}
/**
* Transforms flat row values directly to JsonNode map without building intermediate tree. More
* efficient than build-then-flatten approach for list processing.
*/
public Map<String, JsonNode> transformRowValuesToJsonNodes(final Map<String, ?> row) {
final Map<String, JsonNode> result = new LinkedHashMap<>(row.size());
for (final var entry : row.entrySet()) {
transformEntry(entry, result);
}
return result;
}
private void transformEntry(
final Map.Entry<String, ?> entry, final Map<String, JsonNode> result) {
final String key = entry.getKey();
final Object normalized = normalizeBlankValue(entry.getValue());
final JsonNode valueNode = createValueNode(key, normalized);
result.put(key, valueNode);
}
private Object normalizeBlankValue(final Object rawValue) {
if (rawValue instanceof String stringValue && blanksAsNulls && stringValue.isBlank()) {
return null;
}
return rawValue;
}
private JsonNode createValueNode(final String key, final Object rawValue) {
final MappingConfig.PrimitiveSplitRule splitRule = splitRulesCache.get(key);
if (splitRule != null && rawValue instanceof String stringValue) {
return createSplitArrayNode(stringValue, splitRule);
} else {
return createLeafNode(rawValue);
}
}
private ArrayNode createSplitArrayNode(
final String stringValue, final MappingConfig.PrimitiveSplitRule splitRule) {
final String[] parts =
stringValue.split(java.util.regex.Pattern.quote(splitRule.delimiter()), -1);
final ArrayNode arrayNode = objectMapper.createArrayNode();
for (final String part : parts) {
arrayNode.add(createArrayElement(part, splitRule.trim()));
}
return arrayNode;
}
private JsonNode createArrayElement(final String part, final boolean shouldTrim) {
final String processed = shouldTrim ? part.trim() : part;
if (blanksAsNulls && processed.isBlank()) {
return NullNode.getInstance();
}
return TextNode.valueOf(processed);
}
private JsonNode createLeafNode(final Object rawValue) {
return switch (rawValue) {
case null -> NullNode.getInstance();
case String stringValue -> createStringNode(stringValue);
case Integer intValue -> IntNode.valueOf(intValue);
case Long longValue -> LongNode.valueOf(longValue);
case Double doubleValue -> DoubleNode.valueOf(doubleValue);
case Boolean boolValue -> BooleanNode.valueOf(boolValue);
default -> objectMapper.valueToTree(rawValue);
};
}
private JsonNode createStringNode(final String stringValue) {
if (blanksAsNulls && stringValue.isBlank()) {
return NullNode.getInstance();
}
return TextNode.valueOf(stringValue);
}
}