ComparatorBuilder.java
package io.github.pojotools.flat2pojo.core.engine;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.github.pojotools.flat2pojo.core.config.MappingConfig;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Builds and caches comparators for list ordering. Single Responsibility: Comparator construction
* logic only.
*/
final class ComparatorBuilder {
private final String separator;
private final Map<String, List<Comparator<ObjectNode>>> comparatorCache = new HashMap<>();
ComparatorBuilder(final String separator) {
this.separator = separator;
}
void precomputeComparators(final MappingConfig config) {
for (final var rule : config.lists()) {
final List<Comparator<ObjectNode>> ruleComparators = buildComparators(rule);
comparatorCache.put(rule.path(), ruleComparators);
}
}
List<Comparator<ObjectNode>> getComparatorsForPath(final String path) {
return comparatorCache.getOrDefault(path, List.of());
}
private List<Comparator<ObjectNode>> buildComparators(final MappingConfig.ListRule rule) {
final List<Comparator<ObjectNode>> comparatorList = new ArrayList<>();
for (final var orderBy : rule.orderBy()) {
final String relativePath = orderBy.path();
final boolean isAscending = orderBy.direction() == MappingConfig.OrderDirection.asc;
final boolean nullsFirst = orderBy.nulls() == MappingConfig.Nulls.first;
comparatorList.add(createFieldComparator(relativePath, isAscending, nullsFirst));
}
return comparatorList;
}
private Comparator<ObjectNode> createFieldComparator(
final String relativePath, final boolean isAscending, final boolean nullsFirst) {
return (nodeA, nodeB) -> {
final JsonNode valueA = findValueAtPath(nodeA, relativePath);
final JsonNode valueB = findValueAtPath(nodeB, relativePath);
return compareWithNulls(valueA, valueB, isAscending, nullsFirst);
};
}
private int compareWithNulls(
final JsonNode valueA,
final JsonNode valueB,
final boolean isAscending,
final boolean nullsFirst) {
final boolean isANull = isNullOrMissing(valueA);
final boolean isBNull = isNullOrMissing(valueB);
if (isANull && isBNull) {
return 0;
}
if (isANull) {
return nullsFirst ? -1 : 1;
}
if (isBNull) {
return nullsFirst ? 1 : -1;
}
return compareValues(valueA, valueB, isAscending);
}
private int compareValues(
final JsonNode valueA, final JsonNode valueB, final boolean isAscending) {
final int comparison = compareJsonValues(valueA, valueB);
return isAscending ? comparison : -comparison;
}
private JsonNode findValueAtPath(final ObjectNode base, final String relativePath) {
if (relativePath.isEmpty()) {
return base;
}
return traversePath(base, relativePath);
}
private JsonNode traversePath(final ObjectNode base, final String path) {
JsonNode current = base;
int start = 0;
while (start < path.length()) {
current = navigateToNextSegment(current, path, start);
if (isNullOrMissing(current)) {
return NullNode.getInstance();
}
start = calculateNextStart(path, start);
}
return current;
}
private boolean isNullOrMissing(final JsonNode node) {
return node == null || node.isNull();
}
private JsonNode navigateToNextSegment(
final JsonNode current, final String path, final int start) {
if (!(current instanceof ObjectNode objectNode)) {
return null;
}
final String segment = extractSegment(path, start);
return objectNode.get(segment);
}
private int calculateNextStart(final String path, final int start) {
final int separatorIndex = path.indexOf(separator, start);
return separatorIndex >= 0 ? separatorIndex + separator.length() : path.length();
}
private String extractSegment(final String path, final int start) {
final int separatorIndex = path.indexOf(separator, start);
return separatorIndex >= 0 ? path.substring(start, separatorIndex) : path.substring(start);
}
private static int compareJsonValues(final JsonNode a, final JsonNode b) {
if (a.isNumber() && b.isNumber()) {
return Double.compare(a.asDouble(), b.asDouble());
}
return a.asText().compareTo(b.asText());
}
}