1 /*
2  * Copyright (C) 2004, 2005 Joe Walnes.
3  * Copyright (C) 2006, 2007, 2009, 2013 XStream Committers.
4  * All rights reserved.
5  *
6  * The software in this package is published under the terms of the BSD
7  * style license a copy of which has been included with this distribution in
8  * the LICENSE.txt file.
9  * 
10  * Created on 02. September 2004 by Joe Walnes
11  */

12 package com.thoughtworks.xstream.io.path;
13
14 import com.thoughtworks.xstream.core.util.FastStack;
15
16 import java.util.ArrayList;
17 import java.util.List;
18
19 /**
20  * Represents a path to a single node in the tree.
21  *
22  * <p>Two absolute paths can also be compared to calculate the relative path between them.
23  * A relative path can be applied to an absolute path to calculate another absolute path.</p>
24  * 
25  * <p>Note that the paths are normally XPath compliant, so can be read by other XPath engines.
26  * However, {@link #toString()} will select a node list while {@link #explicit()} will always select 
27  * an individual node. If the return type of the XPath evaluation is a node, the result will be the same,
28  * because XPath will then use the first element of the list. The following are examples of path 
29  * expressions that the Path object supports:</p>
30  *
31  * <p>Note that the implementation does not take care if the paths are XPath compliant, it simply
32  * manages the values between the path separator. However, it normalizes the path if a path element
33  * ends with a selector for the first element (i.e. "[1]"). Those will be handled transparent i.e. two Paths
34  * are treated equal if one was created with path elements containing this selector and the other one 
35  * without.</p>
36  * 
37  * <p>The following are examples of path expressions that the Path object supports:</p>
38  * <ul>
39  *     <li>/</li>
40  *     <li>/some/node</li>
41  *     <li>/a/b/c/b/a</li>
42  *     <li>/a/b[1]/c[1]/b[1]/a[1]</li>
43  *     <li>/some[3]/node[2]/a</li>
44  *     <li>../../../another[3]/node</li>
45  * </ul>
46  *
47  * <h3>Example</h3>
48  *
49  * <pre>
50  * Path a = new Path("/html/body/div[1]/table[2]/tr[3]/td/div");
51  * Path b = new Path("/html/body/div/table[2]/tr[6]/td/form");
52  *
53  * Path relativePath = a.relativeTo(b); // produces: "../../../tr[6]/td/form"
54  * Path c = a.apply(relativePath); // same as Path b.
55  * </pre>
56  *
57  * @see PathTracker
58  *
59  * @author Joe Walnes
60  */

61 public class Path {
62
63     private final String[] chunks;
64     private transient String pathAsString;
65     private transient String pathExplicit;
66     private static final Path DOT = new Path(new String[] {"."});
67
68     public Path(String pathAsString) {
69         // String.split() too slow. StringTokenizer too crappy.
70         List result = new ArrayList();
71         int currentIndex = 0;
72         int nextSeparator;
73         this.pathAsString = pathAsString;
74         while ((nextSeparator = pathAsString.indexOf('/', currentIndex)) != -1) {
75             // normalize explicit paths
76             result.add(normalize(pathAsString, currentIndex, nextSeparator));
77             currentIndex = nextSeparator + 1;
78         }
79         result.add(normalize(pathAsString,currentIndex,pathAsString.length()));
80         String[] arr = new String[result.size()];
81         result.toArray(arr);
82         chunks = arr;
83     }
84     
85     private String normalize(String s, int start, int end) {
86         if (end - start > 3 
87                 && s.charAt(end-3) == '['
88                 && s.charAt(end-2) == '1'
89                 && s.charAt(end-1) == ']') {
90             this.pathAsString = null;
91             return s.substring(start, end-3);
92         } else {
93             return s.substring(start, end);
94         }
95         
96     }
97
98     public Path(String[] chunks) {
99         this.chunks = chunks;
100     }
101
102     public String toString() {
103         if (pathAsString == null) {
104             StringBuffer buffer = new StringBuffer();
105             for (int i = 0; i < chunks.length; i++) {
106                 if (i > 0) buffer.append('/');
107                 buffer.append(chunks[i]);
108             }
109             pathAsString = buffer.toString();
110         }
111         return pathAsString;
112     }
113
114     public String explicit() {
115         if (pathExplicit == null) {
116             StringBuffer buffer = new StringBuffer();
117             for (int i = 0; i < chunks.length; i++) {
118                 if (i > 0) buffer.append('/');
119                 String chunk = chunks[i];
120                 buffer.append(chunk);
121                 int length = chunk.length();
122                 if (length > 0) {
123                     char c = chunk.charAt(length-1);
124                     if (c != ']' && c != '.') {
125                         buffer.append("[1]");
126                     }
127                 }
128             }
129             pathExplicit = buffer.toString();
130         }
131         return pathExplicit;
132     }
133
134     public boolean equals(Object o) {
135         if (this == o) return true;
136         if (!(o instanceof Path)) return false;
137
138         final Path other = (Path) o;
139         if (chunks.length != other.chunks.length) return false;
140         for (int i = 0; i < chunks.length; i++) {
141             if (!chunks[i].equals(other.chunks[i])) return false;
142         }
143
144         return true;
145     }
146
147     public int hashCode() {
148         int result = 543645643;
149         for (int i = 0; i < chunks.length; i++) {
150             result = 29 * result + chunks[i].hashCode();
151         }
152         return result;
153     }
154
155     public Path relativeTo(Path that) {
156         int depthOfPathDivergence = depthOfPathDivergence(chunks, that.chunks);
157         String[] result = new String[chunks.length + that.chunks.length - 2 * depthOfPathDivergence];
158         int count = 0;
159
160         for (int i = depthOfPathDivergence; i < chunks.length; i++) {
161             result[count++] = "..";
162         }
163         for (int j = depthOfPathDivergence; j < that.chunks.length; j++) {
164             result[count++] = that.chunks[j];
165         }
166
167         if (count == 0) {
168             return DOT;
169         } else {
170             return new Path(result);
171         }
172     }
173
174     private int depthOfPathDivergence(String[] path1, String[] path2) {
175         int minLength = Math.min(path1.length, path2.length);
176         for (int i = 0; i < minLength; i++) {
177             if (!path1[i].equals(path2[i])) {
178                 return i;
179             }
180         }
181         return minLength;
182     }
183
184     public Path apply(Path relativePath) {
185         FastStack absoluteStack = new FastStack(16);
186
187         for (int i = 0; i < chunks.length; i++) {
188             absoluteStack.push(chunks[i]);
189         }
190
191         for (int i = 0; i < relativePath.chunks.length; i++) {
192             String relativeChunk = relativePath.chunks[i];
193             if (relativeChunk.equals("..")) {
194                 absoluteStack.pop();
195             } else if (!relativeChunk.equals(".")) {
196                 absoluteStack.push(relativeChunk);
197             }
198         }
199
200         String[] result = new String[absoluteStack.size()];
201         for (int i = 0; i < result.length; i++) {
202             result[i] = (String) absoluteStack.get(i);
203         }
204
205         return new Path(result);
206     }
207     
208     public boolean isAncestor(Path child) {
209         if (child == null || child.chunks.length < chunks.length) {
210             return false;
211         }
212         for (int i = 0; i < chunks.length; i++) {
213             if (!chunks[i].equals(child.chunks[i])) {
214                 return false;
215             }
216         }
217         return true;
218     }
219 }
220