1 | /* This file is part of the project "Hilbert II" - http://www.qedeq.org |
2 | * |
3 | * Copyright 2000-2014, Michael Meyling <mime@qedeq.org>. |
4 | * |
5 | * "Hilbert II" is free software; you can redistribute |
6 | * it and/or modify it under the terms of the GNU General Public |
7 | * License as published by the Free Software Foundation; either |
8 | * version 2 of the License, or (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | */ |
15 | |
16 | package org.qedeq.kernel.xml.tracker; |
17 | |
18 | import java.util.ArrayList; |
19 | import java.util.List; |
20 | import java.util.StringTokenizer; |
21 | |
22 | import org.qedeq.base.utility.EqualsUtility; |
23 | |
24 | |
25 | /** |
26 | * Simple XPath like description of a location in an XML file. |
27 | * |
28 | * @author Michael Meyling |
29 | */ |
30 | public class SimpleXPath { |
31 | |
32 | /** List with element names. */ |
33 | private final List elements; |
34 | |
35 | /** List with element occurrence numbers. */ |
36 | private final List numbers; |
37 | |
38 | /** Attribute of element. */ |
39 | private String attribute; |
40 | |
41 | /** |
42 | * Constructor with simple XPath string as parameter. |
43 | * This is not the standard XPath definition but it is similar to a subset of |
44 | * the abbreviation XPath notation. |
45 | * <p> |
46 | * <code>/element1/element2[3]@attribute</code> is an example for such |
47 | * a notation. This selects from the first occurrence of <code>element1</code> |
48 | * and from the third occurrence of subnode <code>element2</code> the attribute |
49 | * <code>attribute</code>. The attribute is optional. It is always exactly one node or |
50 | * the attribute of one node specified. |
51 | * <p> |
52 | * The general syntax could be described as follows: |
53 | * {"/"<em>element</em>"["<em>index</em>"]"}+ |
54 | * ["@"<em>attribute</em>] |
55 | * |
56 | * |
57 | * @param xpath String with the syntax as described above. If the syntax is violated |
58 | * RuntimeExceptions may occur. |
59 | */ |
60 | public SimpleXPath(final String xpath) { |
61 | elements = new ArrayList(); |
62 | numbers = new ArrayList(); |
63 | attribute = null; |
64 | init(xpath); |
65 | } |
66 | |
67 | /** |
68 | * Empty constructor. |
69 | */ |
70 | public SimpleXPath() { |
71 | elements = new ArrayList(); |
72 | numbers = new ArrayList(); |
73 | attribute = null; |
74 | } |
75 | |
76 | /** |
77 | * Copy constructor. |
78 | * |
79 | * @param original XPath to copy. |
80 | */ |
81 | public SimpleXPath(final SimpleXPath original) { |
82 | elements = new ArrayList(); |
83 | numbers = new ArrayList(); |
84 | attribute = null; |
85 | init(original.toString()); |
86 | } |
87 | |
88 | /** |
89 | * Initialize all object attributes according to XPath parameter. |
90 | * |
91 | * @see SimpleXPath#SimpleXPath(String) |
92 | * |
93 | * @param xpath String with the syntax as described above. If the syntax is violated |
94 | * RuntimeExceptions may occur. |
95 | */ |
96 | private void init(final String xpath) { |
97 | if (xpath == null) { |
98 | throw new NullPointerException(); |
99 | } |
100 | if (xpath.length() <= 0) { |
101 | throw new RuntimeException("XPath must not be empty"); |
102 | } |
103 | if (xpath.charAt(0) != '/') { |
104 | throw new RuntimeException("XPath must start with '/': " + xpath); |
105 | } |
106 | if (xpath.indexOf("//") >= 0) { |
107 | throw new RuntimeException("empty tag not permitted: " + xpath); |
108 | } |
109 | if (xpath.endsWith("/")) { |
110 | throw new RuntimeException("XPath must not end with '/': " + xpath); |
111 | } |
112 | final StringTokenizer tokenizer = new StringTokenizer(xpath, "/"); |
113 | while (tokenizer.hasMoreTokens()) { |
114 | String token = tokenizer.nextToken(); |
115 | if (!tokenizer.hasMoreTokens() && token.indexOf('@') >= 0) { |
116 | attribute = token.substring(token.indexOf('@') + 1); |
117 | if (attribute.length() <= 0) { |
118 | throw new RuntimeException("empty attribute not permitted: " + xpath); |
119 | } |
120 | token = token.substring(0, token.indexOf('@')); |
121 | } |
122 | if (token.indexOf('[') < 0) { |
123 | elements.add(token); |
124 | numbers.add(new Integer(1)); |
125 | } else { |
126 | final StringTokenizer getnu = new StringTokenizer(token, "[]"); |
127 | elements.add(getnu.nextToken()); |
128 | final Integer i; |
129 | try { |
130 | i = new Integer(getnu.nextToken()); |
131 | } catch (RuntimeException e) { |
132 | throw new RuntimeException("not an integer: " + xpath, e); |
133 | } |
134 | if (i.intValue() <= 0) { |
135 | throw new RuntimeException("integer must be greater zero: " + xpath); |
136 | } |
137 | numbers.add(i); |
138 | } |
139 | } |
140 | } |
141 | |
142 | /** |
143 | * Get number of collected exceptions. |
144 | * |
145 | * @return Number of collected exceptions. |
146 | */ |
147 | public final int size() { |
148 | return elements.size(); |
149 | } |
150 | |
151 | /** |
152 | * Get <code>i</code>-th Element name. |
153 | * |
154 | * @param i Starts with 0 and must be smaller than {@link #size()}. |
155 | * @return Wanted element name. |
156 | */ |
157 | public final String getElementName(final int i) { |
158 | return (String) elements.get(i); |
159 | } |
160 | |
161 | /** |
162 | * Get <code>i</code>-th occurrence number. |
163 | * |
164 | * @param i Starts with 0 and must be smaller than {@link #size()}. |
165 | * @return Wanted element occurrence number. |
166 | */ |
167 | public final int getElementOccurrence(final int i) { |
168 | return ((Integer) numbers.get(i)).intValue(); |
169 | } |
170 | |
171 | /** |
172 | * Add new element to end of XPath. |
173 | * |
174 | * @param elementName element to add. |
175 | */ |
176 | public final void addElement(final String elementName) { |
177 | attribute = null; |
178 | elements.add(elementName); |
179 | numbers.add(new Integer(1)); |
180 | } |
181 | |
182 | /** |
183 | * Add new element to end of XPath. |
184 | * |
185 | * @param elementName element to add. |
186 | * @param occurrence Occurrence number of element. Starts with 1. |
187 | */ |
188 | public final void addElement(final String elementName, final int occurrence) { |
189 | attribute = null; |
190 | elements.add(elementName); |
191 | numbers.add(new Integer(occurrence)); |
192 | } |
193 | |
194 | /** |
195 | * Get last XPath element name. |
196 | * |
197 | * @return Last element name. Could be <code>null</code> if no elements exist. |
198 | */ |
199 | public final String getLastElement() { |
200 | int size = elements.size(); |
201 | if (size <= 0) { |
202 | return null; |
203 | } |
204 | return (String) elements.get(size - 1); |
205 | } |
206 | |
207 | /** |
208 | * Get XPath element name before last. |
209 | * |
210 | * @return Before last element name. Could be <code>null</code> if no more than one element |
211 | * exist. |
212 | */ |
213 | public final String getBeforeLastElement() { |
214 | int size = elements.size(); |
215 | if (size <= 1) { |
216 | return null; |
217 | } |
218 | return (String) elements.get(size - 2); |
219 | } |
220 | |
221 | /** |
222 | * Delete last XPath element if any. |
223 | */ |
224 | public void deleteLastElement() { |
225 | int size = elements.size(); |
226 | if (size > 0) { |
227 | elements.remove(size - 1); |
228 | numbers.remove(size - 1); |
229 | attribute = null; |
230 | } |
231 | } |
232 | |
233 | /** |
234 | * Set attribute. |
235 | * |
236 | * @param attribute Attribute, maybe <code>null</code>. |
237 | */ |
238 | public final void setAttribute(final String attribute) { |
239 | this.attribute = attribute; |
240 | } |
241 | |
242 | /** |
243 | * Get attribute. |
244 | * |
245 | * @return Attribute, maybe <code>null</code>. |
246 | */ |
247 | public final String getAttribute() { |
248 | return attribute; |
249 | } |
250 | |
251 | |
252 | public final boolean equals(final Object obj) { |
253 | if (!(obj instanceof SimpleXPath)) { |
254 | return false; |
255 | } |
256 | final SimpleXPath other = (SimpleXPath) obj; |
257 | if (!EqualsUtility.equals(this.getAttribute(), other.getAttribute())) { |
258 | return false; |
259 | } |
260 | return equalsElements(other); |
261 | } |
262 | |
263 | /** |
264 | * Are the elements and occurrences of this and another element equal? No special treatment |
265 | * of "*" elements. |
266 | * |
267 | * @param other Compare with this object. |
268 | * @return Are the elements of this and the parameter object equal? |
269 | */ |
270 | public final boolean equalsElements(final SimpleXPath other) { |
271 | final int size = this.size(); |
272 | if (size != other.size()) { |
273 | return false; |
274 | } |
275 | |
276 | for (int i = 0; i < size; i++) { |
277 | if (!EqualsUtility.equals(this.getElementName(i), other.getElementName(i))) { |
278 | return false; |
279 | } |
280 | if (getElementOccurrence(i) != other.getElementOccurrence(i)) { |
281 | return false; |
282 | } |
283 | } |
284 | return true; |
285 | } |
286 | |
287 | /** |
288 | * Match the elements and occurrences of this finder object and current elements? |
289 | * This object may contain "*" elements. |
290 | * |
291 | * @param current Compare with this current elements. These elements should not |
292 | * contain "*" elements. |
293 | * @param currentSummary Contains only "*" elements. This parameter must be identify the same |
294 | * XPath as <code>current</code> |
295 | * @return Match the elements of this finder object and the parameter objects? |
296 | */ |
297 | public final boolean matchesElements(final SimpleXPath current, |
298 | final SimpleXPath currentSummary) { |
299 | final int size = current.size(); |
300 | if (size != size()) { |
301 | return false; |
302 | } |
303 | if (size != currentSummary.size()) { |
304 | throw new IllegalArgumentException("summary size doesn't match"); |
305 | } |
306 | |
307 | for (int i = 0; i < size; i++) { |
308 | if ("*".equals(getElementName(i))) { |
309 | if (getElementOccurrence(i) != currentSummary.getElementOccurrence(i)) { |
310 | return false; |
311 | } |
312 | continue; |
313 | } |
314 | if (!EqualsUtility.equals(current.getElementName(i), getElementName(i))) { |
315 | return false; |
316 | } |
317 | if (current.getElementOccurrence(i) != getElementOccurrence(i)) { |
318 | return false; |
319 | } |
320 | } |
321 | return true; |
322 | } |
323 | |
324 | /** |
325 | * Match the elements and occurrences of this finder object and current elements? |
326 | * This object may contain "*" elements. Checks only to current.size(). |
327 | * |
328 | * @param current Compare with this current elements. These elements should not |
329 | * contain "*" elements. |
330 | * @param currentSummary Contains only "*" elements. This parameter must be identify the same |
331 | * XPath as <code>current</code> |
332 | * @return Match the elements of this finder object and the parameter objects? |
333 | */ |
334 | public final boolean matchesElementsBegining(final SimpleXPath current, |
335 | final SimpleXPath currentSummary) { |
336 | final int size = current.size(); |
337 | if (size > size()) { |
338 | return false; |
339 | } |
340 | if (size != currentSummary.size()) { |
341 | throw new IllegalArgumentException("summary size doesn't match"); |
342 | } |
343 | |
344 | for (int i = 0; i < size; i++) { |
345 | if ("*".equals(getElementName(i))) { |
346 | if (getElementOccurrence(i) != currentSummary.getElementOccurrence(i)) { |
347 | return false; |
348 | } |
349 | continue; |
350 | } |
351 | if (!EqualsUtility.equals(current.getElementName(i), getElementName(i))) { |
352 | return false; |
353 | } |
354 | if (current.getElementOccurrence(i) != getElementOccurrence(i)) { |
355 | return false; |
356 | } |
357 | } |
358 | return true; |
359 | } |
360 | |
361 | public final String toString() { |
362 | final StringBuffer buffer = new StringBuffer(); |
363 | for (int i = 0; i < size(); i++) { |
364 | buffer.append("/"); |
365 | buffer.append(getElementName(i)); |
366 | if (getElementOccurrence(i) != 1) { |
367 | buffer.append("["); |
368 | buffer.append(getElementOccurrence(i)); |
369 | buffer.append("]"); |
370 | } |
371 | } |
372 | if (getAttribute() != null) { |
373 | buffer.append("@"); |
374 | buffer.append(getAttribute()); |
375 | } |
376 | return buffer.toString(); |
377 | } |
378 | |
379 | public final int hashCode() { |
380 | int code = 0; |
381 | if (attribute != null) { |
382 | code ^= attribute.hashCode(); |
383 | } |
384 | for (int i = 0; i < size(); i++) { |
385 | code ^= i + 1; |
386 | code ^= getElementName(i).hashCode(); |
387 | code ^= getElementOccurrence(i); |
388 | } |
389 | return code; |
390 | } |
391 | |
392 | } |