package org.exist.fluent; import java.util.*; import org.exist.dom.AVLTreeNodeSet; import org.exist.xquery.*; import org.exist.xquery.value.*; /** * The result of a query on the database, holding a collection of items. * The items can be accessed either as structured resources or as string * values. It is also possible to further refine the query by executing queries * within the context of the results. * * @author <a href="mailto:piotr@ideanest.com">Piotr Kaminski</a> * @version $Revision: 1.17 $ ($Date: 2006/08/14 23:18:22 $) */ public class ItemList extends Resource implements Iterable<Item> { /** * A facet that treats each item in the list as its effective string value. Atomic values * are converted to strings, while nodes are converted to the concatenation of all their * text descendants (note: not serialized!). */ public class ValuesFacet implements Iterable<String> { private ValuesFacet() {} /** * Return an iterator over the effective string values of the item list. * * @return a string value iterator */ public Iterator<String> iterator() { return new Iterator<String>() { private final Iterator<Item> delegate = ItemList.this.iterator(); public boolean hasNext() { return delegate.hasNext();} public String next() {return delegate.next().value();} public void remove() {throw new UnsupportedOperationException();} }; } private ItemList itemList() { return ItemList.this; } @Override public boolean equals(Object o) { return (o instanceof ValuesFacet && ItemList.this.equals(((ValuesFacet) o).itemList())); } @Override public int hashCode() { return ItemList.this.hashCode() + 2; } /** * Return an unmodifiable list view over the effective string values of the item list. * * @return a list view */ public List<String> asList() { return new AbstractList<String>() { @Override public String get(int index) {return items.get(index).value();} @Override public int size() {return items.size();} }; } /** * Convert the list of effective string values to an array. * * @return an array of effective string values */ public String[] toArray() { return asList().toArray(new String[size()]); } /** * Convert the list of effective string values to an array. If the supplied array is sufficient * for holding the strings, use it; if it's larger than necessary, put a <code>null</code> after * the end of the list. If the array is too small, allocate a new one. * * @param a an array to fill with effective string values * @return an array of effective string values */ public String[] toArray(String[] a) { return asList().toArray(a); } @Override public String toString() { return ItemList.this.toString(); } } /** * A facet that treats each item in the list as a node. If an operation accesses an item that * is not a node, it will throw a <code>DatabaseException</code>. */ public class NodesFacet implements Iterable<Node> { private NodesFacet() {} /** * Return an iterator over the list of nodes. * * @return an iterator over the list of nodes */ public Iterator<Node> iterator() { return new Iterator<Node>() { private final Iterator<Item> delegate = ItemList.this.iterator(); public boolean hasNext() {return delegate.hasNext();} public Node next() { try { return (Node) delegate.next(); } catch (ClassCastException e) { throw new DatabaseException("item is not a node"); } } public void remove() {throw new UnsupportedOperationException();} }; } /** * Return the set of documents to which the nodes in this list belong. * * @return the set of documents convering the nodes in the list */ public Set<XMLDocument> documents() { Set<XMLDocument> docs = new HashSet<XMLDocument>(); for (Node node : this) { try { docs.add(node.document()); } catch (UnsupportedOperationException e) { // ignore, must be a non-persistent node } } return docs; } private ItemList itemList() { return ItemList.this; } @Override public boolean equals(Object o) { return (o instanceof NodesFacet && ItemList.this.equals(((NodesFacet) o).itemList())); } @Override public int hashCode() { return ItemList.this.hashCode() + 1; } /** * Return an unmodifiable list view over the list of nodes. * * @return a list view */ public List<Node> asList() { return new AbstractList<Node>() { @Override public Node get(int index) { try { return (Node) items.get(index); } catch (ClassCastException e) { throw new DatabaseException("item is not a node"); } } @Override public int size() { return items.size(); } }; } /** * Convert the list of nodes to an array. * * @return an array of nodes */ public Node[] toArray() { return asList().toArray(new Node[size()]); } /** * Convert the list of nodes to an array. If the given array is large enough, fill it; if it's * larger than necessary, put a <code>null</code> marker after the end of the list. If the * array is not large enough, allocate a new one. * * @param a the array to fill with the list of nodes * @return an array of nodes */ public Node[] toArray(Node[] a) { return asList().toArray(a); } @Override public String toString() { StringBuilder buf = new StringBuilder(); for (Node node : this) buf.append(node).append('\n'); return buf.toString(); } } private Sequence seq; private List<Item> items, modifiableItems; private ValuesFacet values; private NodesFacet nodes; private ItemList() { super(null, null); this.seq = Sequence.EMPTY_SEQUENCE; this.items = this.modifiableItems = Collections.emptyList(); } ItemList(Sequence seq, NamespaceMap namespaceBindings, Database db) { super(namespaceBindings, db); this.seq = seq; modifiableItems = new ArrayList<Item>(seq.getItemCount()); try { for (SequenceIterator it = seq.iterate(); it.hasNext(); ) { org.exist.xquery.value.Item existItem = it.nextItem(); if (existItem instanceof NodeValue) { modifiableItems.add(new Node((NodeValue) existItem, namespaceBindings.extend(), db)); } else { modifiableItems.add(new Item(existItem, namespaceBindings.extend(), db)); } } } catch (XPathException xpe) { throw new DatabaseException(xpe); } this.items = Collections.unmodifiableList(modifiableItems); } /** * Remove all deleted nodes from this list. Trying to access the list in a query * context when it contains a deleted node will cause a stale reference exception. * You can call this method whenever you suspect this will be the case, preferably * just prior to access. This will also update the results for all direct access methods * that wouldn't throw an exception, but could return a stale result. * The updates aren't done automatically since they involve tricky synchronization * issues and are expensive when not batched up, and since most clients won't need * this feature. */ public void removeDeletedNodes() { boolean itemsRemoved = false; for (Iterator<Item> it = modifiableItems.iterator(); it.hasNext(); ) { Item item = it.next(); if (item instanceof Node && ((Node) item).staleMarker.stale()) { it.remove(); itemsRemoved = true; } } if (!itemsRemoved) return; // Code inlined from org.exist.xquery.XPathUtil to avoid creating temporary lists boolean nodesOnly = true; for (Item item : items) if (!(item instanceof Node)) {nodesOnly = false; break;} seq = nodesOnly ? new AVLTreeNodeSet() : new ValueSequence(); try { for (Item item : items) seq.add(item.item); } catch (XPathException e) { throw new DatabaseException(e); } } @Override Sequence convertToSequence() { for (Item item : items) if (item instanceof Node) ((Node) item).staleMarker.check(); return seq; } /** * Return the number of elements in this item list. * * @return the number of elements in this item list */ public int size() { return items.size(); } /** * Return whether this item list is empty. * * @return <code>true</code> if this item list has no elements */ public boolean isEmpty() { return items.isEmpty(); } /** * Return the item at the given index in this result. Indexing starts at 0. * * @param index the index of the desired item * @return the item at the given index * @throws IndexOutOfBoundsException if the index is out of bounds */ public Item get(int index) { return items.get(index); } /** * Delete all nodes contained in this item list; skip over any items (values) that * it doesn't make sense to try to delete. */ public void deleteAllNodes() { Transaction tx = Database.requireTransaction(); try { for (Item item : items) if (item instanceof Node) ((Node) item).delete(); tx.commit(); } finally { tx.abortIfIncomplete(); } } @Override public boolean equals(Object o) { if (!(o instanceof ItemList)) return false; return items.equals(((ItemList) o).items); } /** * The hash code computation can be expensive, and the hash codes may not be very well distributed. * You probably shouldn't use item lists in situations where they might get hashed. */ @Override public int hashCode() { int hashCode = 1; for (Item item : items) hashCode = hashCode * 31 + item.hashCode(); return hashCode; } /** * Return an iterator over all the items in this list. * * @return an iterator over this item list */ public Iterator<Item> iterator() { return items.iterator(); } /** * Return an unmodifiable list view over the list of items. * * @return a list view */ public List<Item> asList() { return items; } /** * Convert this list of items to an array. * * @return an array of items */ public Item[] toArray() { return items.toArray(new Item[size()]); } /** * Convert this list of items to an array. If the given array is large enough, fill it; if it's * larger than necessary, put a <code>null</code> marker after the end of the list. If the * array is not large enough, allocate a new one. * * @param a the array to fill with items * @return an array of items */ public Item[] toArray(Item[] a) { return items.toArray(a); } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append("("); boolean first = true; for (Item item : items) { if (first) first = false; else buf.append(", "); buf.append(item); } buf.append(")"); return buf.toString(); } /** * Return a view of this item list as a list of effective string values. Note that no extra * memory is used to present this view; effective string values are computed on demand. * * @return a virtual collection of string values contained in this item list */ public ValuesFacet values() { if (values == null) values = new ValuesFacet(); return values; } /** * Return a view of this item list as a list of nodes. If this list contains any items that are * not nodes, operations on the facet may fail. Note that no extra memory is used to * present this view. * * @return a virtual collection of nodes contained in this item list */ public NodesFacet nodes() { if (nodes == null) nodes = new NodesFacet(); return nodes; } static final ItemList NULL = new ItemList() { @Override public QueryService query() {return QueryService.NULL;} @Override public ValuesFacet values() {return new ValuesFacet() { @Override public Iterator<String> iterator() {return Database.emptyIterator();} // toArray/0 and toArray/1 take care of themselves thanks to size() };} @Override public NodesFacet nodes() {return new NodesFacet() { @Override public Iterator<Node> iterator() {return Database.emptyIterator();} // toArray/0 and toArray/1 take care of themselves thanks to size() };} }; }