All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.sf.saxon.functions.DeepEqual Maven / Gradle / Ivy

There is a newer version: 9.9.1-2
Show newest version
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2018-2023 Saxonica Limited
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

package net.sf.saxon.functions;

import net.sf.saxon.expr.*;
import net.sf.saxon.expr.parser.ExpressionTool;
import net.sf.saxon.expr.sort.AtomicComparer;
import net.sf.saxon.expr.sort.GenericAtomicComparer;
import net.sf.saxon.lib.ErrorReporter;
import net.sf.saxon.lib.StringCollator;
import net.sf.saxon.ma.map.DictionaryMap;
import net.sf.saxon.ma.map.MapItem;
import net.sf.saxon.om.*;
import net.sf.saxon.pattern.NodeSelector;
import net.sf.saxon.pattern.SameNameTest;
import net.sf.saxon.str.StringTool;
import net.sf.saxon.str.StringView;
import net.sf.saxon.str.UnicodeBuilder;
import net.sf.saxon.str.UnicodeString;
import net.sf.saxon.trans.NoDynamicContextException;
import net.sf.saxon.trans.UncheckedXPathException;
import net.sf.saxon.trans.XPathException;
import net.sf.saxon.trans.XmlProcessingIncident;
import net.sf.saxon.transpile.CSharp;
import net.sf.saxon.tree.iter.AtomicIterator;
import net.sf.saxon.tree.iter.AxisIterator;
import net.sf.saxon.tree.iter.ListIterator;
import net.sf.saxon.tree.tiny.WhitespaceTextImpl;
import net.sf.saxon.tree.util.Navigator;
import net.sf.saxon.tree.util.Orphan;
import net.sf.saxon.type.*;
import net.sf.saxon.value.*;
import net.sf.saxon.z.IntHashSet;
import net.sf.saxon.z.IntSet;

import java.util.*;
import java.util.function.Function;

/**
 * XSLT 2.0 deep-equal() function, where the collation is already known.
 * Supports deep comparison of two sequences (of nodes and/or atomic values)
 * optionally using a collation
 */

public class DeepEqual extends CollatingFunctionFixed {

    public static OptionsParameter OPTION_DETAILS;

    static {
        OptionsParameter o = new OptionsParameter();
        o.addAllowedOption("base-uri", SequenceType.SINGLE_BOOLEAN, BooleanValue.FALSE);
        o.addAllowedOption("comments", SequenceType.SINGLE_BOOLEAN, BooleanValue.FALSE);
        o.addAllowedOption("debug", SequenceType.SINGLE_BOOLEAN, BooleanValue.FALSE);
        o.addAllowedOption("false-on-error", SequenceType.SINGLE_BOOLEAN, BooleanValue.FALSE);
        o.addAllowedOption("id-property", SequenceType.SINGLE_BOOLEAN, BooleanValue.FALSE);
        o.addAllowedOption("idrefs-property", SequenceType.SINGLE_BOOLEAN, BooleanValue.FALSE);
        o.addAllowedOption("in-scope-namespaces", SequenceType.SINGLE_BOOLEAN, BooleanValue.FALSE);
        o.addAllowedOption("namespace-prefixes", SequenceType.SINGLE_BOOLEAN, BooleanValue.FALSE);
        o.addAllowedOption("nilled-property", SequenceType.SINGLE_BOOLEAN, BooleanValue.FALSE);
        o.addAllowedOption("normalize-space", SequenceType.SINGLE_BOOLEAN, BooleanValue.FALSE);
        o.addAllowedOption("preserve-space", SequenceType.SINGLE_BOOLEAN, BooleanValue.TRUE);
        o.addAllowedOption("processing-instructions", SequenceType.SINGLE_BOOLEAN, BooleanValue.FALSE);
        o.addAllowedOption("text-boundaries", SequenceType.SINGLE_BOOLEAN, BooleanValue.TRUE);
        o.addAllowedOption("timezones", SequenceType.SINGLE_BOOLEAN, BooleanValue.FALSE);
        o.addAllowedOption("type-annotations", SequenceType.SINGLE_BOOLEAN, BooleanValue.FALSE);
        o.addAllowedOption("type-variety", SequenceType.SINGLE_BOOLEAN, BooleanValue.TRUE);
        o.addAllowedOption("typed-values", SequenceType.SINGLE_BOOLEAN, BooleanValue.TRUE);

        o.addAllowedOption("normalization-form", SequenceType.OPTIONAL_STRING, EmptySequence.getInstance());
        o.setAllowedValues("normalization-form", "FOJS0005", "NFC", "NFD", "NFKC", "NFKD");

        o.addAllowedOption("unordered-elements", BuiltInAtomicType.QNAME.zeroOrMore(), EmptySequence.getInstance());

        OPTION_DETAILS = o;
    }

    @Override
    public Expression makeFunctionCall(Expression... arguments) {
        Expression[] newArgs = new Expression[4];
        newArgs[0] = arguments[0];
        newArgs[1] = arguments[1];
        if (arguments.length < 3 || arguments[2] instanceof DefaultedArgumentExpression) {
            newArgs[2] = new StringLiteral(getRetainedStaticContext().getDefaultCollationName());
        } else {
            newArgs[2] = arguments[2];
        }
        if (arguments.length < 4 || arguments[3] instanceof DefaultedArgumentExpression) {
            newArgs[3] = Literal.makeLiteral(new DictionaryMap());
        } else {
            newArgs[3] = arguments[3];
        }
        setArity(4);
        return super.makeFunctionCall(newArgs);
    }


    // TODO: confine options param to 4.0.

    public static class DeepEqualOptions {
        public boolean baseUriSignificant = false;
        public boolean commentsSignificant = false;
        public boolean debug = false;
        public boolean falseOnError = false;
        public boolean idSignificant = false;
        public boolean idrefSignificant = false;
        public boolean inScopeNamespacesSignificant = false;
        public boolean namespacePrefixesSignificant = false;
        public String normalizationForm = null;
        public boolean nilledSignificant = false;
        public boolean normalizeSpace = false;
        public boolean processingInstructionsSignificant = false;
        public boolean textBoundariesSignificant = true;
        public boolean timezonesSignificant = false;
        public boolean typeAnnotationsSignificant = false;
        public boolean typeVarietySignificant = true;
        public boolean typedValuesSignificant = true;
        public Set unorderedElements = Collections.emptySet();
        public boolean preserveSpace = true;
        public String collationName;
        public StringCollator stringCollator;
        public AtomicComparer comparer;


        private static final String[] booleanOptions = new String[]{
                "base-uri", "comments", "debug", "false-on-error", "id-property", "idrefs-property",
                "in-scope-namespaces", "namespace-prefixes", "nilled-property", "normalize-space",
                "preserve-space", "processing-instructions", "text-boundaries", "timezones",
                "type-annotations", "type-variety", "typed-values"};


        public DeepEqualOptions() {}

        public DeepEqualOptions(MapItem map, String collationName, XPathContext context) throws XPathException {
            Map values = OPTION_DETAILS.processSuppliedOptions(map, context);
            for (String option : DeepEqualOptions.booleanOptions) {
                setBooleanOption(values, option);
            }
            GroundedValue normForm = map.get(new StringValue("normalization-form"));
            if (normForm != null) {
                normalizationForm = normForm.getStringValue();
            }
            GroundedValue listedElements = map.get(new StringValue("unordered-elements"));
            if (listedElements != null) {
                unorderedElements = new HashSet<>();
                for (Item item : listedElements.asIterable()) {
                    if (item instanceof QNameValue) {
                        unorderedElements.add(((QNameValue)item).getStructuredQName());
                    }
                }
            }
            this.collationName = collationName;
            stringCollator = context.getConfiguration().getCollation(collationName);
            if (stringCollator == null) {
                throw new XPathException("Unknown collation " + collationName, "FOCH0002");
            }
            comparer = GenericAtomicComparer.makeAtomicComparer(
                    BuiltInAtomicType.ANY_ATOMIC, BuiltInAtomicType.ANY_ATOMIC,
                    stringCollator, context);

            if (normalizeSpace || normalizationForm != null) {
                comparer = new NormalizingComparer(comparer, this);
            }
        }

        public static DeepEqualOptions defaultOptions() {
            return new DeepEqualOptions();
        }

        private void setBooleanOption(Map map, String optionName) throws XPathException {
            Sequence value = map.get(optionName);
            if (value != null) {
                boolean booleanValue = ExpressionTool.effectiveBooleanValue(value.iterate());
                switch (optionName) {
                    case "base-uri":
                        baseUriSignificant = booleanValue;
                        return;
                    case "comments":
                        commentsSignificant = booleanValue;
                        return;
                    case "debug":
                        debug = booleanValue;
                        return;
                    case "false-on-error":
                        falseOnError = booleanValue;
                        return;
                    case "id-property":
                        idSignificant = booleanValue;
                        return;
                    case "idrefs-property":
                        idrefSignificant = booleanValue;
                        return;
                    case "in-scope-namespaces":
                        inScopeNamespacesSignificant = booleanValue;
                        return;
                    case "namespace-prefixes":
                        namespacePrefixesSignificant = booleanValue;
                        return;
                    case "nilled-property":
                        nilledSignificant = booleanValue;
                        return;
                    case "normalize-space":
                        normalizeSpace = booleanValue;
                        return;
                    case "preserve-space":
                        preserveSpace = booleanValue;
                        return;
                    case "processing-instructions":
                        processingInstructionsSignificant = booleanValue;
                        return;
                    case "text-boundaries":
                        textBoundariesSignificant = booleanValue;
                        return;
                    case "timezones":
                        timezonesSignificant = booleanValue;
                        return;
                    case "type-annotations":
                        typeAnnotationsSignificant = booleanValue;
                        return;
                    case "type-variety":
                        typeVarietySignificant = booleanValue;
                        return;
                    case "typed-values":
                        typedValuesSignificant = booleanValue;
                        return;
                    default:
                        throw new IllegalArgumentException();
                }
            }
        }
    }


    /**
     * Determine when two sequences are deep-equal
     *
     * @param op1     the first sequence
     * @param op2     the second sequence
     * @param context the XPathContext item
     * @param options comparison options.
     * @return true if the sequences are deep-equal
     * @throws XPathException if either sequence contains a function item
     */

    public static boolean deepEqual(SequenceIterator op1, SequenceIterator op2,
                                    XPathContext context, DeepEqualOptions options)
            throws XPathException {
        boolean result = true;
        String reason = null;
        ErrorReporter reporter = context.getErrorReporter();

        try {

            if (!options.textBoundariesSignificant) {
                op1 = mergeAdjacentTextNodes(op1);
                op2 = mergeAdjacentTextNodes(op2);
            }
            int pos1 = 0;
            int pos2 = 0;
            while (true) {
                Item item1 = op1.next();
                Item item2 = op2.next();

                if (item1 == null && item2 == null) {
                    break;
                }

                pos1++;
                pos2++;

                if (item1 == null || item2 == null) {
                    result = false;
                    if (item1 == null) {
                        reason = "Second sequence is longer (first sequence length = " + pos2 + ")";
                    } else {
                        reason = "First sequence is longer (second sequence length = " + pos1 + ")";
                    }
                    if (item1 instanceof WhitespaceTextImpl || item2 instanceof WhitespaceTextImpl) {
                        reason += " (the first extra node is whitespace text)";
                    }
                    break;
                }

                if (item1 instanceof FunctionItem || item2 instanceof FunctionItem) {
                    if (!(item1 instanceof FunctionItem && item2 instanceof FunctionItem)) {
                        reason = "if one item is a function then both must be functions (position " + pos1 + ")";
                        return false;
                    }
                    // two maps or arrays can be deep-equal
                    boolean fe = ((FunctionItem) item1).deepEqual40((FunctionItem) item2, context, options);
                    if (!fe) {
                        result = false;
                        reason = "functions at position " + pos1 + " differ";
                        break;
                    }
                    continue;
                }

                if (item1 instanceof ObjectValue || item2 instanceof ObjectValue) {
                    if (!item1.equals(item2)) {
                        return false;
                    }
                    continue;
                }

                if (item1 instanceof NodeInfo) {
                    if (item2 instanceof NodeInfo) {
                        String message = deepEqual((NodeInfo) item1, (NodeInfo) item2, context, options);
                        if (message != null) {
                            result = false;
                            reason = "nodes at position " + pos1 + " differ: " + message;
                            break;
                        }
                    } else {
                        result = false;
                        reason = "comparing a node to an atomic value at position " + pos1;
                        break;
                    }
                } else {
                    if (item2 instanceof NodeInfo) {
                        result = false;
                        reason = "comparing an atomic value to a node at position " + pos1;
                        break;
                    } else {
                        AtomicValue av1 = (AtomicValue) item1;
                        AtomicValue av2 = (AtomicValue) item2;
                        if (av1.isNaN() && av2.isNaN()) {
                            // treat as equal, no action
                        } else if (!options.comparer.comparesEqual(av1, av2)) {
                            result = false;
                            reason = "atomic values at position " + pos1 + " differ";
                            break;
                        }
                        if (options.typeAnnotationsSignificant && !av1.getItemType().equals(av2.getItemType())) {
                            result = false;
                            reason = "atomic values at position " + pos1 + " have different type annotations";
                            break;
                        }
                        if (options.namespacePrefixesSignificant
                                && av1 instanceof QualifiedNameValue
                                && av2 instanceof QualifiedNameValue
                                && !((QualifiedNameValue)av1).getPrefix().equals(((QualifiedNameValue) av2).getPrefix())) {
                            result = false;
                            reason = "QName values at position " + pos1 + " have different namespace prefixes";
                            break;
                        }
                        if (options.timezonesSignificant
                                && av1 instanceof CalendarValue
                                && av2 instanceof CalendarValue
                                && ((CalendarValue) av1).getTimezoneInMinutes() != ((CalendarValue) av2).getTimezoneInMinutes()) {
                            result = false;
                            reason = "Values at position " + pos1 + " have different timezone";
                            break;
                        }
                    }
                }
            } // end while

        } catch (UncheckedXPathException uxe) {
            throw uxe.getXPathException();
        } catch (ClassCastException err) {
            // this will happen if the sequences contain non-comparable values
            // comparison errors are masked
            //err.printStackTrace();
            result = false;
            reason = "sequences contain non-comparable values";
        }

        if (!result) {
            explain(reporter, reason, options, null, null);
        }

        return result;
    }

    /*
     * Determine whether two nodes are deep-equal
     * @return null if they are deep equal, or an explanation of the reason if not
     */

    public static String deepEqual(NodeInfo n1, NodeInfo n2, XPathContext context, DeepEqualOptions options)
            throws XPathException {
        // shortcut: a node is always deep-equal to itself
        if (n1.equals(n2)) {
            return null;
        }

        ErrorReporter reporter = context.getErrorReporter();

        if (n1.getNodeKind() != n2.getNodeKind()) {
            String reason = "node kinds differ: comparing " + showKind(n1) + " to " + showKind(n2);
            explain(reporter, reason, options, n1, n2);
            return reason;
        }

        if (options.baseUriSignificant && !Objects.equals(n1.getBaseURI(), n2.getBaseURI())) {
            String reason = "base URIs differ: comparing " + n1.getBaseURI() + " to " + n2.getBaseURI();
            explain(reporter, reason, options, n1, n2);
            return reason;
        }

        if (options.typeAnnotationsSignificant && !n1.getSchemaType().equals(n2.getSchemaType())) {
            String reason = "nodes have different type annotations";
            explain(reporter, reason, options, n1, n2);
            return reason;
        }

        switch (n1.getNodeKind()) {
            case Type.ELEMENT:
                if (!Navigator.haveSameName(n1, n2)) {
                    final String reason = "element names differ: " + NameOfNode.makeName(n1).getStructuredQName().getEQName() +
                            " != " + NameOfNode.makeName(n2).getStructuredQName().getEQName();
                    explain(reporter, reason, options, n1, n2);
                    return reason;
                }
                if (options.namespacePrefixesSignificant && !n1.getPrefix().equals(n2.getPrefix())) {
                    final String reason = "element prefixes differ: " + n1.getPrefix() +
                            " != " + n2.getPrefix();
                    explain(reporter, reason, options, n1, n2);
                    return reason;
                }
                if (options.inScopeNamespacesSignificant && !n1.getAllNamespaces().equals(n2.getAllNamespaces())) {
                    final String reason = "in-scope namespaces differ: " + n1.getAllNamespaces() +
                            " versus " + n2.getAllNamespaces();
                    explain(reporter, reason, options, n1, n2);
                    return reason;
                }
                AxisIterator a1 = n1.iterateAxis(AxisInfo.ATTRIBUTE);
                AxisIterator a2 = n2.iterateAxis(AxisInfo.ATTRIBUTE);
                if (!SequenceTool.sameLength(a1, a2)) {
                    final String reason = "elements have different number of attributes";
                    explain(reporter, reason, options, n1, n2);
                    return reason;
                }
                NodeInfo att1;
                a1 = n1.iterateAxis(AxisInfo.ATTRIBUTE);
                while ((att1 = a1.next()) != null) {
                    AxisIterator a2iter = n2.iterateAxis(AxisInfo.ATTRIBUTE,
                                                         new SameNameTest(att1));
                    NodeInfo att2 = a2iter.next();

                    if (att2 == null) {
                        final String reason = "one element has an attribute " +
                                NameOfNode.makeName(att1).getStructuredQName().getEQName() +
                                ", the other does not";
                        explain(reporter, reason, options, n1, n2);
                        return reason;
                    }
                    String attReason = deepEqual(att1, att2, context, options);
                    if (attReason != null) {
                        final String reason = "elements have different values for the attribute " +
                                NameOfNode.makeName(att1).getStructuredQName().getEQName() + " - " + attReason;
                        explain(reporter, reason, options, n1, n2);
                        return reason;
                    }
                }
                if (options.inScopeNamespacesSignificant) {
                    NamespaceMap nm1 = n1.getAllNamespaces();
                    NamespaceMap nm2 = n2.getAllNamespaces();
                    if (!nm1.equals(nm2)) {
                        final String reason = "elements have different in-scope namespaces: " +
                                nm1 + " versus " + nm2;
                        explain(reporter, reason, options, n1, n2);
                        return reason;
                    }
                }

                if (options.typeAnnotationsSignificant) {
                    if (!n1.getSchemaType().equals(n2.getSchemaType())) {
                        final String reason = "elements have different type annotation";
                        explain(reporter, reason, options, n1, n2);
                        return reason;
                    }
                }

                if (options.typeVarietySignificant) {
                    if (n1.getSchemaType().isComplexType() != n2.getSchemaType().isComplexType()) {
                        final String reason = "one element has complex type, the other simple";
                        explain(reporter, reason, options, n1, n2);
                        return reason;
                    }

                    if (n1.getSchemaType().isComplexType()) {
                        ComplexVariety variety1 = ((ComplexType) n1.getSchemaType()).getVariety();
                        ComplexVariety variety2 = ((ComplexType) n2.getSchemaType()).getVariety();
                        if (variety1 != variety2) {
                            final String reason = "both elements have complex type, but a different variety";
                            explain(reporter, reason, options, n1, n2);
                            return reason;
                        }
                    }
                }

                if (options.typedValuesSignificant) {
                    final SchemaType type1 = n1.getSchemaType();
                    final SchemaType type2 = n2.getSchemaType();
                    final boolean isSimple1 = type1.isSimpleType() || ((ComplexType) type1).isSimpleContent();
                    final boolean isSimple2 = type2.isSimpleType() || ((ComplexType) type2).isSimpleContent();
                    if (options.typeVarietySignificant && isSimple1 != isSimple2) {
                        final String reason = "one element has a simple type, the other does not";
                        explain(reporter, reason, options, n1, n2);
                        return reason;
                    }
                    if (isSimple1 && isSimple2) {
                        final AtomicIterator v1 = n1.atomize().iterate();
                        final AtomicIterator v2 = n2.atomize().iterate();
                        boolean typedValueComparison = deepEqual(v1, v2, context, options);
                        return typedValueComparison ? null : "typed values of elements differ";
                    }
                }

                if (options.idSignificant && n1.isId() != n2.isId()) {
                    final String reason = "one element is an ID, the other is not";
                    explain(reporter, reason, options, n1, n2);
                    return reason;
                }
                if (options.idrefSignificant && n1.isIdref() != n2.isIdref()) {
                    final String reason = "one element is an IDREF, the other is not";
                    explain(reporter, reason, options, n1, n2);
                    return reason;
                }
                if (options.nilledSignificant && n1.isNilled() != n2.isNilled()) {
                    final String reason = "one element is nilled, the other is not";
                    explain(reporter, reason, options, n1, n2);
                    return reason;
                }

                if (options.unorderedElements.contains(NameOfNode.makeName(n1).getStructuredQName())) {
                    return hasSameChildrenUnordered(n1, n2, options, context);
                }

                CSharp.emitCode("goto case Saxon.Hej.type.Type.DOCUMENT;");
                // fall through
            case Type.DOCUMENT:
                SequenceIterator c1 = n1.iterateAxis(AxisInfo.CHILD, NodeSelector.of(node -> !isIgnorable(node, options)));
                SequenceIterator c2 = n2.iterateAxis(AxisInfo.CHILD, NodeSelector.of(node -> !isIgnorable(node, options)));

                if (!options.textBoundariesSignificant) {
                    c1 = mergeAdjacentTextNodes(c1);
                    c2 = mergeAdjacentTextNodes(c2);
                }

                while (true) {
                    NodeInfo d1 = (NodeInfo)c1.next();
                    NodeInfo d2 = (NodeInfo)c2.next();
                    if (d1 == null || d2 == null) {
                        boolean r = d1 == d2;
                        if (!r) {
                            String message = "the first operand contains a node with " +
                                    (d1 == null ? "fewer" : "more") +
                                    " children than the second";
                            if (d1 instanceof WhitespaceTextImpl || d2 instanceof WhitespaceTextImpl) {
                                message += " (the first extra child is whitespace text)";
                            }
                            explain(reporter, message, options, n1, n2);
                            return message;
                        }
                        return null;
                    }
                    String recursiveResult = deepEqual(d1, d2, context, options);
                    if (recursiveResult != null) {
                        return recursiveResult;
                    }
                }

            case Type.ATTRIBUTE:
                if (!Navigator.haveSameName(n1, n2)) {
                    final String reason = "attribute names differ: " +
                            NameOfNode.makeName(n1).getStructuredQName().getEQName() +
                            " != " + NameOfNode.makeName(n1).getStructuredQName().getEQName();
                    explain(reporter, reason, options, n1, n2);
                    return reason;
                }
                if (options.namespacePrefixesSignificant && !n1.getPrefix().equals(n2.getPrefix())) {
                    final String reason = "attribute prefixes differ: " + n1.getPrefix() +
                            " != " + n2.getPrefix();
                    explain(reporter, reason, options, n1, n2);
                    return reason;
                }
                if (options.typeAnnotationsSignificant) {
                    if (!n1.getSchemaType().equals(n2.getSchemaType())) {
                        final String reason = "attributes have different type annotations";
                        explain(reporter, reason, options, n1, n2);
                        return reason;
                    }
                }
                boolean ar;
                if (options.typedValuesSignificant) {
                    ar = deepEqual(n1.atomize().iterate(), n2.atomize().iterate(), context, options);
                } else {
                    ar = options.comparer.comparesEqual(new StringValue(n1.getUnicodeStringValue()), new StringValue(n2.getUnicodeStringValue()));
                }
                if (!ar) {
                    final String reason = "attribute values differ";
                    explain(reporter, reason, options, n1, n2);
                    return reason;
                }
                if (options.idSignificant && n1.isId() != n2.isId()) {
                    final String reason = "one attribute is an ID, the other is not";
                    explain(reporter, reason, options, n1, n2);
                    return reason;
                }
                if (options.idrefSignificant && n1.isIdref() != n2.isIdref()) {
                    final String reason = "one attribute is an IDREF, the other is not";
                    explain(reporter, reason, options, n1, n2);
                    return reason;
                }
                return null;


            case Type.PROCESSING_INSTRUCTION:
            case Type.NAMESPACE:
                if (!n1.getLocalPart().equals(n2.getLocalPart())) {
                    final String reason = Type.displayTypeName(n1) + " names differ";
                    explain(reporter, reason, options, n1, n2);
                    return reason;
                }
                CSharp.emitCode("goto case Saxon.Hej.type.Type.TEXT;");
                // drop through
            case Type.TEXT:
            case Type.COMMENT:
                boolean vr = compareStrings(n1.getStringValue(), n2.getStringValue(), options, context);
                        //options.comparer.comparesEqual((AtomicValue) n1.atomize(), (AtomicValue) n2.atomize());
                if (!vr) {
                    if (options.debug) {
                        String v1 = n1.getStringValue();
                        String v2 = n2.getStringValue();
                        String message = "";
                        if (v1.length() != v2.length()) {
                            message = "lengths (" + v1.length() + "," + v2.length() + ")";
                        }
                        if (v1.length() < 10 && v2.length() < 10) {
                            message = " (\"" + v1 + "\" vs \"" + v2 + "\")";
                        } else {
                            int min = Math.min(v1.length(), v2.length());

                            if (v1.substring(0, min).equals(v2.substring(0, min))) {
                                message += " different at char " + min + "(\"" +
                                        StringTool.diagnosticDisplay((v1.length() > v2.length() ? v1 : v2).substring(min)) + "\")";
                            } else if (v1.charAt(0) != v2.charAt(0)) {
                                message += " different at start " + "(\"" +
                                        v1.substring(0, Math.min(v1.length(), 10)) + "\", \"" +
                                        v2.substring(0, Math.min(v2.length(), 10)) + "\")";
                            } else {
                                for (int i = 1; i < min; i++) {
                                    if (!v1.substring(0, i).equals(v2.substring(0, i))) {
                                        message += " different at char " + (i - 1) + "(\"" +
                                                v1.substring(i - 1, Math.min(v1.length(), i + 10)) + "\", \"" +
                                                v2.substring(i - 1, Math.min(v2.length(), i + 10)) + "\")";
                                        break;
                                    }
                                }
                            }
                        }
                        explain(reporter, Type.displayTypeName(n1) + " values differ (" +
                                Navigator.getPath(n1) + ", " + Navigator.getPath(n2) + "): " +
                                message, options, n1, n2);
                        return message;
                    } else {
                        return "atomized values differ";
                    }
                }
                return null;

            default:
                throw new IllegalArgumentException("Unknown node kind");
        }
    }

    /**
     * Determine whether the children of a supplied node are deep-equal to some permutation of
     * the children of another.
     * @param e0 the first node
     * @param e1 the second node
     * @param options the comparison options
     * @param context the dynamic context
     * @return null if one sequence is deep-equal to some permutation of the other; otherwise, an explanation
     * of the difference
     */
    private static String hasSameChildrenUnordered(NodeInfo e0, NodeInfo e1,
                                         DeepEqualOptions options, XPathContext context) throws XPathException {
        List children0 = new ArrayList<>();
        List children1 = new ArrayList<>();

        for (NodeInfo c0 : e0.children()) {
            if (!isIgnorable(c0, options)) {
                children0.add(c0);
            }
        }

        for (NodeInfo c1 : e1.children()) {
            if (!isIgnorable(c1, options)) {
                children1.add(c1);
            }
        }

        if (children0.size() != children1.size()) {
            return "Number of children differs: " + children0.size() + " vs. " + children1.size();
        }

        List hashcodes1 = new ArrayList<>(children1.size());
        IntSet hashSet = new IntHashSet();
        for (NodeInfo nodeInfo : children1) {
            final int hash = computeHashCode(nodeInfo, options);
            hashSet.add(hash);
            hashcodes1.add(hash);
        }

        for (NodeInfo c0 : children0) {
            final int hash = computeHashCode(c0, options);
            if (!hashSet.contains(hash)) {
                return "Node found among first node's children with no counterpart among the second node's children";
            }
            int found = -1;
            for (int j = 0; j < hashcodes1.size(); j++) {
                if (hash == hashcodes1.get(j) && deepEqual(c0, children1.get(j), context, options) == null) {
                    found = j;
                    break;
                }
            }
            if (found >= 0) {
                children1.remove(found);
                hashcodes1.remove(found);
            } else {
                return "Node found among first node's children with no counterpart among the second node's children";
            }
        }
        return null;
    }

    private static int computeHashCode(NodeInfo node, DeepEqualOptions options) {
        // Keep it simple for now - independent of the options
        return node.getNodeKind() << 24 ^ node.getFingerprint() ^ (node.attributes().size() << 10);

    }

    private static long hashCodeOfSequence(GroundedValue value, Function hash) {
        long h = 0;
        for (Item it : value.asIterable()) {
            h ^= hash.apply(it);
        }
        return h;
    }

    private static long hashCodeOfNode(NodeInfo node, DeepEqualOptions options, XPathContext context) {
        int kind = node.getNodeKind();
        long h = 0x7876ABCD2345DCBAL;
        h ^= ((long) node.getFingerprint() << 25);
        if (options.namespacePrefixesSignificant) {
            h ^= ((long) node.getPrefix().hashCode() << 13);
        }
        if (kind == Type.TEXT && !Whitespace.isAllWhite(node.getUnicodeStringValue())) {
            String s = node.getStringValue();
            if (options.normalizeSpace) {
                s = Whitespace.collapseWhitespace(s);
            }
            if (options.normalizationForm != null) {
                try {
                    s = NormalizeUnicode.normalize(s, options.normalizationForm);
                } catch (XPathException e) {
                    throw new IllegalArgumentException(e);
                }
            }
            h ^= (long)s.hashCode() << 5;
        }
        return h;
    }

    private static boolean compareStrings(String s1, String s2, DeepEqualOptions options, XPathContext context) {
        if (options.normalizeSpace) {
            s1 = Whitespace.collapseWhitespace(s1);
            s2 = Whitespace.collapseWhitespace(s2);
        }
        if (options.normalizationForm != null) {
            try {
                s1 = NormalizeUnicode.normalize(s1, options.normalizationForm);
                s2 = NormalizeUnicode.normalize(s2, options.normalizationForm);
            } catch (XPathException e) {
                return false;
            }
        }
        return options.stringCollator.comparesEqual(StringView.of(s1), StringView.of(s2));
    }

    private static boolean isIgnorable(NodeInfo node, DeepEqualOptions options) {
        final int kind = node.getNodeKind();
        if (kind == Type.COMMENT) {
            return !options.commentsSignificant;
        } else if (kind == Type.PROCESSING_INSTRUCTION) {
            return !options.processingInstructionsSignificant;
        } else if (kind == Type.TEXT) {
            return (!options.preserveSpace) &&
                    Whitespace.isAllWhite(node.getUnicodeStringValue());
        }
        return false;
    }

    private static void explain(ErrorReporter reporter, String message, DeepEqualOptions options, NodeInfo n1, NodeInfo n2) {
        if (options.debug) {
            reporter.report(new XmlProcessingIncident("deep-equal() " +
                                                              (n1 != null && n2 != null ?
                                                                       "comparing " + Navigator.getPath(n1) + " to " + Navigator.getPath(n2) + ": " :
                                                                       ": ") +
                                                              message).asWarning());
        }
    }

    private static String showKind(Item item) {
        if (item instanceof NodeInfo && ((NodeInfo) item).getNodeKind() == Type.TEXT &&
                Whitespace.isAllWhite(item.getUnicodeStringValue())) {
            return "whitespace text() node";
        } else {
            return Type.displayTypeName(item);
        }
    }

    private static String showNamespaces(HashSet bindings) {
        StringBuilder sb = new StringBuilder(256);
        for (NamespaceBinding binding : bindings) {
            sb.append(binding.getPrefix());
            sb.append("=");
            sb.append(binding.getNamespaceUri());
            sb.append(" ");
        }
        sb.setLength(sb.length() - 1);
        return sb.toString();
    }

    private static SequenceIterator mergeAdjacentTextNodes(SequenceIterator in) throws XPathException {
        List items = new ArrayList<>(20);
        boolean prevIsText = false;
        UnicodeBuilder textBuffer = new UnicodeBuilder();
        while (true) {
            Item next = in.next();
            if (next == null) {
                break;
            }
            if (next instanceof NodeInfo && ((NodeInfo) next).getNodeKind() == Type.TEXT) {
                textBuffer.accept(next.getUnicodeStringValue());
                prevIsText = true;
            } else {
                if (prevIsText) {
                    Orphan textNode = new Orphan(null);
                    textNode.setNodeKind(Type.TEXT);
                    textNode.setStringValue(textBuffer.toUnicodeString());
                    items.add(textNode);
                    textBuffer.clear();
                }
                prevIsText = false;
                items.add(next);
            }
        }
        if (prevIsText) {
            Orphan textNode = new Orphan(null);
            textNode.setNodeKind(Type.TEXT);
            textNode.setStringValue(textBuffer.toUnicodeString());
            items.add(textNode);
        }
        return new ListIterator.Of<>(items);
    }

    /**
     * Execute a dynamic call to the function
     *
     * @param context   the dynamic evaluation context
     * @param arguments the values of the arguments, supplied as Sequences.
     * @return the result of the evaluation, in the form of a Sequence. It is the responsibility
     * of the callee to ensure that the type of result conforms to the expected result type.
     * @throws XPathException (should not happen)
     */

    @Override
    public BooleanValue call(XPathContext context, Sequence[] arguments) throws XPathException {
        Item arg3 = arguments.length >= 3 ? arguments[2].head() : null;
        String collationName = arg3 == null ? getRetainedStaticContext().getDefaultCollationName() : arg3.getStringValue();

        MapItem options  = new DictionaryMap();
        if (arguments.length >= 4) {
            MapItem suppliedOptions = (MapItem) arguments[3].head();
            if (suppliedOptions != null) {
                options = suppliedOptions;
            }
        }
        //GenericAtomicComparer comparer = new GenericAtomicComparer(getStringCollator(), context);
        DeepEqualOptions eqOptions = new DeepEqualOptions(options, collationName, context);
        boolean b = deepEqual(arguments[0].iterate(), arguments[1].iterate(), context, eqOptions);
        return BooleanValue.get(b);
    }

    @Override
    public String getStreamerName() {
        return "DeepEqual";
    }

    private static class NormalizingComparer implements AtomicComparer {

        private AtomicComparer baseComparer;
        private DeepEqualOptions options;

        public NormalizingComparer(AtomicComparer baseComparer, DeepEqualOptions options) {
            this.baseComparer = baseComparer;
            this.options = options;
        }

        @Override
        public StringCollator getCollator() {
            return baseComparer.getCollator();
        }

        @Override
        public AtomicComparer provideContext(XPathContext context) {
            baseComparer = baseComparer.provideContext(context);
            return this; // TODO: thread safety?
        }

        @Override
        public int compareAtomicValues(AtomicValue v0, AtomicValue v1) throws NoDynamicContextException {
            return baseComparer.compareAtomicValues(v0, v1);
        }

        @Override
        public boolean comparesEqual(AtomicValue v0, AtomicValue v1) throws NoDynamicContextException {
            if (v0 instanceof StringValue && v1 instanceof StringValue) {
                UnicodeString u0 = v0.getUnicodeStringValue();
                UnicodeString u1 = v1.getUnicodeStringValue();
                if (options.normalizeSpace) {
                    u0 = Whitespace.collapseWhitespace(u0);
                    u1 = Whitespace.collapseWhitespace(u1);
                }
                if (options.normalizationForm != null) {
                    try {
                        u0 = StringView.of(NormalizeUnicode.normalize(u0.toString(), options.normalizationForm));
                    } catch (XPathException e) {
                        throw new IllegalArgumentException(e);
                    }
                    try {
                        u1 = StringView.of(NormalizeUnicode.normalize(u1.toString(), options.normalizationForm));
                    } catch (XPathException e) {
                        throw new IllegalArgumentException();
                    }
                }
                return getCollator().comparesEqual(u0, u1);
            } else {
                return baseComparer.comparesEqual(v0, v1);
            }
        }

        @Override
        public String save() {
            return null;
        }
    }

}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy