001package com.streamconverter.path;
002
003import java.util.ArrayList;
004import java.util.Arrays;
005import java.util.List;
006
007/**
008 * Represents a hierarchical path for tree-like data structures (JSON, XML).
009 *
010 * <p>This class handles parsing and matching of path expressions in both JSON-style ("$.user.name")
011 * and XML-style ("user/name") formats. It converts path expressions into hierarchical segments for
012 * efficient matching during data processing.
013 */
014public class TreePath implements IPath<List<String>> {
015
016  private final List<String> segments;
017  private final String originalPath;
018
019  // Private constructor for factory methods
020  private TreePath(String originalPath, List<String> segments) {
021    this.originalPath = originalPath;
022    this.segments = segments;
023  }
024
025  /**
026   * Creates a TreePath from an XML path expression.
027   *
028   * @param xmlPath the XML path expression (e.g., "user/name")
029   * @return TreePath instance
030   * @throws IllegalArgumentException if xmlPath is null or invalid
031   */
032  public static TreePath fromXml(String xmlPath) {
033    if (xmlPath == null || xmlPath.trim().isEmpty()) {
034      throw new IllegalArgumentException("XML path cannot be null or empty");
035    }
036    String trimmedPath = xmlPath.trim();
037    List<String> segments = parseXmlPathToSegments(trimmedPath);
038    return new TreePath(trimmedPath, segments);
039  }
040
041  /**
042   * Creates a TreePath from a JSON path expression.
043   *
044   * @param jsonPath the JSON path expression (e.g., "$.user.name")
045   * @return TreePath instance
046   * @throws IllegalArgumentException if jsonPath is null or invalid
047   */
048  public static TreePath fromJson(String jsonPath) {
049    if (jsonPath == null || jsonPath.trim().isEmpty()) {
050      throw new IllegalArgumentException("JSON path cannot be null or empty");
051    }
052    String trimmedPath = jsonPath.trim();
053    List<String> segments = parseJsonPathToSegments(trimmedPath);
054    return new TreePath(trimmedPath, segments);
055  }
056
057  /**
058   * Checks if current path matches the target path segments
059   *
060   * @param currentPath current path segments to match against
061   * @return true if paths match exactly
062   */
063  public boolean matches(List<String> currentPath) {
064    if (currentPath == null) {
065      return false;
066    }
067    return segments.equals(currentPath);
068  }
069
070  /**
071   * Returns the original path expression.
072   *
073   * @return the original path expression
074   */
075  @Override
076  public String toString() {
077    return originalPath;
078  }
079
080  @Override
081  public boolean equals(Object obj) {
082    if (this == obj) return true;
083    if (obj == null || getClass() != obj.getClass()) return false;
084    TreePath treePath = (TreePath) obj;
085    return segments.equals(treePath.segments);
086  }
087
088  @Override
089  public int hashCode() {
090    return segments.hashCode();
091  }
092
093  // === Internal Implementation ===
094
095  private static List<String> parseJsonPathToSegments(String jsonPath) {
096    // Handle root path
097    if ("$".equals(jsonPath)) {
098      return new ArrayList<>();
099    }
100
101    // Handle array syntax preservation for JsonFilterCommand compatibility
102    if (jsonPath.contains("[")) {
103      // For complex paths with arrays, preserve original parsing logic
104      // This ensures JsonFilterCommand continues to work
105      return parseComplexJsonPath(jsonPath);
106    }
107
108    // Simple property paths: $.property or $.nested.property
109    if (jsonPath.startsWith("$.")) {
110      String pathWithoutRoot = jsonPath.substring(2);
111      if (pathWithoutRoot.isEmpty()) {
112        return new ArrayList<>();
113      }
114      return Arrays.asList(pathWithoutRoot.split("\\."));
115    }
116
117    throw new IllegalArgumentException("Invalid JSON path format: " + jsonPath);
118  }
119
120  private static List<String> parseComplexJsonPath(String jsonPath) {
121    // For paths with array syntax like $[*].name or $.users[0].name
122    // Keep them as single segments to maintain compatibility
123    // The actual array handling is done in JsonFilterCommand
124    return List.of(jsonPath);
125  }
126
127  private static List<String> parseXmlPathToSegments(String xmlPath) {
128    // Remove leading and trailing slashes without regex to avoid polynomial complexity
129    String normalizedPath = removeLeadingTrailingSlashes(xmlPath);
130    if (normalizedPath.isEmpty()) {
131      return new ArrayList<>();
132    }
133
134    // Split by single slash and filter empty segments to handle multiple consecutive slashes
135    List<String> segments = new ArrayList<>();
136    int start = 0;
137    for (int i = 0; i <= normalizedPath.length(); i++) {
138      if (i == normalizedPath.length() || normalizedPath.charAt(i) == '/') {
139        if (i > start) {
140          segments.add(normalizedPath.substring(start, i));
141        }
142        start = i + 1;
143      }
144    }
145
146    return segments;
147  }
148
149  /**
150   * Removes leading and trailing slashes from path without regex
151   *
152   * @param path the input path
153   * @return path with leading/trailing slashes removed
154   */
155  private static String removeLeadingTrailingSlashes(String path) {
156    if (path == null || path.isEmpty()) {
157      return "";
158    }
159
160    int start = 0;
161    int end = path.length();
162
163    // Remove leading slashes
164    while (start < end && path.charAt(start) == '/') {
165      start++;
166    }
167
168    // Remove trailing slashes
169    while (end > start && path.charAt(end - 1) == '/') {
170      end--;
171    }
172
173    return path.substring(start, end);
174  }
175}