PathOps.java

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

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.List;

/**
 * High-performance path manipulation utilities for flat2pojo.
 *
 * <p>This class provides optimized string operations for handling hierarchical paths without the
 * overhead of regular expressions or unnecessary string allocations. All methods are static and
 * thread-safe.
 */
public final class PathOps {
  private PathOps() {}

  public static String tailAfter(final String s, final String prefix, final String sep) {
    if (!s.startsWith(prefix)) {
      return s;
    }
    final int prefixLen = prefix.length();
    if (s.length() == prefixLen) {
      return "";
    }
    if (s.startsWith(sep, prefixLen)) {
      return s.substring(prefixLen + sep.length());
    }
    return s.substring(prefixLen);
  }

  public static boolean isUnder(final String path, final String prefix, final String sep) {
    return path.equals(prefix) || path.startsWith(prefix + sep);
  }

  public static List<String> splitPath(final String path, final String separator) {
    return List.of(path.split(java.util.regex.Pattern.quote(separator), -1));
  }

  /**
   * Traverses a path and ensures all intermediate ObjectNodes exist. Returns the final parent
   * ObjectNode where the last segment should be set. Supports multi-character separators.
   */
  public static ObjectNode traverseAndEnsurePath(
      final ObjectNode root,
      final String path,
      final String separator,
      final ObjectNodeEnsurer ensurer) {
    ObjectNode current = root;
    int start = 0;
    int sepIndex;

    while ((sepIndex = path.indexOf(separator, start)) >= 0) {
      final String segment = path.substring(start, sepIndex);
      current = ensurer.ensureObject(current, segment);
      start = sepIndex + separator.length();
    }

    return current;
  }

  /** Gets the final segment of a path after the last separator. */
  public static String getFinalSegment(final String path, final String separator) {
    final int lastSep = path.lastIndexOf(separator);
    return lastSep >= 0 ? path.substring(lastSep + separator.length()) : path;
  }

  /** Interface for ensuring ObjectNode creation - allows different implementations. */
  @FunctionalInterface
  public interface ObjectNodeEnsurer {
    ObjectNode ensureObject(ObjectNode parent, String fieldName);
  }

  /** Standard implementation of ensureObject - consolidated from 3 duplicates. */
  public static ObjectNode ensureObject(final ObjectNode parent, final String fieldName) {
    final JsonNode existing = parent.get(fieldName);
    if (existing instanceof ObjectNode objectNode) {
      return objectNode;
    }
    // If field doesn't exist or is not an ObjectNode, create/replace with ObjectNode
    final ObjectNode created = parent.objectNode();
    parent.set(fieldName, created);
    return created;
  }

  /**
   * Finds the nearest parent path from a set of candidate paths by iterating backwards. Returns the
   * longest prefix of the given path that exists in the candidates set.
   *
   * @param path the path to find a parent for
   * @param candidates set of potential parent paths
   * @param separator the path separator
   * @return the nearest parent path, or null if none found
   */
  public static String findParentPath(
      final String path, final java.util.Set<String> candidates, final String separator) {
    int lastSepIndex = path.lastIndexOf(separator);
    while (lastSepIndex > 0) {
      final String prefix = path.substring(0, lastSepIndex);
      if (candidates.contains(prefix)) {
        return prefix;
      }
      lastSepIndex = prefix.lastIndexOf(separator);
    }
    return null;
  }

  /**
   * Checks if a path is under any of the candidate paths.
   *
   * @param path the path to check
   * @param candidates collection of potential parent paths
   * @param separator the path separator
   * @return true if path is under any candidate, false otherwise
   */
  public static boolean isUnderAny(
      final String path, final java.util.Collection<String> candidates, final String separator) {
    for (final String candidate : candidates) {
      if (isUnder(path, candidate, separator)) {
        return true;
      }
    }
    return false;
  }
}