001: /*
002: ******************************************************************
003: Copyright (c) 2001-2007, Jeff Martin, Tim Bacon
004: All rights reserved.
005:
006: Redistribution and use in source and binary forms, with or without
007: modification, are permitted provided that the following conditions
008: are met:
009:
010: * Redistributions of source code must retain the above copyright
011: notice, this list of conditions and the following disclaimer.
012: * Redistributions in binary form must reproduce the above
013: copyright notice, this list of conditions and the following
014: disclaimer in the documentation and/or other materials provided
015: with the distribution.
016: * Neither the name of the xmlunit.sourceforge.net nor the names
017: of its contributors may be used to endorse or promote products
018: derived from this software without specific prior written
019: permission.
020:
021: THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
022: "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
023: LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
024: FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
025: COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
026: INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
027: BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
028: LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
029: CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
030: LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
031: ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
032: POSSIBILITY OF SUCH DAMAGE.
033:
034: ******************************************************************
035: */
036: package org.custommonkey.xmlunit;
037:
038: import java.util.ArrayList;
039: import java.util.HashMap;
040: import java.util.Iterator;
041: import java.util.List;
042: import java.util.Map;
043:
044: import org.w3c.dom.Attr;
045: import org.w3c.dom.Element;
046: import org.w3c.dom.Node;
047: import org.w3c.dom.NodeList;
048:
049: /**
050: * Tracks Nodes visited by the DifferenceEngine and
051: * converts that information into an Xpath-String to supply
052: * to the NodeDetail of a Difference instance
053: * @see NodeDetail#getXpathLocation()
054: * @see Difference#getControlNodeDetail
055: * @see Difference#getTestNodeDetail
056: */
057: public class XpathNodeTracker implements XMLConstants {
058: private final List indentationList = new ArrayList();
059: private TrackingEntry currentEntry;
060:
061: /**
062: * Simple constructor
063: */
064: public XpathNodeTracker() {
065: newLevel();
066: }
067:
068: /**
069: * Clear state data.
070: * Call if required to reuse an existing instance.
071: */
072: public void reset() {
073: indentationList.clear();
074: indent();
075: }
076:
077: /**
078: * Call before examining child nodes one level of indentation into DOM
079: */
080: public void indent() {
081: if (currentEntry != null) {
082: currentEntry.clearTrackedAttribute();
083: }
084: newLevel();
085: }
086:
087: private void newLevel() {
088: currentEntry = new TrackingEntry();
089: indentationList.add(currentEntry);
090: }
091:
092: /**
093: * Call after processing attributes of an element and turining to
094: * compare the child nodes.
095: */
096: public void clearTrackedAttribute() {
097: if (currentEntry != null) {
098: currentEntry.clearTrackedAttribute();
099: }
100: }
101:
102: /**
103: * Call after examining child nodes, ie before returning back one level of indentation from DOM
104: */
105: public void outdent() {
106: int last = indentationList.size() - 1;
107: indentationList.remove(last);
108: --last;
109: if (last >= 0) {
110: currentEntry = (TrackingEntry) indentationList.get(last);
111: }
112: }
113:
114: /**
115: * Call when visiting a node whose xpath location needs tracking
116: * @param node the Node being visited
117: */
118: public void visited(Node node) {
119: String nodeName;
120: switch (node.getNodeType()) {
121: case Node.ATTRIBUTE_NODE:
122: nodeName = ((Attr) node).getLocalName();
123: if (nodeName == null || nodeName.length() == 0) {
124: nodeName = node.getNodeName();
125: }
126: visitedAttribute(nodeName);
127: break;
128: case Node.ELEMENT_NODE:
129: nodeName = ((Element) node).getLocalName();
130: if (nodeName == null || nodeName.length() == 0) {
131: nodeName = node.getNodeName();
132: }
133: visitedNode(node, nodeName);
134: break;
135: case Node.COMMENT_NODE:
136: visitedNode(node, XPATH_COMMENT_IDENTIFIER);
137: break;
138: case Node.PROCESSING_INSTRUCTION_NODE:
139: visitedNode(node, XPATH_PROCESSING_INSTRUCTION_IDENTIFIER);
140: break;
141: case Node.CDATA_SECTION_NODE:
142: case Node.TEXT_NODE:
143: visitedNode(node, XPATH_CHARACTER_NODE_IDENTIFIER);
144: break;
145: default:
146: // ignore unhandled node types
147: break;
148: }
149: }
150:
151: protected void visitedNode(Node visited, String value) {
152: currentEntry.trackNode(visited, value);
153: }
154:
155: protected void visitedAttribute(String visited) {
156: currentEntry.trackAttribute(visited);
157: }
158:
159: /**
160: * Preload the items in a NodeList by visiting each in turn
161: * Required for pieces of test XML whose node children can be visited
162: * out of sequence by a DifferenceEngine comparison
163: * @param nodeList the items to preload
164: */
165: public void preloadNodeList(NodeList nodeList) {
166: currentEntry.trackNodesAsWellAsValues(true);
167: int length = nodeList.getLength();
168: for (int i = 0; i < length; ++i) {
169: visited(nodeList.item(i));
170: }
171: currentEntry.trackNodesAsWellAsValues(false);
172: }
173:
174: /**
175: * Preload the items in a List by visiting each in turn
176: * Required for pieces of test XML whose node children can be visited
177: * out of sequence by a DifferenceEngine comparison
178: * @param nodeList the items to preload
179: */
180: public void preloadChildList(List nodeList) {
181: currentEntry.trackNodesAsWellAsValues(true);
182: int length = nodeList.size();
183: for (int i = 0; i < length; ++i) {
184: visited((Node) nodeList.get(i));
185: }
186: currentEntry.trackNodesAsWellAsValues(false);
187: }
188:
189: /**
190: * @return the last visited node as an xpath-location String
191: */
192: public String toXpathString() {
193: StringBuffer buf = new StringBuffer();
194: TrackingEntry nextEntry;
195: for (Iterator iter = indentationList.iterator(); iter.hasNext();) {
196: nextEntry = (TrackingEntry) iter.next();
197: nextEntry.appendEntryTo(buf);
198: }
199: return buf.toString();
200: }
201:
202: /**
203: * Wrapper class around a mutable <code>int</code> value
204: * Avoids creation of many immutable <code>Integer</code> objects
205: */
206: private static final class Int {
207: private int value;
208:
209: public Int(int startAt) {
210: value = startAt;
211: }
212:
213: public void increment() {
214: ++value;
215: }
216:
217: public int getValue() {
218: return value;
219: }
220:
221: public Integer toInteger() {
222: return new Integer(value);
223: }
224: }
225:
226: /**
227: * Holds node tracking details - one instance is used for each level of indentation in a DOM
228: * Provides reference between a String-ified Node value and the xpath index of that value
229: */
230: private static final class TrackingEntry {
231: private final Map valueMap = new HashMap();
232: private String currentValue, currentAttribute;
233:
234: private Map nodeReferenceMap;
235: private boolean trackNodeReferences = false;
236: private Integer nodeReferenceLookup = null;
237:
238: private Int lookup(String value) {
239: return (Int) valueMap.get(value);
240: }
241:
242: /**
243: * Keep a reference to the current visited (non-attribute) node
244: * @param visited the non-attribute node visited
245: * @param value the String-ified value of the non-attribute node visited
246: */
247: public void trackNode(Node visited, String value) {
248: if (nodeReferenceMap == null || trackNodeReferences) {
249: Int occurrence = lookup(value);
250: if (occurrence == null) {
251: occurrence = new Int(1);
252: valueMap.put(value, occurrence);
253: } else {
254: occurrence.increment();
255: }
256: if (trackNodeReferences) {
257: nodeReferenceMap.put(visited, occurrence
258: .toInteger());
259: }
260: } else {
261: nodeReferenceLookup = (Integer) nodeReferenceMap
262: .get(visited);
263: }
264: currentValue = value;
265: clearTrackedAttribute();
266: }
267:
268: /**
269: * Keep a reference to the visited attribute at the current visited node
270: * @param value the attribute visited
271: */
272: public void trackAttribute(String visited) {
273: currentAttribute = visited;
274: }
275:
276: /**
277: * Clear any reference to the current visited attribute
278: */
279: public void clearTrackedAttribute() {
280: currentAttribute = null;
281: }
282:
283: /**
284: * Append the details of the current visited node to a StringBuffer
285: * @param buf the StringBuffer to append to
286: */
287: public void appendEntryTo(StringBuffer buf) {
288: if (currentValue == null) {
289: return;
290: }
291: buf.append(XPATH_SEPARATOR).append(currentValue);
292:
293: int value = nodeReferenceLookup == null ? lookup(
294: currentValue).getValue() : nodeReferenceLookup
295: .intValue();
296: buf.append(XPATH_NODE_INDEX_START).append(value).append(
297: XPATH_NODE_INDEX_END);
298:
299: if (currentAttribute != null) {
300: buf.append(XPATH_SEPARATOR).append(
301: XPATH_ATTRIBUTE_IDENTIFIER).append(
302: currentAttribute);
303: }
304: }
305:
306: public void trackNodesAsWellAsValues(boolean yesNo) {
307: this .trackNodeReferences = yesNo;
308: if (yesNo) {
309: nodeReferenceMap = new HashMap();
310: }
311: }
312: }
313: }
|