AbstractBeanModelTest.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.commons.jxpath.ri.model;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;

import org.apache.commons.jxpath.AbstractFactory;
import org.apache.commons.jxpath.AbstractJXPathTest;
import org.apache.commons.jxpath.ClassFunctions;
import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.NestedTestBean;
import org.apache.commons.jxpath.Pointer;
import org.apache.commons.jxpath.ri.QName;
import org.apache.commons.jxpath.ri.compiler.NodeNameTest;
import org.apache.commons.jxpath.ri.compiler.TestFunctions;
import org.apache.commons.jxpath.ri.model.beans.PropertyOwnerPointer;
import org.apache.commons.jxpath.ri.model.beans.PropertyPointer;
import org.apache.commons.jxpath.ri.model.dynabeans.DynaBeanModelTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/**
 * Abstract superclass for Bean access with JXPath.
 */
public abstract class AbstractBeanModelTest extends AbstractJXPathTest {

    private JXPathContext context;

    protected abstract Object createContextBean();

    protected abstract AbstractFactory getAbstractFactory();

    private int relativeProperty(final PropertyPointer holder, final int offset) {
        final String[] names = holder.getPropertyNames();
        for (int i = 0; i < names.length; i++) {
            if (names[i].equals("integers")) {
                return i + offset;
            }
        }
        return -1;
    }

    @Override
    @BeforeEach
    public void setUp() {
//        if (context == null) {
        context = JXPathContext.newContext(createContextBean());
        context.setLocale(Locale.US);
        context.setFactory(getAbstractFactory());
//        }
    }

    @Test
    void testAttributeLang() {
        assertXPathValue(context, "@xml:lang", "en-US");
        assertXPathValue(context, "count(@xml:*)", Double.valueOf(1));
        assertXPathValue(context, "lang('en')", Boolean.TRUE);
        assertXPathValue(context, "lang('fr')", Boolean.FALSE);
    }

    /**
     * Testing the pseudo-attribute "name" that java beans objects appear to have.
     */
    @Test
    void testAttributeName() {
        assertXPathValue(context, "nestedBean[@name = 'int']", Integer.valueOf(1));
        assertXPathPointer(context, "nestedBean[@name = 'int']", "/nestedBean/int");
    }

    @Test
    void testAxisAncestor() {
        // ancestor::
        assertXPathValue(context, "int/ancestor::root = /", Boolean.TRUE);
        assertXPathValue(context, "count(beans/name/ancestor-or-self::node())", Double.valueOf(5));
        assertXPathValue(context, "beans/name/ancestor-or-self::node()[3] = /", Boolean.TRUE);
    }

    @Test
    void testAxisAttribute() {
        // Attributes are just like children to beans
        assertXPathValue(context, "count(@*)", Double.valueOf(21.0));
        // Unknown attribute
        assertXPathValueLenient(context, "@foo", null);
    }

    @Test
    void testAxisChild() {
        assertXPathValue(context, "boolean", Boolean.FALSE);
        assertXPathPointer(context, "boolean", "/boolean");
        assertXPathPointerIterator(context, "boolean", list("/boolean"));
        // Count elements in a child collection
        assertXPathValue(context, "count(set)", Double.valueOf(3));
//        assertXPathValue(context,"boolean/class/name", "java.lang.Boolean");
        // Child with namespace - should not find any
        assertXPathValueIterator(context, "foo:boolean", list());
        // Count all children with a wildcard
        assertXPathValue(context, "count(*)", Double.valueOf(21));
        // Same, constrained by node type = node()
        assertXPathValue(context, "count(child::node())", Double.valueOf(21));
    }

    @Test
    void testAxisChildNestedBean() {
        // Nested bean
        assertXPathValue(context, "nestedBean/name", "Name 0");
        assertXPathPointer(context, "nestedBean/name", "/nestedBean/name");
        assertXPathPointerIterator(context, "nestedBean/name", list("/nestedBean/name"));
    }

    @Test
    void testAxisChildNestedCollection() {
        assertXPathValueIterator(context, "integers", list(Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3), Integer.valueOf(4)));
        assertXPathPointer(context, "integers", "/integers");
        assertXPathPointerIterator(context, "integers", list("/integers[1]", "/integers[2]", "/integers[3]", "/integers[4]"));
    }

    @Test
    void testAxisDescendant() {
        // descendant::
        assertXPathValue(context, "count(descendant::node())", Double.valueOf(65));
        // Should not find any descendants with name root
        assertXPathValue(context, "count(descendant::root)", Double.valueOf(0));
        assertXPathValue(context, "count(descendant::name)", Double.valueOf(7));
    }

    @Test
    void testAxisDescendantOrSelf() {
        // descendant-or-self::
        assertXPathValueIterator(context, "descendant-or-self::name", set("Name 1", "Name 2", "Name 3", "Name 6", "Name 0", "Name 5", "Name 4"));
        // Same - abbreviated syntax
        assertXPathValueIterator(context, "//name", set("Name 1", "Name 2", "Name 3", "Name 6", "Name 0", "Name 5", "Name 4"));
        // See that it actually finds self
        assertXPathValue(context, "count(descendant-or-self::root)", Double.valueOf(1));
        // Combine descendant-or-self:: and and self::
        assertXPathValue(context, "count(nestedBean//.)", Double.valueOf(7));
        // Combine descendant-or-self:: and and self::name
        assertXPathValue(context, "count(//self::beans)", Double.valueOf(2));
        // Count all nodes in the tree
        assertXPathValue(context, "count(descendant-or-self::node())", Double.valueOf(66));
    }

    @Test
    void testAxisFollowing() {
        // following::
        assertXPathValue(context, "count(nestedBean/strings[2]/following::node())", Double.valueOf(21));
        assertXPathValue(context, "count(nestedBean/strings[2]/following::strings)", Double.valueOf(7));
    }

    @Test
    void testAxisFollowingSibling() {
        // following-sibling::
        assertXPathValue(context, "count(/nestedBean/following-sibling::node())", Double.valueOf(8));
        assertXPathValue(context, "count(/nestedBean/following-sibling::object)", Double.valueOf(1));
        // Combine parent:: and following-sibling::
        assertXPathValue(context, "count(/nestedBean/boolean/../following-sibling::node())", Double.valueOf(8));
        assertXPathValue(context, "count(/nestedBean/boolean/../following-sibling::object)", Double.valueOf(1));
        // Combine descendant:: and following-sibling::
        assertXPathValue(context, "count(/descendant::boolean/following-sibling::node())", Double.valueOf(53));
        assertXPathValue(context, "count(/descendant::boolean/following-sibling::name)", Double.valueOf(7));
    }

    @Test
    void testAxisParent() {
        // parent::
        assertXPathValue(context, "count(/beans/..)", Double.valueOf(1));
        assertXPathValue(context, "count(//..)", Double.valueOf(9));
        assertXPathValue(context, "count(//../..)", Double.valueOf(2));
        assertXPathValueIterator(context, "//parent::beans/name", list("Name 1", "Name 2"));
    }

    @Test
    void testAxisPreceding() {
        // preceding::
        assertXPathValue(context, "count(beans[2]/int/preceding::node())", Double.valueOf(8));
        assertXPathValue(context, "count(beans[2]/int/preceding::boolean)", Double.valueOf(2));
    }

    @Test
    void testAxisPrecedingSibling() {
        // preceding-sibling::
        assertXPathValue(context, "count(/boolean/preceding-sibling::node())", Double.valueOf(2));
        assertXPathValue(context, "count(/nestedBean/int/../preceding-sibling::node())", Double.valueOf(12));
        assertXPathValue(context, "count(/descendant::int/preceding-sibling::node())", Double.valueOf(10));
    }

    @Test
    void testAxisSelf() {
        // self::
        assertXPathValue(context, "self::node() = /", Boolean.TRUE);
        assertXPathValue(context, "self::root = /", Boolean.TRUE);
    }

    @Test
    void testBooleanPredicate() {
        // use child axis
        // bean[1]/int = 1
        // bean[2]/int = 3
        assertXPathValue(context, "beans[int > 2]/name", "Name 2");
        assertXPathValueIterator(context, "beans[int > 2]/name", list("Name 2"));
        assertXPathValueIterator(context, "beans[int >= 1]/name", list("Name 1", "Name 2"));
        assertXPathValueIterator(context, "beans[int < 2]/name", list("Name 1"));
        assertXPathValueIterator(context, "beans[int <= 3]/name", list("Name 1", "Name 2"));
        assertXPathValueIterator(context, "beans[1]/strings[string-length() = 8]", list("String 1", "String 2", "String 3"));
        // use some fancy axis and the child axis in the predicate
        assertXPathValueIterator(context, "//self::node()[name = 'Name 0']/name", list("Name 0"));
        // use context-dependent function in the predicate
        assertXPathValue(context, "beans/strings[name(.)='strings'][2]", "String 2");
        // use context-independent function in the predicate
        assertXPathValueIterator(context, "//self::node()[name(.) = concat('n', 'a', 'm', 'e')]",
                list("Name 1", "Name 2", "Name 3", "Name 6", "Name 0", "Name 5", "Name 4"));
        assertXPathValueIterator(context, "integers[position()<3]", list(Integer.valueOf(1), Integer.valueOf(2)));
        context.getVariables().declareVariable("temp", context.getValue("beans"));
        assertXPathValueIterator(context, "$temp[int < 2]/int", list(Integer.valueOf(1)));
    }

    @Test
    void testCoreFunctions() {
        assertXPathValue(context, "boolean(boolean)", Boolean.TRUE);
        assertXPathValue(context, "boolean(boolean = false())", Boolean.TRUE);
        assertXPathValue(context, "boolean(integers[position() < 3])", Boolean.TRUE);
        assertXPathValue(context, "boolean(integers[position() > 4])", Boolean.FALSE);
        assertXPathValue(context, "sum(integers)", Double.valueOf(10));
        assertXPathValueAndPointer(context, "integers[last()]", Integer.valueOf(4), "/integers[4]");
        assertXPathValueAndPointer(context, "//strings[last()]", "String 3", "/beans[1]/strings[3]");
    }

    @Test
    void testCreatePath() {
        context.setValue("nestedBean", null);
        // Calls factory.createObject(..., TestBean, "nestedBean")
        assertXPathCreatePath(context, "/nestedBean/int", Integer.valueOf(1), "/nestedBean/int");
        assertThrows(Exception.class,
                () -> assertXPathCreatePath(context, "/nestedBean/beans[last() + 1]", Integer.valueOf(1), "/nestedBean/beans[last() + 1]"),
                "Exception thrown on invalid path for creation");
    }

    @Test
    void testCreatePathAndSetValue() {
        context.setValue("nestedBean", null);
        // Calls factory.createObject(..., TestBean, "nestedBean")
        assertXPathCreatePathAndSetValue(context, "/nestedBean/int", Integer.valueOf(2), "/nestedBean/int");
    }

    @Test
    void testCreatePathAndSetValueCreateBeanExpandCollection() {
        context.setValue("nestedBean", null);
        // Calls factory.createObject(..., TestBean, "nestedBean")
        // Calls factory.createObject(..., nestedBean, "strings", 2)
        assertXPathCreatePathAndSetValue(context, "/nestedBean/strings[2]", "Test", "/nestedBean/strings[2]");
    }

    @Test
    void testCreatePathAndSetValueExpandExistingCollection() {
        // Another, but the collection already exists
        assertXPathCreatePathAndSetValue(context, "/beans[3]/int", Integer.valueOf(2), "/beans[3]/int");
    }

    @Test
    void testCreatePathAndSetValueExpandNewCollection() {
        context.setValue("beans", null);
        // Calls factory.createObject(..., testBean, "beans", 2),
        // then factory.createObject(..., testBean, "beans", 2)
        assertXPathCreatePathAndSetValue(context, "/beans[2]/int", Integer.valueOf(2), "/beans[2]/int");
    }

    @Test
    void testCreatePathCreateBeanExpandCollection() {
        context.setValue("nestedBean", null);
        // Calls factory.createObject(..., TestBean, "nestedBean")
        // Calls factory.createObject(..., nestedBean, "strings", 2)
        assertXPathCreatePath(context, "/nestedBean/strings[2]", "String 2", "/nestedBean/strings[2]");
    }

    @Test
    void testCreatePathExpandExistingCollection() {
        // Calls factory.createObject(..., TestBean, "integers", 5)
        // to expand collection
        assertXPathCreatePathAndSetValue(context, "/integers[5]", Integer.valueOf(3), "/integers[5]");
    }

    @Test
    void testCreatePathExpandExistingCollectionAndSetProperty() {
        // Another, but the collection already exists
        assertXPathCreatePath(context, "/beans[3]/int", Integer.valueOf(1), "/beans[3]/int");
    }

    @Test
    void testCreatePathExpandNewCollection() {
        context.setValue("beans", null);
        // Calls factory.createObject(..., testBean, "beans", 2),
        // then factory.createObject(..., testBean, "beans", 2)
        assertXPathCreatePath(context, "/beans[2]/int", Integer.valueOf(1), "/beans[2]/int");
    }

    @Test
    void testDocumentOrder() {
        assertDocumentOrder(context, "boolean", "int", -1);
        assertDocumentOrder(context, "integers[1]", "integers[2]", -1);
        assertDocumentOrder(context, "integers[1]", "integers[1]", 0);
        assertDocumentOrder(context, "nestedBean/int", "nestedBean", 1);
        assertDocumentOrder(context, "nestedBean/int", "nestedBean/strings", -1);
        assertDocumentOrder(context, "nestedBean/int", "object/int", -1);
    }

    @Test
    void testIndexPredicate() {
        assertXPathValue(context, "integers[2]", Integer.valueOf(2));
        assertXPathPointer(context, "integers[2]", "/integers[2]");
        assertXPathPointerIterator(context, "integers[2]", list("/integers[2]"));
        assertXPathValue(context, "beans[1]/name", "Name 1");
        assertXPathPointer(context, "beans[1]/name", "/beans[1]/name");
        assertXPathValueIterator(context, "beans[1]/strings", list("String 1", "String 2", "String 3"));
        assertXPathValueIterator(context, "beans/strings[2]", list("String 2", "String 2"));
        // Find the first match
        assertXPathValue(context, "beans/strings[2]", "String 2");
        // Indexing in a set collected from a UnionContext
        assertXPathValue(context, "(beans/strings[2])[1]", "String 2");
    }

    private void testIndividual(final int relativePropertyIndex, final int offset, final boolean useStartLocation, final boolean reverse, final int expected) {
        final PropertyOwnerPointer root = (PropertyOwnerPointer) NodePointer.newNodePointer(new QName(null, "root"), createContextBean(), Locale.getDefault());
        NodeIterator it;
        PropertyPointer start = null;
        if (useStartLocation) {
            start = root.getPropertyPointer();
            start.setPropertyIndex(relativeProperty(start, relativePropertyIndex));
            start.setIndex(offset);
        }
        it = root.childIterator(new NodeNameTest(new QName(null, "integers")), reverse, start);
        int size = 0;
        while (it.setPosition(it.getPosition() + 1)) {
            size++;
        }
        assertEquals(expected, size, "ITERATIONS: Individual, relativePropertyIndex=" + relativePropertyIndex + ", offset=" + offset + ", useStartLocation="
                + useStartLocation + ", reverse=" + reverse);
    }

    /**
     * Test property iterators, the core of the graph traversal engine
     */
    @Test
    void testIndividualIterators() {
        testIndividual(+1, 0, true, false, 0);
        testIndividual(-1, 0, true, false, 4);
        testIndividual(0, -1, true, true, 4);
        testIndividual(+1, -1, true, true, 4);
        testIndividual(-1, -1, true, true, 0);
        testIndividual(0, 1, true, false, 2);
        testIndividual(0, 1, true, true, 1);
        testIndividual(0, 0, false, false, 4);
        testIndividual(0, 0, false, true, 4);
    }

    @Test
    void testIterateAndSet() {
        final JXPathContext context = JXPathContext.newContext(createContextBean());
        Iterator<Pointer> it = context.iteratePointers("beans/int");
        int i = 5;
        while (it.hasNext()) {
            final NodePointer pointer = (NodePointer) it.next();
            pointer.setValue(Integer.valueOf(i++));
        }
        it = context.iteratePointers("beans/int");
        final List<Object> actual = new ArrayList<>();
        while (it.hasNext()) {
            actual.add(it.next().getValue());
        }
        assertEquals(list(Integer.valueOf(5), Integer.valueOf(6)), actual, "Iterating <beans/int>");
    }

    /**
     * Test contributed by Kate Dvortsova
     */
    @Test
    void testIteratePointerSetValue() {
        final JXPathContext context = JXPathContext.newContext(createContextBean());
        assertXPathValue(context, "/beans[1]/name", "Name 1");
        assertXPathValue(context, "/beans[2]/name", "Name 2");
        // Test setting via context
        context.setValue("/beans[2]/name", "Name 2 set");
        assertXPathValue(context, "/beans[2]/name", "Name 2 set");
        // Restore original value
        context.setValue("/beans[2]/name", "Name 2");
        assertXPathValue(context, "/beans[2]/name", "Name 2");
        int iterCount = 0;
        final Iterator<Pointer> iter = context.iteratePointers("/beans/name");
        while (iter.hasNext()) {
            iterCount++;
            final Pointer pointer = iter.next();
            String s = (String) pointer.getValue();
            s += "suffix";
            pointer.setValue(s);
            assertEquals(s, pointer.getValue(), "pointer.getValue");
            // fails right here, the value isn't getting set in the bean.
            assertEquals(s, context.getValue(pointer.asPath()), "context.getValue");
        }
        assertEquals(2, iterCount, "Iteration count");
        assertXPathValue(context, "/beans[1]/name", "Name 1suffix");
        assertXPathValue(context, "/beans[2]/name", "Name 2suffix");
    }

    @Test
    void testIteratePropertyArrayWithHasNext() {
        final JXPathContext context = JXPathContext.newContext(createContextBean());
        final Iterator<Pointer> it = context.iteratePointers("/integers");
        final List<String> actual = new ArrayList<>();
        while (it.hasNext()) {
            actual.add(it.next().asPath());
        }
        assertEquals(list("/integers[1]", "/integers[2]", "/integers[3]", "/integers[4]"), actual, "Iterating 'hasNext'/'next'</integers>");
    }

    @Test
    void testIteratePropertyArrayWithoutHasNext() {
        final JXPathContext context = JXPathContext.newContext(createContextBean());
        final Iterator<Pointer> it = context.iteratePointers("/integers");
        final List<String> actual = new ArrayList<>();
        for (int i = 0; i < 4; i++) {
            actual.add(it.next().toString());
        }
        assertEquals(list("/integers[1]", "/integers[2]", "/integers[3]", "/integers[4]"), actual, "Iterating 'next'</integers>");
    }

    private void testMultiple(final int propertyIndex, final int offset, final boolean useStartLocation, final boolean reverse, final int expected) {
        final PropertyOwnerPointer root = (PropertyOwnerPointer) NodePointer.newNodePointer(new QName(null, "root"), createContextBean(), Locale.getDefault());
        NodeIterator it;
        PropertyPointer start = null;
        if (useStartLocation) {
            start = root.getPropertyPointer();
            start.setPropertyIndex(propertyIndex);
            start.setIndex(offset);
        }
        it = root.childIterator(null, reverse, start);
        int size = 0;
        while (it.setPosition(it.getPosition() + 1)) {
//            System.err.println("LOC: " + it.getCurrentNodePointer());
            size++;
        }
        assertEquals(expected, size, "ITERATIONS: Multiple, propertyIndex=" + propertyIndex + ", offset=" + offset + ", useStartLocation=" + useStartLocation
                + ", reverse=" + reverse);
    }

    /**
     * Test property iterators with multiple properties returned
     */
    @Test
    void testMultipleIterators() {
        testMultiple(0, 0, true, false, 20);
        testMultiple(3, 0, true, false, 16);
        testMultiple(3, -1, true, true, 8);
        testMultiple(3, 0, true, true, 4);
        testMultiple(0, 0, false, false, 21);
        testMultiple(0, 0, false, true, 21);
        testMultiple(3, 1, true, false, 15);
        testMultiple(3, 3, true, false, 13);
    }

    @Test
    void testRelativeContextAbsolutePath() {
        final JXPathContext relative = context.getRelativeContext(context.getPointer("nestedBean"));
        assertXPathValueAndPointer(relative, "/integers[2]", Integer.valueOf(2), "/integers[2]");
    }

    @Test
    void testRelativeContextInheritance() {
        context.setFunctions(new ClassFunctions(TestFunctions.class, "test"));
        final JXPathContext relative = context.getRelativeContext(context.getPointer("nestedBean"));
        assertXPathValue(relative, "test:countPointers(strings)", Integer.valueOf(3));
    }

    @Test
    void testRelativeContextParent() {
        final JXPathContext relative = context.getRelativeContext(context.getPointer("nestedBean"));
        assertXPathValueAndPointer(relative, "../integers[2]", Integer.valueOf(2), "/integers[2]");
    }

    @Test
    void testRelativeContextRelativePath() {
        final JXPathContext relative = context.getRelativeContext(context.getPointer("nestedBean"));
        assertXPathValueAndPointer(relative, "int", Integer.valueOf(1), "/nestedBean/int");
    }

    @Test
    void testRemoveAllArrayElements() {
        context.removeAll("nestedBean/strings");
        assertXPathValueIterator(context, "nestedBean/strings", list());
    }

    @Test
    void testRemoveAllListElements() {
        context.removeAll("list");
        assertXPathValueIterator(context, "list", this instanceof DynaBeanModelTest ? list(null, null, null) : list());
    }

    @Test
    void testRemoveAllMapEntries() {
        context.removeAll("map/*");
        assertXPathValue(context, "map", Collections.EMPTY_MAP);
    }

    @Test
    void testRemovePathArrayElement() {
        // Assigns a new array to the property
        context.removePath("nestedBean/strings[1]");
        assertEquals("String 2", context.getValue("nestedBean/strings[1]"), "Remove array element");
    }

    @Test
    void testRemovePathBeanValue() {
        context.removePath("nestedBean");
        assertNull(context.getValue("nestedBean"), "Remove collection element");
    }

    @Test
    void testRemovePathPropertyValue() {
        // Remove property value
        context.removePath("nestedBean/int");
        assertEquals(Integer.valueOf(0), context.getValue("nestedBean/int"), "Remove property value");
    }

    @Test
    void testRoot() {
        assertXPathValueAndPointer(context, "/", context.getContextBean(), "/");
    }

    @Test
    void testSetCollectionElement() {
        // Collection element
        assertXPathSetValue(context, "integers[2]", Integer.valueOf(5));
        // Collection element with conversion
        assertXPathSetValue(context, "integers[2]", new int[] { 6 }, Integer.valueOf(6));
    }

    @Test
    void testSetContextDependentNode() {
        // Find node without using SimplePathInterpreter
        assertXPathSetValue(context, "integers[position() = 1]", Integer.valueOf(8));
        // Find node without using SimplePathInterpreter and set its property
        assertXPathSetValue(context, "beans[name = 'Name 1']/int", Integer.valueOf(9));
    }

    @Test
    void testSetNonPrimitiveValue() {
        // First, let's see if we can set a collection element to null
        assertXPathSetValue(context, "beans[2]", null);
        // Now, assign it a whole bean
        context.setValue("beans[2]", new NestedTestBean("Name 9"));
        assertEquals("Name 9", context.getValue("beans[2]/name"), "Modified <beans[2]/name>");
    }

    @Test
    void testSetPropertyValue() {
        // Simple property
        assertXPathSetValue(context, "int", Integer.valueOf(2));
        // Simple property with conversion from string
        assertXPathSetValue(context, "int", "3", Integer.valueOf(3));
        // Simple property with conversion from array
        assertXPathSetValue(context, "int", new int[] { 4 }, Integer.valueOf(4));
        // Attribute (which is the same as a child for beans
        assertXPathSetValue(context, "@int", Integer.valueOf(10));
    }

    @Test
    void testUnion() {
        // Union - note corrected document order
        assertXPathValueIterator(context, "integers | beans[1]/strings",
                list("String 1", "String 2", "String 3", Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3), Integer.valueOf(4)));
        assertXPathValue(context, "count((integers | beans[1]/strings)[contains(., '1')])", Double.valueOf(2));
        assertXPathValue(context, "count((integers | beans[1]/strings)[name(.) = 'strings'])", Double.valueOf(3));
        // Note that the following is different from "integer[2]" -
        // it is a filter expression
        assertXPathValue(context, "(integers)[2]", Integer.valueOf(2));
    }
}