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}