ListRuleProcessor.java

package io.github.pojotools.flat2pojo.core.impl;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.github.pojotools.flat2pojo.core.config.MappingConfig;
import io.github.pojotools.flat2pojo.core.engine.ArrayManager;
import io.github.pojotools.flat2pojo.core.engine.Path;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

/** Processes a single list rule for a row. Single Responsibility: List rule processing logic. */
final class ListRuleProcessor {
  private final ProcessingContext context;
  private final ArrayManager arrayManager;
  private final ListElementWriter writer;
  private final Map<String, ObjectNode> listElementCache =
      new LinkedHashMap<>(); // Shared across rows

  ListRuleProcessor(final AssemblerDependencies dependencies, final ProcessingContext context) {
    this.context = context;
    this.arrayManager = dependencies.arrayManager();
    this.writer = new ListElementWriter(context, dependencies.primitiveArrayManager());
  }

  void processRule(
      final Map<String, JsonNode> rowValues,
      final Set<String> skippedListPaths,
      final MappingConfig.ListRule rule,
      final ObjectNode root) {
    if (shouldSkipDueToParent(rule.path(), skippedListPaths)) {
      return;
    }
    processListElementCreation(rowValues, skippedListPaths, rule, root);
  }

  private void processListElementCreation(
      final Map<String, JsonNode> rowValues,
      final Set<String> skippedListPaths,
      final MappingConfig.ListRule rule,
      final ObjectNode root) {
    final ObjectNode listElement = createListElement(rowValues, rule, root);
    if (listElement == null) {
      markAsSkipped(skippedListPaths, rule);
    } else {
      listElementCache.put(rule.path(), listElement);
      copyValuesToElement(rowValues, listElement, rule);
    }
  }

  private boolean shouldSkipDueToParent(final String listPath, final Set<String> skippedListPaths) {
    if (isSkippedDueToParent(listPath, skippedListPaths)) {
      context
          .config()
          .reporter()
          .ifPresent(
              r ->
                  r.warn(
                      "Skipping list rule '"
                          + listPath
                          + "' because parent list was skipped due to missing keyPath"));
      return true;
    }
    return false;
  }

  private void markAsSkipped(
      final Set<String> skippedListPaths, final MappingConfig.ListRule rule) {
    skippedListPaths.add(rule.path());
    context
        .config()
        .reporter()
        .ifPresent(
            r ->
                r.warn(
                    "Skipping list rule '"
                        + rule.path()
                        + "' because keyPath(s) "
                        + rule.keyPaths()
                        + " are missing or null"));
  }

  private boolean isSkippedDueToParent(final String listPath, final Set<String> skippedListPaths) {
    return context.pathResolver().isUnderAny(listPath, skippedListPaths);
  }

  private ObjectNode createListElement(
      final Map<String, JsonNode> rowValues,
      final MappingConfig.ListRule rule,
      final ObjectNode root) {
    final String listPath = rule.path();
    final String parentListPath = context.hierarchyCache().getParentListPath(listPath);
    final ObjectNode baseObject = findBaseObject(parentListPath, root);
    final String relativePath = computeRelativePath(listPath, parentListPath);
    return arrayManager.upsertListElement(baseObject, relativePath, rowValues, rule);
  }

  private ObjectNode findBaseObject(final String parentListPath, final ObjectNode root) {
    final ObjectNode baseObject = resolveBaseObject(parentListPath, root);
    if (baseObject == null) {
      throw new IllegalStateException(
          "Parent list element for '" + parentListPath + "' not found in cache");
    }
    return baseObject;
  }

  private String computeRelativePath(final String listPath, final String parentListPath) {
    return parentListPath == null
        ? listPath
        : context.pathResolver().tailAfter(listPath, parentListPath);
  }

  private ObjectNode resolveBaseObject(final String parentListPath, final ObjectNode root) {
    return parentListPath == null ? root : listElementCache.get(parentListPath);
  }

  private void copyValuesToElement(
      final Map<String, JsonNode> rowValues,
      final ObjectNode element,
      final MappingConfig.ListRule rule) {
    final WriteContext writeContext =
        new WriteContext(rule, context.pathResolver().buildPrefix(rule.path()));
    for (final var entry : rowValues.entrySet()) {
      if (entry.getKey().startsWith(writeContext.pathPrefix())) {
        writeValueIfNotUnderChild(element, entry, writeContext);
      }
    }
  }

  private void writeValueIfNotUnderChild(
      final ObjectNode element,
      final Map.Entry<String, JsonNode> entry,
      final WriteContext writeContext) {
    final String relativePath =
        context.pathResolver().stripPrefix(entry.getKey(), writeContext.pathPrefix());
    if (!context.hierarchyCache().isUnderAnyChildList(relativePath, writeContext.rule().path())) {
      final String absolutePath = entry.getKey();
      final Path path = new Path(relativePath, absolutePath);
      writer.writeWithConflictPolicy(
          element, path, entry.getValue(), writeContext.rule().onConflict());
    }
  }

  private record WriteContext(MappingConfig.ListRule rule, String pathPrefix) {}
}