diff --git a/src/main/java/org/apache/xmlbeans/impl/store/Locale.java b/src/main/java/org/apache/xmlbeans/impl/store/Locale.java
index ddaef58e3..06d4f33e8 100755
--- a/src/main/java/org/apache/xmlbeans/impl/store/Locale.java
+++ b/src/main/java/org/apache/xmlbeans/impl/store/Locale.java
@@ -1630,6 +1630,9 @@ Xobj fetch(Xobj parent, QName name, QNameSet set, int n) {
_version = Locale.this.version();
_parent = parent;
_name = name;
+ // both keys have to be kept in step, or a lookup by set can match a
+ // cached position that was seeded by a lookup by name
+ _set = set;
_child = null;
_n = -1;
diff --git a/src/main/java/org/apache/xmlbeans/impl/store/Xobj.java b/src/main/java/org/apache/xmlbeans/impl/store/Xobj.java
index 4a58e19a7..eb0ae6f6e 100644
--- a/src/main/java/org/apache/xmlbeans/impl/store/Xobj.java
+++ b/src/main/java/org/apache/xmlbeans/impl/store/Xobj.java
@@ -1963,23 +1963,16 @@ public int count_elements(QNameSet names) {
}
public TypeStoreUser find_element_user(QName name, int i) {
- for (Xobj x = _firstChild; x != null; x = x._nextSibling) {
- if (x.isElem() && x._name.equals(name) && --i < 0) {
- return x.getUser();
- }
- }
+ // a negative index has always resolved to the first matching element
+ Xobj x = _locale.findNthChildElem(this, name, null, Math.max(i, 0));
- return null;
+ return x == null ? null : x.getUser();
}
public TypeStoreUser find_element_user(QNameSet names, int i) {
- for (Xobj x = _firstChild; x != null; x = x._nextSibling) {
- if (x.isElem() && names.contains(x._name) && --i < 0) {
- return x.getUser();
- }
- }
+ Xobj x = _locale.findNthChildElem(this, null, names, Math.max(i, 0));
- return null;
+ return x == null ? null : x.getUser();
}
@SuppressWarnings("unchecked")
diff --git a/src/main/java/org/apache/xmlbeans/impl/values/JavaListIterator.java b/src/main/java/org/apache/xmlbeans/impl/values/JavaListIterator.java
new file mode 100644
index 000000000..77eca6aa5
--- /dev/null
+++ b/src/main/java/org/apache/xmlbeans/impl/values/JavaListIterator.java
@@ -0,0 +1,141 @@
+/* Copyright 2004 The Apache Software Foundation
+ *
+ * Licensed 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
+ *
+ * http://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.xmlbeans.impl.values;
+
+import java.util.List;
+import java.util.ListIterator;
+import java.util.NoSuchElementException;
+
+/**
+ * Iterator over one of the live lists handed out by the generated
+ * getXxxList() accessors.
+ *
+ * The iterators {@link java.util.AbstractList} supplies ask the list for its
+ * {@link List#size()} on every call to {@link #hasNext()}. For a list backed by the
+ * XML store that size is a walk over all of the children of the parent element, so a
+ * single pass over the list ends up doing O(n^2) work on top of the cost of reading
+ * the elements themselves. This iterator asks for the size only when it thinks it has
+ * reached the end, so a pass costs one size() call rather than one per element.
+ *
+ * The list stays live: elements appended while the iteration is in progress are still
+ * picked up, because reaching the end of the run re-reads the size before giving up.
+ */
+class JavaListIterator implements ListIterator {
+ private final List list;
+
+ /** index of the element that a call to {@link #next()} would return */
+ private int cursor;
+
+ /** index of the element returned by the last next()/previous(), -1 if there is none */
+ private int lastRet = -1;
+
+ /** the last size read back from the list, -1 if it needs to be read again */
+ private int knownSize = -1;
+
+ JavaListIterator(List list, int index) {
+ this.list = list;
+ this.cursor = index;
+ }
+
+ @Override
+ public boolean hasNext() {
+ if (knownSize < 0 || cursor >= knownSize) {
+ knownSize = list.size();
+ }
+
+ return cursor < knownSize;
+ }
+
+ @Override
+ public T next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+
+ try {
+ T value = list.get(cursor);
+ lastRet = cursor++;
+ return value;
+ } catch (IndexOutOfBoundsException e) {
+ // the list shrank underneath us since the size was last read
+ knownSize = -1;
+ throw new NoSuchElementException();
+ }
+ }
+
+ @Override
+ public boolean hasPrevious() {
+ return cursor > 0;
+ }
+
+ @Override
+ public T previous() {
+ if (cursor <= 0) {
+ throw new NoSuchElementException();
+ }
+
+ try {
+ T value = list.get(cursor - 1);
+ lastRet = --cursor;
+ return value;
+ } catch (IndexOutOfBoundsException e) {
+ knownSize = -1;
+ throw new NoSuchElementException();
+ }
+ }
+
+ @Override
+ public int nextIndex() {
+ return cursor;
+ }
+
+ @Override
+ public int previousIndex() {
+ return cursor - 1;
+ }
+
+ @Override
+ public void remove() {
+ if (lastRet < 0) {
+ throw new IllegalStateException();
+ }
+
+ list.remove(lastRet);
+
+ if (lastRet < cursor) {
+ cursor--;
+ }
+
+ lastRet = -1;
+ knownSize = -1;
+ }
+
+ @Override
+ public void set(T t) {
+ if (lastRet < 0) {
+ throw new IllegalStateException();
+ }
+
+ list.set(lastRet, t);
+ }
+
+ @Override
+ public void add(T t) {
+ list.add(cursor++, t);
+ lastRet = -1;
+ knownSize = -1;
+ }
+}
diff --git a/src/main/java/org/apache/xmlbeans/impl/values/JavaListObject.java b/src/main/java/org/apache/xmlbeans/impl/values/JavaListObject.java
index 15d880983..ee46bc9f7 100644
--- a/src/main/java/org/apache/xmlbeans/impl/values/JavaListObject.java
+++ b/src/main/java/org/apache/xmlbeans/impl/values/JavaListObject.java
@@ -16,6 +16,8 @@
package org.apache.xmlbeans.impl.values;
import java.util.AbstractList;
+import java.util.Iterator;
+import java.util.ListIterator;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -86,4 +88,18 @@ public int size() {
}
return sizer.get();
}
+
+ @Override
+ public Iterator iterator() {
+ return new JavaListIterator<>(this, 0);
+ }
+
+ @Override
+ public ListIterator listIterator(int index) {
+ if (index < 0 || index > size()) {
+ throw new IndexOutOfBoundsException("Index: " + index);
+ }
+
+ return new JavaListIterator<>(this, index);
+ }
}
diff --git a/src/main/java/org/apache/xmlbeans/impl/values/JavaListXmlObject.java b/src/main/java/org/apache/xmlbeans/impl/values/JavaListXmlObject.java
index 5d0b9c1f3..eda7b8eaf 100644
--- a/src/main/java/org/apache/xmlbeans/impl/values/JavaListXmlObject.java
+++ b/src/main/java/org/apache/xmlbeans/impl/values/JavaListXmlObject.java
@@ -18,6 +18,8 @@
import org.apache.xmlbeans.XmlObject;
import java.util.AbstractList;
+import java.util.Iterator;
+import java.util.ListIterator;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -88,4 +90,18 @@ public int size() {
}
return sizer.get();
}
+
+ @Override
+ public Iterator iterator() {
+ return new JavaListIterator<>(this, 0);
+ }
+
+ @Override
+ public ListIterator listIterator(int index) {
+ if (index < 0 || index > size()) {
+ throw new IndexOutOfBoundsException("Index: " + index);
+ }
+
+ return new JavaListIterator<>(this, index);
+ }
}
diff --git a/src/test/java/org/apache/xmlbeans/impl/values/FindElementUserTest.java b/src/test/java/org/apache/xmlbeans/impl/values/FindElementUserTest.java
new file mode 100644
index 000000000..e17bd305b
--- /dev/null
+++ b/src/test/java/org/apache/xmlbeans/impl/values/FindElementUserTest.java
@@ -0,0 +1,379 @@
+/* Copyright 2004 The Apache Software Foundation
+ *
+ * Licensed 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
+ *
+ * http://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.xmlbeans.impl.values;
+
+import org.apache.xmlbeans.QNameSet;
+import org.apache.xmlbeans.XmlCursor;
+import org.apache.xmlbeans.XmlException;
+import org.apache.xmlbeans.XmlObject;
+import org.junit.jupiter.api.Test;
+
+import javax.xml.namespace.QName;
+import java.util.Random;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+/**
+ * Exercises TypeStore.find_element_user directly - it is what every generated
+ * getXxxArray(i) accessor resolves an index through.
+ */
+class FindElementUserTest {
+ private static final String NS = "urn:find-element-user";
+
+ private static final QName A = new QName(NS, "a");
+ private static final QName B = new QName(NS, "b");
+ private static final QName C = new QName(NS, "c");
+ private static final QName Z = new QName(NS, "z");
+ private static final QName NO_NS_A = new QName("", "a");
+
+ private static final QNameSet AC = QNameSet.forArray(new QName[]{A, C});
+ private static final QNameSet ABC = QNameSet.forArray(new QName[]{A, B, C});
+ private static final QNameSet JUST_B = QNameSet.forArray(new QName[]{B});
+
+ /**
+ * Elements in document order: a0 b0 a1 c0 a2 b1 a3. The attribute, the comment,
+ * the processing instruction and the loose text are all there to be skipped over.
+ */
+ private static final String MIXED =
+ "" +
+ "loose text" +
+ "a0" +
+ "" +
+ "b0" +
+ "a1" +
+ "" +
+ "c0" +
+ "more loose text" +
+ "a2" +
+ "b1" +
+ "a3" +
+ "";
+
+ private static TypeStore store(String xml) throws XmlException {
+ return store(XmlObject.Factory.parse(xml), "root");
+ }
+
+ private static TypeStore store(XmlObject doc, String element) {
+ XmlObject[] found = doc.selectChildren(new QName(NS, element));
+ assertEquals(1, found.length);
+ return ((TypeStoreUser) found[0]).get_store();
+ }
+
+ private static String text(TypeStoreUser user) {
+ if (user == null) {
+ return null;
+ }
+
+ try (XmlCursor cursor = ((XmlObject) user).newCursor()) {
+ return cursor.getTextValue();
+ }
+ }
+
+ private static String repeated(String name, int count) {
+ StringBuilder xml = new StringBuilder("");
+ for (int i = 0; i < count; i++) {
+ xml.append('<').append(name).append('>').append(name).append(i).append("").append(name).append('>');
+ }
+ return xml.append("").toString();
+ }
+
+ private static int[] shuffled(int n) {
+ int[] order = new int[n];
+ for (int i = 0; i < n; i++) {
+ order[i] = i;
+ }
+
+ Random random = new Random(1234);
+ for (int i = n - 1; i > 0; i--) {
+ int j = random.nextInt(i + 1);
+ int t = order[i];
+ order[i] = order[j];
+ order[j] = t;
+ }
+
+ return order;
+ }
+
+ // ---- find_element_user(QName, int) ----
+
+ @Test
+ void byNameFindsTheNthElementOfThatName() throws Exception {
+ TypeStore store = store(MIXED);
+
+ assertEquals("a0", text(store.find_element_user(A, 0)));
+ assertEquals("a1", text(store.find_element_user(A, 1)));
+ assertEquals("a2", text(store.find_element_user(A, 2)));
+ assertEquals("a3", text(store.find_element_user(A, 3)));
+
+ assertEquals("b0", text(store.find_element_user(B, 0)));
+ assertEquals("b1", text(store.find_element_user(B, 1)));
+
+ assertEquals("c0", text(store.find_element_user(C, 0)));
+ }
+
+ @Test
+ void byNameIgnoresAttributesCommentsProcessingInstructionsAndText() throws Exception {
+ TypeStore store = store(MIXED);
+
+ // the root carries an attribute whose name is the same QName as the "a" elements
+ assertEquals("a0", text(store.find_element_user(A, 0)));
+ assertNull(store.find_element_user(A, 4));
+ }
+
+ @Test
+ void byNameReturnsNullPastTheLastMatch() throws Exception {
+ TypeStore store = store(MIXED);
+
+ assertNull(store.find_element_user(A, 4));
+ assertNull(store.find_element_user(B, 2));
+ assertNull(store.find_element_user(C, 1));
+ assertNull(store.find_element_user(A, 4000));
+ }
+
+ @Test
+ void byNameReturnsNullForANameThatIsNotThere() throws Exception {
+ TypeStore store = store(MIXED);
+
+ assertNull(store.find_element_user(Z, 0));
+ assertNull(store.find_element_user(NO_NS_A, 0));
+ }
+
+ @Test
+ void byNameANegativeIndexResolvesToTheFirstMatch() throws Exception {
+ // not obviously the right answer, but it is what this method has always done
+ TypeStore store = store(MIXED);
+
+ assertEquals("a0", text(store.find_element_user(A, -1)));
+ assertEquals("a0", text(store.find_element_user(A, -100)));
+ assertEquals("b0", text(store.find_element_user(B, -1)));
+ assertNull(store.find_element_user(Z, -1));
+ }
+
+ @Test
+ void byNameReturnsTheSameUserEveryTime() throws Exception {
+ TypeStore store = store(MIXED);
+
+ TypeStoreUser first = store.find_element_user(A, 2);
+ assertNotNull(first);
+ assertSame(first, store.find_element_user(A, 2));
+ assertSame(first, store.find_element_user(A, 2));
+
+ // and after a lookup that moves the cursor elsewhere
+ store.find_element_user(A, 0);
+ store.find_element_user(B, 1);
+ assertSame(first, store.find_element_user(A, 2));
+ }
+
+ @Test
+ void byNameFindsTheSameElementWhateverOrderTheIndexesComeIn() throws Exception {
+ int n = 200;
+ TypeStore store = store(repeated("a", n));
+
+ for (int i = 0; i < n; i++) {
+ assertEquals("a" + i, text(store.find_element_user(A, i)));
+ }
+
+ for (int i = n - 1; i >= 0; i--) {
+ assertEquals("a" + i, text(store.find_element_user(A, i)));
+ }
+
+ for (int i : shuffled(n)) {
+ assertEquals("a" + i, text(store.find_element_user(A, i)));
+ }
+
+ for (int i = 0; i < n / 2; i++) {
+ assertEquals("a" + i, text(store.find_element_user(A, i)));
+ assertEquals("a" + (n - 1 - i), text(store.find_element_user(A, n - 1 - i)));
+ }
+ }
+
+ @Test
+ void byNameOnAnElementWithNoElementChildren() throws Exception {
+ XmlObject doc = XmlObject.Factory.parse("just text");
+ TypeStore store = store(doc, "root");
+
+ assertNull(store.find_element_user(A, 0));
+ assertNull(store.find_element_user(A, 1));
+ assertNull(store.find_element_user(A, -1));
+ }
+
+ // ---- find_element_user(QNameSet, int) ----
+
+ @Test
+ void bySetFindsTheNthElementMatchingTheSet() throws Exception {
+ TypeStore store = store(MIXED);
+
+ // a0 a1 c0 a2 a3 in document order
+ assertEquals("a0", text(store.find_element_user(AC, 0)));
+ assertEquals("a1", text(store.find_element_user(AC, 1)));
+ assertEquals("c0", text(store.find_element_user(AC, 2)));
+ assertEquals("a2", text(store.find_element_user(AC, 3)));
+ assertEquals("a3", text(store.find_element_user(AC, 4)));
+ assertNull(store.find_element_user(AC, 5));
+
+ // every element child
+ String[] all = {"a0", "b0", "a1", "c0", "a2", "b1", "a3"};
+ for (int i = 0; i < all.length; i++) {
+ assertEquals(all[i], text(store.find_element_user(ABC, i)));
+ }
+ assertNull(store.find_element_user(ABC, all.length));
+ }
+
+ @Test
+ void bySetANegativeIndexResolvesToTheFirstMatch() throws Exception {
+ TypeStore store = store(MIXED);
+
+ assertEquals("a0", text(store.find_element_user(AC, -1)));
+ assertEquals("b0", text(store.find_element_user(JUST_B, -3)));
+ }
+
+ @Test
+ void bySetReturnsNullWhenNothingMatches() throws Exception {
+ TypeStore store = store(MIXED);
+
+ assertNull(store.find_element_user(QNameSet.forArray(new QName[]{Z}), 0));
+ assertNull(store.find_element_user(QNameSet.EMPTY, 0));
+ }
+
+ @Test
+ void bySetFindsTheSameElementWhateverOrderTheIndexesComeIn() throws Exception {
+ int n = 200;
+ TypeStore store = store(repeated("a", n));
+
+ for (int i = 0; i < n; i++) {
+ assertEquals("a" + i, text(store.find_element_user(AC, i)));
+ }
+
+ for (int i = n - 1; i >= 0; i--) {
+ assertEquals("a" + i, text(store.find_element_user(AC, i)));
+ }
+
+ for (int i : shuffled(n)) {
+ assertEquals("a" + i, text(store.find_element_user(AC, i)));
+ }
+ }
+
+ // ---- the two of them together ----
+
+ @Test
+ void lookupsByNameAndBySetDoNotShareACachedPosition() throws Exception {
+ TypeStore store = store(MIXED);
+
+ String[] bySet = {"a0", "b0", "a1", "c0", "a2", "b1", "a3"};
+ String[] byName = {"a0", "a1", "a2", "a3"};
+
+ for (int round = 0; round < 3; round++) {
+ for (int i = 0; i < bySet.length; i++) {
+ assertEquals(bySet[i], text(store.find_element_user(ABC, i)));
+ assertEquals(byName[i % byName.length], text(store.find_element_user(A, i % byName.length)));
+ assertEquals("b" + (i % 2), text(store.find_element_user(B, i % 2)));
+ assertEquals("b" + (i % 2), text(store.find_element_user(JUST_B, i % 2)));
+ }
+ }
+ }
+
+ @Test
+ void twoDifferentSetsOverTheSameParentDoNotShareACachedPosition() throws Exception {
+ TypeStore store = store(MIXED);
+
+ for (int round = 0; round < 3; round++) {
+ assertEquals("c0", text(store.find_element_user(AC, 2)));
+ assertEquals("a1", text(store.find_element_user(ABC, 2)));
+ assertEquals("b1", text(store.find_element_user(JUST_B, 1)));
+ assertEquals("a3", text(store.find_element_user(AC, 4)));
+ assertEquals("b0", text(store.find_element_user(ABC, 1)));
+ }
+ }
+
+ @Test
+ void lookupsAgainstTwoParentsDoNotDisturbEachOther() throws Exception {
+ String xml =
+ "" +
+ "left0left1left2" +
+ "right0right1right2" +
+ "";
+
+ XmlObject outer = XmlObject.Factory.parse(xml).selectChildren(new QName(NS, "outer"))[0];
+ XmlObject[] roots = outer.selectChildren(new QName(NS, "root"));
+ assertEquals(2, roots.length);
+
+ TypeStore left = ((TypeStoreUser) roots[0]).get_store();
+ TypeStore right = ((TypeStoreUser) roots[1]).get_store();
+
+ for (int round = 0; round < 3; round++) {
+ for (int i = 0; i < 3; i++) {
+ assertEquals("left" + i, text(left.find_element_user(A, i)));
+ assertEquals("right" + i, text(right.find_element_user(A, i)));
+ assertEquals("left" + (2 - i), text(left.find_element_user(AC, 2 - i)));
+ assertEquals("right" + (2 - i), text(right.find_element_user(AC, 2 - i)));
+ }
+ }
+ }
+
+ // ---- and after the document changes ----
+
+ @Test
+ void followsElementsBeingRemoved() throws Exception {
+ TypeStore store = store(repeated("a", 6));
+
+ assertEquals("a3", text(store.find_element_user(A, 3)));
+
+ store.remove_element(A, 0);
+ assertEquals("a1", text(store.find_element_user(A, 0)));
+ assertEquals("a4", text(store.find_element_user(A, 3)));
+
+ store.remove_element(A, 4);
+ assertNull(store.find_element_user(A, 4));
+ assertEquals("a4", text(store.find_element_user(AC, 3)));
+ }
+
+ @Test
+ void followsElementsBeingInserted() throws Exception {
+ TypeStore store = store(repeated("a", 4));
+
+ assertEquals("a2", text(store.find_element_user(A, 2)));
+
+ TypeStoreUser inserted = store.insert_element_user(A, 2);
+ assertNotNull(inserted);
+ assertSame(inserted, store.find_element_user(A, 2));
+ assertEquals("a2", text(store.find_element_user(A, 3)));
+ assertEquals("a3", text(store.find_element_user(A, 4)));
+ assertNull(store.find_element_user(A, 5));
+
+ // and the set based lookup sees it in the same place
+ assertSame(inserted, store.find_element_user(AC, 2));
+ assertEquals("a3", text(store.find_element_user(AC, 4)));
+ }
+
+ @Test
+ void followsElementsBeingRenamedAroundIt() throws Exception {
+ TypeStore store = store(repeated("a", 4));
+
+ assertEquals("a1", text(store.find_element_user(A, 1)));
+
+ try (XmlCursor cursor = ((XmlObject) store.find_element_user(A, 1)).newCursor()) {
+ cursor.setName(B);
+ }
+
+ assertEquals("a2", text(store.find_element_user(A, 1)));
+ assertEquals("a1", text(store.find_element_user(B, 0)));
+ assertEquals("a1", text(store.find_element_user(ABC, 1)));
+ assertNull(store.find_element_user(A, 3));
+ }
+}
diff --git a/src/test/java/org/apache/xmlbeans/impl/values/JavaListIteratorTest.java b/src/test/java/org/apache/xmlbeans/impl/values/JavaListIteratorTest.java
new file mode 100644
index 000000000..e8222d9d9
--- /dev/null
+++ b/src/test/java/org/apache/xmlbeans/impl/values/JavaListIteratorTest.java
@@ -0,0 +1,281 @@
+/* Copyright 2004 The Apache Software Foundation
+ *
+ * Licensed 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
+ *
+ * http://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.xmlbeans.impl.values;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.NoSuchElementException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Covers the iterators handed out by the lists that the generated getXxxList()
+ * accessors return. The list itself is backed by an ArrayList here so that the
+ * calls it makes can be counted - against the XML store, size() is a walk over
+ * all of the children of the parent element.
+ */
+class JavaListIteratorTest {
+ private final List backing = new ArrayList<>();
+ private int sizeCalls;
+
+ private JavaListObject list() {
+ return new JavaListObject<>(
+ backing::get,
+ backing::set,
+ backing::add,
+ i -> backing.remove((int) i),
+ () -> {
+ sizeCalls++;
+ return backing.size();
+ }
+ );
+ }
+
+ private void fill(String... values) {
+ backing.addAll(Arrays.asList(values));
+ }
+
+ @Test
+ void aPassOverTheListAsksForTheSizeAConstantNumberOfTimes() {
+ for (int i = 0; i < 100; i++) {
+ backing.add("v" + i);
+ }
+
+ List list = list();
+ sizeCalls = 0;
+
+ int seen = 0;
+ for (String value : list) {
+ assertEquals("v" + seen++, value);
+ }
+
+ assertEquals(100, seen);
+ // once to find the end of the list, once to confirm it - not one per element
+ assertEquals(2, sizeCalls);
+ }
+
+ @Test
+ void iterationSeesElementsAppendedWhileItIsRunning() {
+ fill("a", "b", "c");
+
+ List seen = new ArrayList<>();
+ for (String value : list()) {
+ seen.add(value);
+ if (seen.size() == 1) {
+ backing.add("d");
+ }
+ }
+
+ assertEquals(Arrays.asList("a", "b", "c", "d"), seen);
+ }
+
+ @Test
+ void iteratorRemoveDropsTheElementAndCarriesOn() {
+ fill("a", "b", "c", "d");
+
+ List seen = new ArrayList<>();
+ for (Iterator it = list().iterator(); it.hasNext(); ) {
+ String value = it.next();
+ seen.add(value);
+ if ("b".equals(value) || "d".equals(value)) {
+ it.remove();
+ }
+ }
+
+ assertEquals(Arrays.asList("a", "b", "c", "d"), seen);
+ assertEquals(Arrays.asList("a", "c"), backing);
+ }
+
+ @Test
+ void nextThrowsOnceTheListIsExhausted() {
+ fill("a");
+
+ Iterator it = list().iterator();
+ assertEquals("a", it.next());
+ assertFalse(it.hasNext());
+ assertThrows(NoSuchElementException.class, it::next);
+ }
+
+ @Test
+ void listIteratorWalksInBothDirections() {
+ fill("a", "b", "c");
+
+ ListIterator it = list().listIterator();
+ assertEquals(-1, it.previousIndex());
+ assertEquals("a", it.next());
+ assertEquals("b", it.next());
+ assertEquals(2, it.nextIndex());
+ assertEquals("b", it.previous());
+ assertEquals("a", it.previous());
+ assertFalse(it.hasPrevious());
+ assertThrows(NoSuchElementException.class, it::previous);
+ }
+
+ @Test
+ void listIteratorSetsAndAdds() {
+ fill("a", "b");
+
+ ListIterator it = list().listIterator();
+ it.next();
+ it.set("z");
+ it.add("y");
+
+ assertEquals(Arrays.asList("z", "y", "b"), backing);
+ assertEquals(2, it.nextIndex());
+ assertEquals("b", it.next());
+ }
+
+ @Test
+ void listIteratorRejectsAnIndexOutsideTheList() {
+ fill("a", "b");
+
+ List list = list();
+ assertThrows(IndexOutOfBoundsException.class, () -> list.listIterator(-1));
+ assertThrows(IndexOutOfBoundsException.class, () -> list.listIterator(3));
+ assertEquals(2, list.listIterator(2).nextIndex());
+ }
+
+ @Test
+ void theListStillCompares() {
+ fill("a", "b", "c");
+
+ List list = list();
+ assertEquals(1, list.indexOf("b"));
+ assertEquals(2, list.lastIndexOf("c"));
+ assertTrue(list.contains("c"));
+ assertEquals(Arrays.asList("a", "b", "c"), list);
+ assertEquals(Arrays.asList("a", "b", "c").hashCode(), list.hashCode());
+ }
+
+ @Test
+ void walkingBackwardsNeverAsksForTheSize() {
+ fill("a", "b", "c");
+
+ ListIterator it = list().listIterator(3);
+ sizeCalls = 0;
+
+ assertEquals("c", it.previous());
+ assertEquals("b", it.previous());
+ assertEquals("a", it.previous());
+ assertFalse(it.hasPrevious());
+ assertEquals(0, sizeCalls);
+ }
+
+ @Test
+ void anEmptyListHasNothingToIterate() {
+ Iterator it = list().iterator();
+ assertFalse(it.hasNext());
+ assertThrows(NoSuchElementException.class, it::next);
+
+ ListIterator listIt = list().listIterator();
+ assertFalse(listIt.hasNext());
+ assertFalse(listIt.hasPrevious());
+ assertEquals(0, listIt.nextIndex());
+ assertEquals(-1, listIt.previousIndex());
+ }
+
+ @Test
+ void aListIteratorAtTheEndHasAPreviousButNoNext() {
+ fill("a", "b");
+
+ ListIterator it = list().listIterator(2);
+ assertFalse(it.hasNext());
+ assertTrue(it.hasPrevious());
+ assertEquals(2, it.nextIndex());
+ assertEquals(1, it.previousIndex());
+ assertThrows(NoSuchElementException.class, it::next);
+ assertEquals("b", it.previous());
+ }
+
+ @Test
+ void removeBeforeAnythingHasBeenReturnedIsRejected() {
+ fill("a", "b");
+
+ ListIterator it = list().listIterator();
+ assertThrows(IllegalStateException.class, it::remove);
+ assertThrows(IllegalStateException.class, () -> it.set("z"));
+ }
+
+ @Test
+ void removeTwiceInARowIsRejected() {
+ fill("a", "b");
+
+ Iterator it = list().iterator();
+ it.next();
+ it.remove();
+ assertThrows(IllegalStateException.class, it::remove);
+ assertEquals(Arrays.asList("b"), backing);
+ }
+
+ @Test
+ void setAfterRemoveIsRejected() {
+ fill("a", "b");
+
+ ListIterator it = list().listIterator();
+ it.next();
+ it.remove();
+ assertThrows(IllegalStateException.class, () -> it.set("z"));
+ }
+
+ @Test
+ void setAfterAddIsRejected() {
+ fill("a", "b");
+
+ ListIterator it = list().listIterator();
+ it.next();
+ it.add("inserted");
+ assertThrows(IllegalStateException.class, () -> it.set("z"));
+ assertEquals(Arrays.asList("a", "inserted", "b"), backing);
+ }
+
+ @Test
+ void removeAfterPreviousDropsThatElement() {
+ fill("a", "b", "c");
+
+ ListIterator it = list().listIterator(3);
+ assertEquals("c", it.previous());
+ it.remove();
+ assertEquals(Arrays.asList("a", "b"), backing);
+ assertEquals("b", it.previous());
+ assertEquals(1, it.nextIndex());
+ }
+
+ @Test
+ void aListBuiltWithoutASizerCannotBeIterated() {
+ fill("a");
+
+ JavaListObject list = new JavaListObject<>(backing::get, null, null, null, null);
+ assertThrows(IllegalStateException.class, () -> list.iterator().hasNext());
+ }
+
+ @Test
+ void aListBuiltWithoutAGetterCannotBeIterated() {
+ fill("a");
+
+ JavaListObject list = new JavaListObject<>(null, null, null, null, backing::size);
+ Iterator it = list.iterator();
+ assertTrue(it.hasNext());
+ assertThrows(IllegalStateException.class, it::next);
+ }
+}
diff --git a/src/test/java/xmlobject/checkin/IndexedElementAccessTest.java b/src/test/java/xmlobject/checkin/IndexedElementAccessTest.java
new file mode 100644
index 000000000..8259a3b00
--- /dev/null
+++ b/src/test/java/xmlobject/checkin/IndexedElementAccessTest.java
@@ -0,0 +1,218 @@
+/* Copyright 2004 The Apache Software Foundation
+ *
+ * Licensed 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
+ *
+ * http://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 xmlobject.checkin;
+
+import com.easypo.XmlLineItemBean;
+import com.easypo.XmlPurchaseOrderDocumentBean;
+import com.easypo.XmlPurchaseOrderDocumentBean.PurchaseOrder;
+import org.apache.xmlbeans.XmlCursor;
+import org.apache.xmlbeans.XmlException;
+import org.junit.jupiter.api.Test;
+import org.openuri.sgs.RootDocument;
+
+import javax.xml.namespace.QName;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Random;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The indexed getXxxArray(i) accessors resolve an index by position within the
+ * children of the parent element. These check that they land on the right
+ * element whatever order the indexes are asked for in, and that they notice
+ * when the document changes underneath them.
+ */
+class IndexedElementAccessTest {
+ private static final String SGS = "http://openuri.org/sgs";
+ private static final int COUNT = 64;
+
+ private static PurchaseOrder order(int items) throws XmlException {
+ StringBuilder xml = new StringBuilder("");
+ for (int i = 0; i < items; i++) {
+ xml.append("item").append(i).append("");
+ }
+ xml.append("");
+
+ return XmlPurchaseOrderDocumentBean.Factory.parse(xml.toString()).getPurchaseOrder();
+ }
+
+ /** A root whose children cycle through the A/B/C substitution group, so the accessor matches on a QNameSet. */
+ private static RootDocument.Root substitutionGroupRoot(int children) throws XmlException {
+ String[] names = {"A", "B", "C"};
+ StringBuilder xml = new StringBuilder("");
+ for (int i = 0; i < children; i++) {
+ String name = names[i % names.length];
+ xml.append('<').append(name).append('>').append("v").append(i).append("").append(name).append('>');
+ }
+ xml.append("");
+
+ return RootDocument.Factory.parse(xml.toString()).getRoot();
+ }
+
+ private static int[] shuffled(int n) {
+ int[] order = new int[n];
+ for (int i = 0; i < n; i++) {
+ order[i] = i;
+ }
+
+ Random random = new Random(42);
+ for (int i = n - 1; i > 0; i--) {
+ int j = random.nextInt(i + 1);
+ int t = order[i];
+ order[i] = order[j];
+ order[j] = t;
+ }
+
+ return order;
+ }
+
+ @Test
+ void indexedAccessAgreesWithTheBulkArrayInAnyOrder() throws Exception {
+ PurchaseOrder po = order(COUNT);
+ assertEquals(COUNT, po.sizeOfLineItemArray());
+
+ for (int i = 0; i < COUNT; i++) {
+ assertEquals("item" + i, po.getLineItemArray(i).getDescription());
+ }
+
+ for (int i = COUNT - 1; i >= 0; i--) {
+ assertEquals("item" + i, po.getLineItemArray(i).getDescription());
+ }
+
+ for (int i : shuffled(COUNT)) {
+ assertEquals("item" + i, po.getLineItemArray(i).getDescription());
+ }
+
+ // the same index twice in a row, and the two ends alternating
+ assertEquals("item7", po.getLineItemArray(7).getDescription());
+ assertEquals("item7", po.getLineItemArray(7).getDescription());
+ for (int i = 0; i < 8; i++) {
+ assertEquals("item" + i, po.getLineItemArray(i).getDescription());
+ assertEquals("item" + (COUNT - 1 - i), po.getLineItemArray(COUNT - 1 - i).getDescription());
+ }
+ }
+
+ @Test
+ void indexedAccessFollowsTheDocumentAsItChanges() throws Exception {
+ PurchaseOrder po = order(8);
+ assertEquals("item3", po.getLineItemArray(3).getDescription());
+
+ po.removeLineItem(0);
+ assertEquals("item4", po.getLineItemArray(3).getDescription());
+
+ po.insertNewLineItem(0).setDescription("head");
+ assertEquals("head", po.getLineItemArray(0).getDescription());
+ assertEquals("item3", po.getLineItemArray(3).getDescription());
+
+ po.getLineItemArray(2).setDescription("changed");
+ assertEquals("changed", po.getLineItemArray(2).getDescription());
+ }
+
+ @Test
+ void aNegativeIndexStillResolvesToTheFirstElement() throws Exception {
+ // not obviously the right answer, but it is what this accessor has always done
+ PurchaseOrder po = order(4);
+ assertEquals("item0", po.getLineItemArray(-1).getDescription());
+ }
+
+ @Test
+ void substitutionGroupAccessorsAreIndexedCorrectly() throws Exception {
+ RootDocument.Root root = substitutionGroupRoot(COUNT);
+ assertEquals(COUNT, root.sizeOfAArray());
+
+ for (int i = 0; i < COUNT; i++) {
+ assertEquals("v" + i, root.getAArray(i));
+ }
+
+ for (int i = COUNT - 1; i >= 0; i--) {
+ assertEquals("v" + i, root.getAArray(i));
+ }
+
+ for (int i : shuffled(COUNT)) {
+ assertEquals("v" + i, root.getAArray(i));
+ }
+ }
+
+ @Test
+ void aLookupByNameDoesNotDisturbALookupBySubstitutionGroup() throws Exception {
+ // the accessor matches any of A/B/C by QNameSet while the cursor matches B by name,
+ // both against the same parent element
+ RootDocument.Root root = substitutionGroupRoot(COUNT);
+ QName b = new QName(SGS, "B");
+
+ // every third child is a B, and they are asked for out of step with the A indexes
+ int bs = (COUNT + 1) / 3;
+
+ try (XmlCursor cursor = root.newCursor()) {
+ for (int i = 0; i < COUNT; i++) {
+ assertEquals("v" + i, root.getAArray(i));
+
+ int nth = i % bs;
+ cursor.push();
+ assertTrue(cursor.toChild(b, nth));
+ assertEquals("v" + (nth * 3 + 1), cursor.getTextValue());
+ cursor.pop();
+ }
+ }
+ }
+
+ @Test
+ void listIterationAgreesWithTheArray() throws Exception {
+ PurchaseOrder po = order(COUNT);
+
+ int i = 0;
+ for (XmlLineItemBean item : po.getLineItemList()) {
+ assertEquals("item" + i++, item.getDescription());
+ }
+
+ assertEquals(COUNT, i);
+ assertEquals(COUNT, po.getLineItemList().size());
+ }
+
+ @Test
+ void listIterationSeesElementsAddedWhileItIsRunning() throws Exception {
+ PurchaseOrder po = order(2);
+
+ List seen = new ArrayList<>();
+ for (XmlLineItemBean item : po.getLineItemList()) {
+ seen.add(item.getDescription());
+ if (seen.size() == 1) {
+ po.addNewLineItem().setDescription("added");
+ }
+ }
+
+ assertEquals(Arrays.asList("item0", "item1", "added"), seen);
+ }
+
+ @Test
+ void removingThroughTheIteratorUpdatesTheDocument() throws Exception {
+ PurchaseOrder po = order(4);
+
+ Iterator it = po.getLineItemList().iterator();
+ assertEquals("item0", it.next().getDescription());
+ assertEquals("item1", it.next().getDescription());
+ it.remove();
+ assertEquals("item2", it.next().getDescription());
+ assertEquals("item3", it.next().getDescription());
+
+ assertEquals(3, po.sizeOfLineItemArray());
+ assertEquals("item2", po.getLineItemArray(1).getDescription());
+ }
+}
diff --git a/src/test/java/xmlobject/checkin/XmlBeanListLiveEditTest.java b/src/test/java/xmlobject/checkin/XmlBeanListLiveEditTest.java
new file mode 100644
index 000000000..600d64317
--- /dev/null
+++ b/src/test/java/xmlobject/checkin/XmlBeanListLiveEditTest.java
@@ -0,0 +1,366 @@
+/* Copyright 2004 The Apache Software Foundation
+ *
+ * Licensed 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
+ *
+ * http://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 xmlobject.checkin;
+
+import com.easypo.XmlLineItemBean;
+import com.easypo.XmlPurchaseOrderDocumentBean;
+import com.easypo.XmlPurchaseOrderDocumentBean.PurchaseOrder;
+import org.apache.xmlbeans.XmlException;
+import org.apache.xmlbeans.XmlObject;
+import org.apache.xmlbeans.XmlOptions;
+import org.junit.jupiter.api.Test;
+import org.openuri.sgs.RootDocument;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The lists handed out by getXxxList() are views over the document rather than
+ * copies of it. Everything written through the list or through one of its iterators
+ * has to land in the store, and anything written to the bean has to show up in a
+ * list that was handed out earlier. Each check reads the change back twice: once
+ * through the typed accessors, and once from the document serialized and parsed
+ * again, which only passes if the edit really reached the store.
+ */
+class XmlBeanListLiveEditTest {
+ private static final String SGS = "http://openuri.org/sgs";
+
+ // ---- line items, a list of XmlObjects (JavaListXmlObject) ----
+
+ private static PurchaseOrder order(String... descriptions) throws XmlException {
+ StringBuilder xml = new StringBuilder("");
+ for (String description : descriptions) {
+ xml.append("").append(description).append("");
+ }
+ xml.append("");
+
+ return XmlPurchaseOrderDocumentBean.Factory.parse(xml.toString()).getPurchaseOrder();
+ }
+
+ /** The element itself rather than just its contents, so that it can be parsed again. */
+ private static String xml(XmlObject o) {
+ return o.xmlText(new XmlOptions().setSaveOuter());
+ }
+
+ private static XmlLineItemBean lineItem(String description) {
+ XmlLineItemBean item = XmlLineItemBean.Factory.newInstance();
+ item.setDescription(description);
+ return item;
+ }
+
+ private static List descriptionsOf(PurchaseOrder po) {
+ List descriptions = new ArrayList<>();
+ for (XmlLineItemBean item : po.getLineItemArray()) {
+ descriptions.add(item.getDescription());
+ }
+ return descriptions;
+ }
+
+ /** Reads the descriptions back from the typed accessors and from the serialized document. */
+ private static void assertDocument(PurchaseOrder po, String... expected) throws XmlException {
+ List wanted = Arrays.asList(expected);
+
+ assertEquals(wanted, descriptionsOf(po));
+ assertEquals(expected.length, po.sizeOfLineItemArray());
+ assertEquals(expected.length, po.getLineItemList().size());
+
+ PurchaseOrder reparsed = XmlPurchaseOrderDocumentBean.Factory.parse(xml(po)).getPurchaseOrder();
+ assertEquals(wanted, descriptionsOf(reparsed), "the edit did not reach the document");
+ }
+
+ @Test
+ void setThroughTheListIsWrittenToTheDocument() throws Exception {
+ PurchaseOrder po = order("i0", "i1", "i2");
+
+ po.getLineItemList().set(1, lineItem("replaced"));
+
+ assertDocument(po, "i0", "replaced", "i2");
+ assertFalse(xml(po).contains("i1"));
+ }
+
+ @Test
+ void addThroughTheListAppendsToTheDocument() throws Exception {
+ PurchaseOrder po = order("i0", "i1");
+
+ assertTrue(po.getLineItemList().add(lineItem("added")));
+
+ assertDocument(po, "i0", "i1", "added");
+ }
+
+ @Test
+ void addAtAnIndexInsertsIntoTheDocument() throws Exception {
+ PurchaseOrder po = order("i0", "i1", "i2");
+
+ po.getLineItemList().add(1, lineItem("inserted"));
+
+ assertDocument(po, "i0", "inserted", "i1", "i2");
+ }
+
+ @Test
+ void addAllThroughTheListIsWrittenToTheDocument() throws Exception {
+ PurchaseOrder po = order("i0");
+
+ po.getLineItemList().addAll(Arrays.asList(lineItem("x"), lineItem("y")));
+
+ assertDocument(po, "i0", "x", "y");
+ }
+
+ @Test
+ void removeThroughTheListDeletesFromTheDocument() throws Exception {
+ PurchaseOrder po = order("i0", "i1", "i2");
+
+ po.getLineItemList().remove(1);
+
+ assertDocument(po, "i0", "i2");
+ assertFalse(xml(po).contains("i1"));
+ }
+
+ @Test
+ void clearThroughTheListEmptiesTheDocument() throws Exception {
+ PurchaseOrder po = order("i0", "i1", "i2", "i3");
+
+ po.getLineItemList().clear();
+
+ assertDocument(po);
+ assertFalse(xml(po).contains("line-item"));
+ }
+
+ @Test
+ void iteratorRemoveIsWrittenToTheDocument() throws Exception {
+ PurchaseOrder po = order("keep0", "drop0", "keep1", "drop1", "keep2");
+
+ for (Iterator it = po.getLineItemList().iterator(); it.hasNext(); ) {
+ if (it.next().getDescription().startsWith("drop")) {
+ it.remove();
+ }
+ }
+
+ assertDocument(po, "keep0", "keep1", "keep2");
+ assertFalse(xml(po).contains("drop"));
+ }
+
+ @Test
+ void listIteratorSetIsWrittenToTheDocument() throws Exception {
+ PurchaseOrder po = order("i0", "i1", "i2");
+
+ ListIterator it = po.getLineItemList().listIterator();
+ it.next();
+ it.next();
+ it.set(lineItem("second"));
+
+ assertDocument(po, "i0", "second", "i2");
+ }
+
+ @Test
+ void listIteratorAddIsWrittenToTheDocument() throws Exception {
+ PurchaseOrder po = order("i0", "i1", "i2");
+
+ ListIterator it = po.getLineItemList().listIterator();
+ it.next();
+ it.add(lineItem("between"));
+
+ // the added element goes before the cursor, so the walk carries on with i1
+ assertEquals("i1", it.next().getDescription());
+ assertDocument(po, "i0", "between", "i1", "i2");
+ }
+
+ @Test
+ void listIteratorSetAfterPreviousWritesToThatElement() throws Exception {
+ PurchaseOrder po = order("i0", "i1", "i2");
+
+ ListIterator it = po.getLineItemList().listIterator();
+ it.next();
+ it.next();
+ assertEquals("i1", it.previous().getDescription());
+ it.set(lineItem("backwards"));
+
+ assertDocument(po, "i0", "backwards", "i2");
+ }
+
+ @Test
+ void theListIsAViewOfTheDocumentNotACopyOfIt() throws Exception {
+ PurchaseOrder po = order("i0", "i1", "i2");
+ List list = po.getLineItemList();
+
+ po.addNewLineItem().setDescription("late");
+ assertEquals(4, list.size());
+ assertEquals("late", list.get(3).getDescription());
+
+ po.removeLineItem(0);
+ assertEquals(3, list.size());
+ assertEquals("i1", list.get(0).getDescription());
+
+ po.insertNewLineItem(0).setDescription("head");
+ assertEquals(4, list.size());
+ assertEquals("head", list.get(0).getDescription());
+
+ po.getLineItemArray(1).setDescription("edited");
+ assertEquals("edited", list.get(1).getDescription());
+
+ List seen = new ArrayList<>();
+ for (XmlLineItemBean item : list) {
+ seen.add(item.getDescription());
+ }
+ assertEquals(Arrays.asList("head", "edited", "i2", "late"), seen);
+ }
+
+ @Test
+ void twoListsOverTheSameBeanSeeEachOthersEdits() throws Exception {
+ PurchaseOrder po = order("i0", "i1", "i2");
+ List first = po.getLineItemList();
+ List second = po.getLineItemList();
+
+ first.remove(0);
+ assertEquals(2, second.size());
+ assertEquals("i1", second.get(0).getDescription());
+
+ second.add(lineItem("fromSecond"));
+ assertEquals(3, first.size());
+ assertEquals("fromSecond", first.get(2).getDescription());
+
+ assertDocument(po, "i1", "i2", "fromSecond");
+ }
+
+ @Test
+ void anEditMadeDuringIterationIsSeenByThatIterator() throws Exception {
+ PurchaseOrder po = order("i0", "i1", "i2", "i3");
+
+ List seen = new ArrayList<>();
+ for (XmlLineItemBean item : po.getLineItemList()) {
+ seen.add(item.getDescription());
+ if (seen.size() == 1) {
+ // an element the iterator has not reached yet
+ po.getLineItemArray(2).setDescription("edited");
+ }
+ }
+
+ assertEquals(Arrays.asList("i0", "i1", "edited", "i3"), seen);
+ }
+
+ // ---- substitution group values, a list of Strings (JavaListObject) ----
+
+ private static RootDocument.Root root(String... values) throws XmlException {
+ String[] names = {"A", "B", "C"};
+ StringBuilder xml = new StringBuilder("");
+ for (int i = 0; i < values.length; i++) {
+ String name = names[i % names.length];
+ xml.append('<').append(name).append('>').append(values[i]).append("").append(name).append('>');
+ }
+ xml.append("");
+
+ return RootDocument.Factory.parse(xml.toString()).getRoot();
+ }
+
+ private static void assertDocument(RootDocument.Root root, String... expected) throws XmlException {
+ List wanted = Arrays.asList(expected);
+
+ assertEquals(wanted, Arrays.asList(root.getAArray()));
+ assertEquals(expected.length, root.sizeOfAArray());
+ assertEquals(wanted, root.getAList());
+
+ RootDocument.Root reparsed = RootDocument.Factory.parse(xml(root)).getRoot();
+ assertEquals(wanted, Arrays.asList(reparsed.getAArray()), "the edit did not reach the document");
+ }
+
+ @Test
+ void setThroughTheValueListIsWrittenToTheDocument() throws Exception {
+ RootDocument.Root root = root("v0", "v1", "v2", "v3");
+
+ assertEquals("v1", root.getAList().set(1, "changed"));
+
+ assertDocument(root, "v0", "changed", "v2", "v3");
+ }
+
+ @Test
+ void addThroughTheValueListAppendsToTheDocument() throws Exception {
+ RootDocument.Root root = root("v0", "v1");
+
+ assertTrue(root.getAList().add("v2"));
+
+ assertDocument(root, "v0", "v1", "v2");
+ }
+
+ @Test
+ void addAtAnIndexInsertsIntoTheValueListsDocument() throws Exception {
+ RootDocument.Root root = root("v0", "v1", "v2");
+
+ root.getAList().add(1, "inserted");
+
+ assertDocument(root, "v0", "inserted", "v1", "v2");
+ }
+
+ @Test
+ void removeThroughTheValueListDeletesFromTheDocument() throws Exception {
+ RootDocument.Root root = root("v0", "v1", "v2");
+
+ assertEquals("v1", root.getAList().remove(1));
+
+ assertDocument(root, "v0", "v2");
+ assertFalse(xml(root).contains("v1"));
+ }
+
+ @Test
+ void iteratorRemoveOnTheValueListIsWrittenToTheDocument() throws Exception {
+ RootDocument.Root root = root("keep0", "drop0", "keep1", "drop1");
+
+ for (Iterator it = root.getAList().iterator(); it.hasNext(); ) {
+ if (it.next().startsWith("drop")) {
+ it.remove();
+ }
+ }
+
+ assertDocument(root, "keep0", "keep1");
+ assertFalse(xml(root).contains("drop"));
+ }
+
+ @Test
+ void listIteratorSetAndAddOnTheValueListAreWrittenToTheDocument() throws Exception {
+ RootDocument.Root root = root("v0", "v1", "v2");
+
+ ListIterator it = root.getAList().listIterator();
+ it.next();
+ it.set("first");
+ it.add("second");
+ assertEquals("v1", it.next());
+
+ assertDocument(root, "first", "second", "v1", "v2");
+ }
+
+ @Test
+ void theValueListIsAViewOfTheDocumentNotACopyOfIt() throws Exception {
+ RootDocument.Root root = root("v0", "v1", "v2");
+ List list = root.getAList();
+
+ root.setAArray(0, "direct");
+ assertEquals("direct", list.get(0));
+
+ root.insertA(0, "head");
+ assertEquals(4, list.size());
+ assertEquals("head", list.get(0));
+ assertEquals(Arrays.asList("head", "direct", "v1", "v2"), new ArrayList<>(list));
+
+ root.removeA(3);
+ assertEquals(3, list.size());
+ assertEquals(Arrays.asList("head", "direct", "v1"), new ArrayList<>(list));
+ }
+}