001: package net.sf.saxon.functions;
002:
003: import net.sf.saxon.Configuration;
004: import net.sf.saxon.expr.Expression;
005: import net.sf.saxon.expr.StaticContext;
006: import net.sf.saxon.expr.XPathContext;
007: import net.sf.saxon.om.*;
008: import net.sf.saxon.pattern.NameTest;
009: import net.sf.saxon.sort.AtomicComparer;
010: import net.sf.saxon.sort.IntHashSet;
011: import net.sf.saxon.trans.XPathException;
012: import net.sf.saxon.trans.DynamicError;
013: import net.sf.saxon.type.ComplexType;
014: import net.sf.saxon.type.SchemaType;
015: import net.sf.saxon.type.Type;
016: import net.sf.saxon.value.BooleanValue;
017: import net.sf.saxon.value.Whitespace;
018:
019: import javax.xml.transform.TransformerException;
020:
021: /**
022: * XSLT 2.0 deep-equal() function.
023: * Supports deep comparison of two sequences (of nodes and/or atomic values)
024: * optionally using a collation
025: */
026:
027: public class DeepEqual extends CollatingFunction {
028:
029: /**
030: * Flag indicating that two elements should only be considered equal if they have the same
031: * in-scope namespaces
032: */
033: public static final int INCLUDE_NAMESPACES = 1 << 0;
034:
035: /**
036: * Flag indicating that comment children are taken into account when comparing element or document nodes
037: */
038: public static final int INCLUDE_COMMENTS = 1 << 2;
039:
040: /**
041: * Flag indicating that processing instruction nodes are taken into account when comparing element or document nodes
042: */
043: public static final int INCLUDE_PROCESSING_INSTRUCTIONS = 1 << 3;
044:
045: /**
046: * Flag indicating that whitespace text nodes are ignored when comparing element nodes
047: */
048: public static final int EXCLUDE_WHITESPACE_TEXT_NODES = 1 << 4;
049:
050: /**
051: * Flag indicating that elements and attributes should always be compared according to their string
052: * value, not their typed value
053: */
054: public static final int COMPARE_STRING_VALUES = 1 << 5;
055:
056: /**
057: * Flag indicating that elements and attributes must have the same type annotation to be considered
058: * deep-equal
059: */
060: public static final int COMPARE_ANNOTATIONS = 1 << 6;
061:
062: /**
063: * Flag indicating that a warning message explaining the reason why the sequences were deemed non-equal
064: * should be sent to the ErrorListener
065: */
066: public static final int WARNING_IF_FALSE = 1 << 7;
067:
068: private transient Configuration config = null;
069:
070: /**
071: * preEvaluate: if all arguments are known statically, evaluate early
072: */
073:
074: public Expression preEvaluate(StaticContext env)
075: throws XPathException {
076: config = env.getConfiguration();
077: return super .preEvaluate(env);
078: }
079:
080: /**
081: * Evaluate the expression
082: */
083:
084: public Item evaluateItem(XPathContext context)
085: throws XPathException {
086: AtomicComparer collator = getAtomicComparer(2, context);
087:
088: SequenceIterator op1 = argument[0].iterate(context);
089: SequenceIterator op2 = argument[1].iterate(context);
090:
091: Configuration config = (this .config != null ? this .config
092: : context.getConfiguration());
093: return BooleanValue.get(deepEquals(op1, op2, collator, config,
094: 0));
095: }
096:
097: /**
098: * Determine when two sequences are deep-equal
099: * @param op1 the first sequence
100: * @param op2 the second sequence
101: * @param collator the collator to be used
102: * @param config the configuration (gives access to the NamePool)
103: * @param flags bit-significant integer giving comparison options. Always zero for standard
104: * F+O deep-equals comparison.
105: * @return true if the sequences are deep-equal
106: */
107:
108: public static boolean deepEquals(SequenceIterator op1,
109: SequenceIterator op2, AtomicComparer collator,
110: Configuration config, int flags) {
111: boolean result = true;
112: String reason = null;
113: try {
114: while (true) {
115: Item item1 = op1.next();
116: Item item2 = op2.next();
117:
118: if (item1 == null && item2 == null) {
119: break;
120: }
121:
122: if (item1 == null || item2 == null) {
123: result = false;
124: reason = "sequences have different lengths";
125: break;
126: }
127:
128: if (item1 instanceof NodeInfo) {
129: if (item2 instanceof NodeInfo) {
130: if (!deepEquals((NodeInfo) item1,
131: (NodeInfo) item2, collator, config,
132: flags)) {
133: result = false;
134: reason = "nodes at position "
135: + op1.position() + " differ";
136: break;
137: }
138: } else {
139: result = false;
140: reason = "comparing a node to an atomic value at position "
141: + op1.position();
142: break;
143: }
144: } else {
145: if (item2 instanceof NodeInfo) {
146: result = false;
147: reason = "comparing an atomic value to a node at position "
148: + op1.position();
149: break;
150: } else {
151: if (!collator.comparesEqual(item1, item2)) {
152: result = false;
153: reason = "atomic values at position "
154: + op1.position() + " differ";
155: break;
156: }
157: }
158: }
159: } // end while
160:
161: } catch (ClassCastException err) {
162: // this will happen if the sequences contain non-comparable values
163: // comparison errors are masked
164: result = false;
165: reason = "sequences contain non-comparable values";
166: } catch (XPathException err) {
167: // comparison errors are masked
168: result = false;
169: reason = "error occurred while comparing two values ("
170: + err.getMessage() + ')';
171: }
172:
173: if (!result) {
174: explain(config, reason, flags);
175: // config.getErrorListener().warning(
176: // new DynamicError("deep-equal(): " + reason)
177: // );
178: }
179:
180: return result;
181: }
182:
183: /**
184: * Determine whether two nodes are deep-equal
185: */
186:
187: private static boolean deepEquals(NodeInfo n1, NodeInfo n2,
188: AtomicComparer collator, Configuration config, int flags)
189: throws XPathException {
190: // shortcut: a node is always deep-equal to itself
191: if (n1.isSameNodeInfo(n2))
192: return true;
193:
194: if (n1.getNodeKind() != n2.getNodeKind()) {
195: explain(config, "node kinds differ: comparing "
196: + Type.displayTypeName(n1) + " to "
197: + Type.displayTypeName(n2), flags);
198: return false;
199: }
200:
201: final NamePool pool = config.getNamePool();
202: switch (n1.getNodeKind()) {
203: case Type.ELEMENT:
204: if (n1.getFingerprint() != n2.getFingerprint()) {
205: explain(config, "element names differ: "
206: + config.getNamePool().getClarkName(
207: n1.getFingerprint())
208: + " != "
209: + config.getNamePool().getClarkName(
210: n2.getFingerprint()), flags);
211: return false;
212: }
213: AxisIterator a1 = n1.iterateAxis(Axis.ATTRIBUTE);
214: AxisIterator a2 = n2.iterateAxis(Axis.ATTRIBUTE);
215: if (Aggregate.count(a1.getAnother()) != Aggregate.count(a2)) {
216: explain(config,
217: "elements have different number of attributes",
218: flags);
219: return false;
220: }
221: while (true) {
222: NodeInfo att1 = (NodeInfo) a1.next();
223: if (att1 == null)
224: break;
225:
226: AxisIterator a2iter = n2.iterateAxis(Axis.ATTRIBUTE,
227: new NameTest(Type.ATTRIBUTE, att1
228: .getFingerprint(), pool));
229: NodeInfo att2 = (NodeInfo) a2iter.next();
230:
231: if (att2 == null) {
232: explain(config, "one element has an attribute "
233: + config.getNamePool().getClarkName(
234: att1.getFingerprint())
235: + ", the other does not", flags);
236: return false;
237: }
238: if (!deepEquals(att1, att2, collator, config, flags)) {
239: explain(
240: config,
241: "elements have different values for the attribute "
242: + config
243: .getNamePool()
244: .getClarkName(
245: att1
246: .getFingerprint()),
247: flags);
248: return false;
249: }
250: }
251: if ((flags & INCLUDE_NAMESPACES) != 0) {
252: IntHashSet ns1 = new IntHashSet(10);
253: IntHashSet ns2 = new IntHashSet(10);
254: AxisIterator it1 = n1.iterateAxis(Axis.NAMESPACE);
255: while (true) {
256: NodeInfo nn1 = (NodeInfo) it1.next();
257: if (nn1 == null) {
258: break;
259: }
260: int nscode1 = pool.getNamespaceCode(nn1
261: .getLocalPart(), nn1.getStringValue());
262: ns1.add(nscode1);
263: }
264: AxisIterator it2 = n2.iterateAxis(Axis.NAMESPACE);
265: while (true) {
266: NodeInfo nn2 = (NodeInfo) it2.next();
267: if (nn2 == null) {
268: break;
269: }
270: int nscode2 = pool.getNamespaceCode(nn2
271: .getLocalPart(), nn2.getStringValue());
272: ns2.add(nscode2);
273: }
274: if (!ns1.equals(ns2)) {
275: explain(
276: config,
277: "elements have different in-scope namespaces",
278: flags);
279: return false;
280: }
281: }
282:
283: if ((flags & COMPARE_ANNOTATIONS) != 0) {
284: if (n1.getTypeAnnotation() != n2.getTypeAnnotation()) {
285: explain(config,
286: "elements have different type annotation",
287: flags);
288: return false;
289: }
290: }
291:
292: if ((flags & COMPARE_STRING_VALUES) == 0) {
293: final int ann1 = n1.getTypeAnnotation();
294: final int ann2 = n2.getTypeAnnotation();
295: final SchemaType type1 = config.getSchemaType(ann1);
296: final SchemaType type2 = config.getSchemaType(ann2);
297: final boolean isSimple1 = type1.isSimpleType()
298: || ((ComplexType) type1).isSimpleContent();
299: final boolean isSimple2 = type2.isSimpleType()
300: || ((ComplexType) type2).isSimpleContent();
301: if (isSimple1 != isSimple2) {
302: explain(
303: config,
304: "one element has a simple type, the other does not",
305: flags);
306: return false;
307: }
308: if (isSimple1 && isSimple2) {
309: final SequenceIterator v1 = n1.getTypedValue();
310: final SequenceIterator v2 = n2.getTypedValue();
311: return deepEquals(v1, v2, collator, config, flags);
312: }
313: }
314: // fall through
315: case Type.DOCUMENT:
316: AxisIterator c1 = n1.iterateAxis(Axis.CHILD);
317: AxisIterator c2 = n2.iterateAxis(Axis.CHILD);
318: while (true) {
319: NodeInfo d1 = (NodeInfo) c1.next();
320: while (d1 != null && isIgnorable(d1, flags)) {
321: d1 = (NodeInfo) c1.next();
322: }
323: NodeInfo d2 = (NodeInfo) c2.next();
324: while (d2 != null && isIgnorable(d2, flags)) {
325: d2 = (NodeInfo) c2.next();
326: }
327: if (d1 == null || d2 == null) {
328: boolean r = (d1 == d2);
329: if (!r) {
330: explain(
331: config,
332: "nodes have different numbers of children",
333: flags);
334: }
335: return r;
336: }
337: if (!deepEquals(d1, d2, collator, config, flags)) {
338: return false;
339: }
340: }
341:
342: case Type.ATTRIBUTE:
343: if (n1.getFingerprint() != n2.getFingerprint()) {
344: explain(config, "attribute names differ: "
345: + config.getNamePool().getClarkName(
346: n1.getFingerprint())
347: + " != "
348: + config.getNamePool().getClarkName(
349: n2.getFingerprint()), flags);
350: return false;
351: }
352: if ((flags & COMPARE_ANNOTATIONS) != 0) {
353: if (n1.getTypeAnnotation() != n2.getTypeAnnotation()) {
354: explain(
355: config,
356: "attributes have different type annotations",
357: flags);
358: return false;
359: }
360: }
361: boolean ar;
362: if ((flags & COMPARE_STRING_VALUES) == 0) {
363: ar = deepEquals(n1.getTypedValue(), n2.getTypedValue(),
364: collator, config, 0);
365: } else {
366: ar = collator.compare(n1.getStringValue(), n2
367: .getStringValue()) == 0;
368: }
369: if (!ar) {
370: explain(config, "attribute values differ", flags);
371: }
372: return ar;
373:
374: case Type.PROCESSING_INSTRUCTION:
375: case Type.NAMESPACE:
376: if (n1.getFingerprint() != n2.getFingerprint()) {
377: explain(config, Type.displayTypeName(n1)
378: + " names differ", flags);
379: return false;
380: }
381: // drop through
382: case Type.TEXT:
383: case Type.COMMENT:
384: boolean vr = (collator.comparesEqual(n1.getStringValue(),
385: n2.getStringValue()));
386: if (!vr) {
387: explain(config, Type.displayTypeName(n1)
388: + " values differ", flags);
389: }
390: return vr;
391:
392: default:
393: throw new IllegalArgumentException("Unknown node type");
394: }
395: }
396:
397: private static boolean isIgnorable(NodeInfo node, int flags) {
398: final int kind = node.getNodeKind();
399: if (kind == Type.COMMENT) {
400: return (flags & INCLUDE_COMMENTS) == 0;
401: } else if (kind == Type.PROCESSING_INSTRUCTION) {
402: return (flags & INCLUDE_PROCESSING_INSTRUCTIONS) == 0;
403: } else if (kind == Type.TEXT) {
404: return ((flags & EXCLUDE_WHITESPACE_TEXT_NODES) != 0)
405: && Whitespace.isWhite(node.getStringValueCS());
406: }
407: return false;
408: }
409:
410: private static void explain(Configuration config, String message,
411: int flags) {
412: try {
413: if ((flags & WARNING_IF_FALSE) != 0) {
414: config.getErrorListener().warning(
415: new DynamicError("deep-equal(): " + message));
416: }
417: } catch (TransformerException e) {
418: //
419: }
420: }
421:
422: }
423:
424: //
425: // The contents of this file are subject to the Mozilla Public License Version 1.0 (the "License");
426: // you may not use this file except in compliance with the License. You may obtain a copy of the
427: // License at http://www.mozilla.org/MPL/
428: //
429: // Software distributed under the License is distributed on an "AS IS" basis,
430: // WITHOUT WARRANTY OF ANY KIND, either express or implied.
431: // See the License for the specific language governing rights and limitations under the License.
432: //
433: // The Original Code is: all this file.
434: //
435: // The Initial Developer of the Original Code is Michael Kay
436: //
437: // Portions created by (your name) are Copyright (C) (your legal entity). All Rights Reserved.
438: //
439: // Contributor(s): none.
440: //
|