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   * Checks if current path matches after stripping array-index syntax from this path's segments.
072   *
073   * <p>A segment like {@code "orders[*]"} or {@code "orders[0]"} is normalized to {@code "orders"}
074   * before comparison. This allows paths such as {@code "$.orders[*].product_code"} to match the
075   * streaming {@code currentPath} list {@code ["orders", "product_code"]}.
076   *
077   * @param currentPath current path segments built by the streaming traversal
078   * @return true if the normalized segments equal {@code currentPath}
079   */
080  public boolean matchesIgnoringArraySyntax(List<String> currentPath) {
081    if (currentPath == null) {
082      return false;
083    }
084    List<String> normalized = new ArrayList<>();
085    for (String segment : segments) {
086      // Strip leading $. prefix if present (handles complex path single-segment case)
087      String s = segment;
088      if (s.startsWith("$.")) {
089        s = s.substring(2);
090      } else if (s.startsWith("$")) {
091        s = s.substring(1);
092      }
093      // Split on dots to expand compound segments like "orders[*].product_code"
094      for (String part : s.split("\\.")) {
095        // Remove array index notation: [*], [0], [1], etc.
096        String stripped = part.replaceAll("\\[.*?\\]", "");
097        if (!stripped.isEmpty()) {
098          normalized.add(stripped);
099        }
100      }
101    }
102    return normalized.equals(currentPath);
103  }
104
105  /**
106   * Returns the original path expression.
107   *
108   * @return the original path expression
109   */
110  @Override
111  public String toString() {
112    return originalPath;
113  }
114
115  /**
116   * 2つのTreePathが等しいかどうかをセグメントリストで比較する。
117   *
118   * <p>注意: 元のパス文字列({@link #toString()} の値)が異なっていても、セグメントに展開した結果が同じであれば等しいとみなす。 例えば JSONスタイルの {@code
119   * "$.user.name"} と XMLスタイルの {@code "user/name"} がパース後に同じセグメントになる場合、 等しいと判定される。
120   *
121   * @param obj 比較対象のオブジェクト
122   * @return セグメントリストが等しい場合true
123   */
124  @Override
125  public boolean equals(Object obj) {
126    if (this == obj) return true;
127    if (obj == null || getClass() != obj.getClass()) return false;
128    TreePath treePath = (TreePath) obj;
129    return segments.equals(treePath.segments);
130  }
131
132  /**
133   * セグメントリストに基づくハッシュコードを返す。
134   *
135   * @return ハッシュコード
136   */
137  @Override
138  public int hashCode() {
139    return segments.hashCode();
140  }
141
142  // === Internal Implementation ===
143
144  private static List<String> parseJsonPathToSegments(String jsonPath) {
145    // Handle root path
146    if ("$".equals(jsonPath)) {
147      return new ArrayList<>();
148    }
149
150    // Handle array syntax preservation for JsonFilterCommand compatibility
151    if (jsonPath.contains("[")) {
152      // For complex paths with arrays, preserve original parsing logic
153      // This ensures JsonFilterCommand continues to work
154      return parseComplexJsonPath(jsonPath);
155    }
156
157    // Simple property paths: $.property or $.nested.property
158    if (jsonPath.startsWith("$.")) {
159      String pathWithoutRoot = jsonPath.substring(2);
160      if (pathWithoutRoot.isEmpty()) {
161        return new ArrayList<>();
162      }
163      return Arrays.asList(pathWithoutRoot.split("\\."));
164    }
165
166    throw new IllegalArgumentException("Invalid JSON path format: " + jsonPath);
167  }
168
169  private static List<String> parseComplexJsonPath(String jsonPath) {
170    // For paths with array syntax like $[*].name or $.users[0].name
171    // Keep them as single segments to maintain compatibility
172    // The actual array handling is done in JsonFilterCommand
173    return List.of(jsonPath);
174  }
175
176  private static List<String> parseXmlPathToSegments(String xmlPath) {
177    // Remove leading and trailing slashes without regex to avoid polynomial complexity
178    String normalizedPath = removeLeadingTrailingSlashes(xmlPath);
179    if (normalizedPath.isEmpty()) {
180      return new ArrayList<>();
181    }
182
183    // Split by single slash and filter empty segments to handle multiple consecutive slashes
184    List<String> segments = new ArrayList<>();
185    int start = 0;
186    for (int i = 0; i <= normalizedPath.length(); i++) {
187      if (i == normalizedPath.length() || normalizedPath.charAt(i) == '/') {
188        if (i > start) {
189          segments.add(normalizedPath.substring(start, i));
190        }
191        start = i + 1;
192      }
193    }
194
195    return segments;
196  }
197
198  /**
199   * Removes leading and trailing slashes from path without regex
200   *
201   * @param path the input path
202   * @return path with leading/trailing slashes removed
203   */
204  private static String removeLeadingTrailingSlashes(String path) {
205    if (path == null || path.isEmpty()) {
206      return "";
207    }
208
209    int start = 0;
210    int end = path.length();
211
212    // Remove leading slashes
213    while (start < end && path.charAt(start) == '/') {
214      start++;
215    }
216
217    // Remove trailing slashes
218    while (end > start && path.charAt(end - 1) == '/') {
219      end--;
220    }
221
222    return path.substring(start, end);
223  }
224}