/* * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package xmlkit; // -*- mode: java; indent-tabs-mode: nil -*- // XML Implementation packages: import java.util.*; import java.io.Reader; import java.io.Writer; import java.io.OutputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.BufferedReader; import java.io.PrintWriter; import java.io.StringWriter; import java.io.StringReader; import java.io.IOException; import org.xml.sax.XMLReader; import org.xml.sax.InputSource; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import org.xml.sax.Attributes; import org.xml.sax.ext.LexicalHandler; import org.xml.sax.helpers.AttributesImpl; /** * A kit of methods and classes useful for manipulating XML trees in * memory. They are very compact and easy to use. An XML element * occupies six pointers of overhead (like two arrays) plus a pointer * for its name, each attribute name and value, and each sub-element. * Many useful XML operations (or Lisp-like calls) can be accomplished * with a single method call on an element itself. * <p> * There is strong integration with the Java collection classes. * There are viewing and conversion operators to and from various * collection types. Elements directly support list iterators. * Most <tt>List</tt> methods work analogously on elements. * <p> * Because of implementation compromises, these XML trees are less * functional than many standard XML classes. * <ul> * <li>There are no parent or sibling pointers in the tree.</li> * <li>Attribute names are simple strings, with no namespaces.</li> * <li>There is no internal support for schemas or validation.</li> * </ul> * <p> * Here is a summary of functionality in <tt>XMLKit</tt>. * (Overloaded groups of methods are summarized by marking some * arguments optional with their default values. Some overloaded * arguments are marked with their alternative types separated by * a bar "|". Arguments or return values for which a null is * specially significant are marked by an alternative "|null". * Accessors which have corresponding setters are marked * by "/set". Removers which have corresponding retainers are marked * by "/retain".) * <pre> * --- element construction * new Element(int elemCapacity=4), String name="" * new Element(String name, String[] attrs={}, Element[] elems={}, int elemCapacity=4) * new Element(String name, String[] attrs, Object[] elems, int elemCapacity=4) * new Element(Element original) // shallow copy * new Element(String name="", Collection elems) // coercion * * Element shallowCopy() * Element shallowFreeze() // side-effecting * Element deepCopy() * Element deepFreeze() // not side-effecting * * EMPTY // frozen empty anonymous element * void ensureExtraCapacity(int) * void trimToSize() * void sortAttrs() // sort by key * * --- field accessors * String getName()/set * int size() * boolean isEmpty() * boolean isFrozen() * boolean isAnonymous() * int getExtraCapacity()/set * int attrSize() * * --- attribute accessors * String getAttr(int i)/set * String getAttrName(int i) * * String getAttr(String key)/set * List getAttrList(String key)/set * Number getAttrNumber(String key)/set * long getAttrLong(String key)/set * double getAttrDouble(String key)/set * * String getAttr(String key, String dflt=null) * long getAttrLong(String key, long dflt=0) * double getAttrDouble(String key, double dflt=0) * * Element copyAttrsOnly() * Element getAttrs()/set => <em><><key>value</key>...</></em> * void addAttrs(Element attrs) * * void removeAttr(int i) * void clearAttrs() * * --- element accessors * Object get(int i)/set * Object getLast() | null * Object[] toArray() * Element copyContentOnly() * * void add(int i=0, Object subElem) * int addAll(int i=0, Collection | Element elems) * int addContent(int i=0, TokenList|Element|Object|null) * void XMLKit.addContent(TokenList|Element|Object|null, Collection sink|null) * * void clear(int beg=0, int end=size) * void sort(Comparator=contentOrder()) * void reverse() * void shuffle(Random rnd=(anonymous)) * void rotate(int distance) * Object min/max(Comparator=contentOrder()) * * --- text accessors * CharSequence getText()/set * CharSequence getUnmarkedText() * int addText(int i=size, CharSequence) * void trimText(); * * --- views * List asList() // element view * ListIterator iterator() * PrintWriter asWriter() * Map asAttrMap() * Iterable<CharSequence> texts() * Iterable<Element> elements() * Iterable<T> partsOnly(Class<T>) * String[] toStrings() * * --- queries * boolean equals(Element | Object) * int compareTo(Element | Object) * boolean equalAttrs(Element) * int hashCode() * boolean isText() // every sub-elem is CharSequence * boolean hasText() // some sub-elem is CharSequence * * boolean contains(Object) * boolean containsAttr(String) * * int indexOf(Object) * int indexOf(Filter, int fromIndex=0) * int lastIndexOf(Object) * int lastIndexOf(Filter, int fromIndex=size-1) * * int indexOfAttr(String) * * // finders, removers, and replacers do addContent of each filtered value * // (i.e., TokenLists and anonymous Elements are broken out into their parts) * boolean matches(Filter) * * Object find(Filter, int fromIndex=0) * Object findLast(Filter, int fromIndex=size-1) * Element findAll(Filter, int fromIndex=0 & int toIndex=size) * int findAll(Filter, Collection sink | null, int fromIndex=0 & int toIndex=size) * * Element removeAllInTree(Filter)/retain * int findAllInTree(Filter, Collection sink | null) * int countAllInTree(Filter) * Element removeAllInTree(Filter)/retain * int removeAllInTree(Filter, Collection sink | null)/retain * void replaceAllInTree(Filter) * * Element findElement(String name=any) * Element findAllElements(String name=any) * * Element findWithAttr(String key, String value=any) * Element findAllWithAttr(String key, String value=any) * * Element removeElement(String name=any) * Element removeAllElements(String name=any)/retain * * Element removeWithAttr(String key, String value=any) * Element removeAllWithAttr(String key, String value=any)/retain * * //countAll is the same as findAll but with null sink * int countAll(Filter) * int countAllElements(String name=any) * int countAllWithAttr(String key, String value=any) * * void replaceAll(Filter, int fromIndex=0 & int toIndex=size) * void replaceAllInTree(Filter) * void XMLKit.replaceAll(Filter, List target) //if(fx){remove x;addContent fx} * * --- element mutators * boolean remove(Object) * Object remove(int) * Object removeLast() | null * * Object remove(Filter, int fromIndex=0) * Object removeLast(Filter, int fromIndex=size-1) * Element sink = removeAll(Filter, int fromIndex=0 & int toIndex=size)/retain * int count = removeAll(Filter, int fromIndex=0 & int toIndex=size, Collection sink | null)/retain * * Element removeAllElements(String name=any) * * --- attribute mutators * ??int addAllAttrsFrom(Element attrSource) * * --- parsing and printing * void tokenize(String delims=whitespace, returnDelims=false) * void writeTo(Writer) * void writePrettyTo(Writer) * String prettyString() * String toString() * * ContentHandler XMLKit.makeBuilder(Collection sink, tokenizing=false, makeFrozen=false) // for standard XML parser * Element XMLKit.readFrom(Reader, tokenizing=false, makeFrozen=false) * void XMLKit.prettyPrintTo(Writer | OutputStream, Element) * class XMLKit.Printer(Writer) { void print/Recursive(Element) } * void XMLKit.output(Object elem, ContentHandler, LexicalHandler=null) * void XMLKit.writeToken(String, char quote, Writer) * void XMLKit.writeCData(String, Writer) * Number XMLKit.convertToNumber(String, Number dflt=null) * long XMLKit.convertToLong(String, long dflt=0) * double XMLKit.convertToDouble(String, double dflt=0) * * --- filters * XMLKit.ElementFilter { Element filter(Element) } * XMLKit.elementFilter(String name=any | Collection nameSet) * XMLKit.AttrFilter(String key) { boolean test(String value) } * XMLKit.attrFilter(String key, String value=any) * XMLKit.attrFilter(Element matchThis, String key) * XMLKit.classFilter(Class) * XMLKit.textFilter() // matches any CharSequence * XMLKit.specialFilter() // matches any Special element * XMLKit.methodFilter(Method m, Object[] args=null, falseResult=null) * XMLKit.testMethodFilter(Method m, Object[] args=null) * XMLKit.not(Filter) // inverts sense of Filter * XMLKit.and(Filter&Filter | Filter[]) * XMLKit.or(Filter&Filter | Filter[]) * XMLKit.stack(Filter&Filter | Filter[]) // result is (fx && g(fx)) * XMLKit.content(Filter, Collection sink) // copies content to sink * XMLKit.replaceInTree(Filter pre, Filter post=null) // pre-replace else recur * XMLKit.findInTree(Filter pre, Collection sink=null) // pre-find else recur * XMLKit.nullFilter() // ignores input, always returns null (i.e., false) * XMLKit.selfFilter( ) // always returns input (i.e., true) * XMLKit.emptyFilter() // ignores input, always returns EMPTY * XMLKit.constantFilter(Object) // ignores input, always returns constant * * --- misc * Comparator XMLKit.contentOrder() // for comparing/sorting mixed content * Method XMLKit.Element.method(String name) // returns Element method * </pre> * * @author jrose */ public abstract class XMLKit { private XMLKit() { } // We need at least this much slop if the element is to stay unfrozen. static final int NEED_SLOP = 1; static final Object[] noPartsFrozen = {}; static final Object[] noPartsNotFrozen = new Object[NEED_SLOP]; static final String WHITESPACE_CHARS = " \t\n\r\f"; static final String ANON_NAME = new String("*"); // unique copy of "*" public static final class Element implements Comparable<Element>, Iterable<Object> { // Note: Does not implement List, because it has more // significant parts besides its sub-elements. Therefore, // hashCode and equals must be more distinctive than Lists. // <name> of element String name; // number of child elements, in parts[0..size-1] int size; // The parts start with child elements:: {e0, e1, e2, ...}. // Following that are optional filler elements, all null. // Following that are attributes as key/value pairs. // They are in reverse: {...key2, val2, key1, val1, key0, val0}. // Child elements and attr keys and values are never null. Object[] parts; // Build a partially-constructed node. // Caller is responsible for initializing promised attributes. Element(String name, int size, int capacity) { this.name = name.toString(); this.size = size; assert (size <= capacity); this.parts = capacity > 0 ? new Object[capacity] : noPartsFrozen; } /** An anonymous, empty element. * Optional elemCapacity argument is expected number of sub-elements. */ public Element() { this(ANON_NAME, 0, NEED_SLOP + 4); } public Element(int extraCapacity) { this(ANON_NAME, 0, NEED_SLOP + Math.max(0, extraCapacity)); } /** An empty element with the given name. * Optional extraCapacity argument is expected number of sub-elements. */ public Element(String name) { this(name, 0, NEED_SLOP + 4); } public Element(String name, int extraCapacity) { this(name, 0, NEED_SLOP + Math.max(0, extraCapacity)); } /** An empty element with the given name and attributes. * Optional extraCapacity argument is expected number of sub-elements. */ public Element(String name, String... attrs) { this(name, attrs, (Element[]) null, 0); } public Element(String name, String[] attrs, int extraCapacity) { this(name, attrs, (Element[]) null, extraCapacity); } /** An empty element with the given name and sub-elements. * Optional extraCapacity argument is expected extra sub-elements. */ public Element(String name, Element... elems) { this(name, (String[]) null, elems, 0); } public Element(String name, Element[] elems, int extraCapacity) { this(name, (String[]) null, elems, extraCapacity); } /** An empty element with the given name, attributes, and sub-elements. * Optional extraCapacity argument is expected extra sub-elements. */ public Element(String name, String[] attrs, Object... elems) { this(name, attrs, elems, 0); } public Element(String name, String[] attrs, Object[] elems, int extraCapacity) { this(name, 0, ((elems == null) ? 0 : elems.length) + Math.max(0, extraCapacity) + NEED_SLOP + ((attrs == null) ? 0 : attrs.length)); int ne = ((elems == null) ? 0 : elems.length); int na = ((attrs == null) ? 0 : attrs.length); int fillp = 0; for (int i = 0; i < ne; i++) { if (elems[i] != null) { parts[fillp++] = elems[i]; } } size = fillp; for (int i = 0; i < na; i += 2) { setAttr(attrs[i + 0], attrs[i + 1]); } } public Element(Collection c) { this(c.size()); addAll(c); } public Element(String name, Collection c) { this(name, c.size()); addAll(c); } /** Shallow copy. Same as old.shallowCopy(). * Optional extraCapacity argument is expected extra sub-elements. */ public Element(Element old) { this(old, 0); } public Element(Element old, int extraCapacity) { this(old.name, old.size, old.size + Math.max(0, extraCapacity) + NEED_SLOP + old.attrLength()); // copy sub-elements System.arraycopy(old.parts, 0, parts, 0, size); int alen = parts.length - (size + Math.max(0, extraCapacity) + NEED_SLOP); // copy attributes System.arraycopy(old.parts, old.parts.length - alen, parts, parts.length - alen, alen); assert (!isFrozen()); } /** Shallow copy. Same as new Element(this). */ public Element shallowCopy() { return new Element(this); } static public final Element EMPTY = new Element(ANON_NAME, 0, 0); Element deepFreezeOrCopy(boolean makeFrozen) { if (makeFrozen && isFrozen()) { return this; // no need to copy it } int alen = attrLength(); int plen = size + (makeFrozen ? 0 : NEED_SLOP) + alen; Element copy = new Element(name, size, plen); // copy attributes System.arraycopy(parts, parts.length - alen, copy.parts, plen - alen, alen); // copy sub-elements for (int i = 0; i < size; i++) { Object e = parts[i]; String str; if (e instanceof Element) { // recursion is common case e = ((Element) e).deepFreezeOrCopy(makeFrozen); } else if (makeFrozen) { // Freeze StringBuffers, etc. e = fixupString(e); } copy.setRaw(i, e); } return copy; } /** Returns new Element(this), and also recursively copies sub-elements. */ public Element deepCopy() { return deepFreezeOrCopy(false); } /** Returns frozen version of deepCopy. */ public Element deepFreeze() { return deepFreezeOrCopy(true); } /** Freeze this element. * Throw an IllegalArgumentException if any sub-element is not already frozen. * (Use deepFreeze() to make a frozen copy of an entire element tree.) */ public void shallowFreeze() { if (isFrozen()) { return; } int alen = attrLength(); Object[] nparts = new Object[size + alen]; // copy attributes System.arraycopy(parts, parts.length - alen, nparts, size, alen); // copy sub-elements for (int i = 0; i < size; i++) { Object e = parts[i]; String str; if (e instanceof Element) { // recursion is common case if (!((Element) e).isFrozen()) { throw new IllegalArgumentException("Sub-element must be frozen."); } } else { // Freeze StringBuffers, etc. e = fixupString(e); } nparts[i] = e; } parts = nparts; assert (isFrozen()); } /** Return the name of this element. */ public String getName() { return name; } /** Change the name of this element. */ public void setName(String name) { checkNotFrozen(); this.name = name.toString(); } /** Reports if the element's name is a particular string (spelled "*"). * Such elements are created by the nullary Element constructor, * and by query functions which return multiple values, * such as <tt>findAll</tt>. */ public boolean isAnonymous() { return name == ANON_NAME; } /** Return number of elements. (Does not include attributes.) */ public int size() { return size; } /** True if no elements. (Does not consider attributes.) */ public boolean isEmpty() { return size == 0; } /** True if this element does not allow modification. */ public boolean isFrozen() { // It is frozen iff there is no slop space. return !hasNulls(NEED_SLOP); } void checkNotFrozen() { if (isFrozen()) { throw new UnsupportedOperationException("cannot modify frozen element"); } } /** Remove specified elements. (Does not affect attributes.) */ public void clear() { clear(0, size); } public void clear(int beg) { clear(beg, size); } public void clear(int beg, int end) { if (end > size) { badIndex(end); } if (beg < 0 || beg > end) { badIndex(beg); } if (beg == end) { return; } checkNotFrozen(); if (end == size) { if (beg == 0 && parts.length > 0 && parts[parts.length - 1] == null) { // If no attributes, free the parts array. parts = noPartsNotFrozen; size = 0; } else { clearParts(beg, size); size = beg; } } else { close(beg, end - beg); } } void clearParts(int beg, int end) { for (int i = beg; i < end; i++) { parts[i] = null; } } /** True if name, attributes, and elements are the same. */ public boolean equals(Element that) { if (!this.name.equals(that.name)) { return false; } if (this.size != that.size) { return false; } // elements must be equal and ordered Object[] thisParts = this.parts; Object[] thatParts = that.parts; for (int i = 0; i < size; i++) { Object thisPart = thisParts[i]; Object thatPart = thatParts[i]; if (thisPart instanceof Element) { // recursion is common case if (!thisPart.equals(thatPart)) { return false; } } else { // If either is a non-string char sequence, normalize it. thisPart = fixupString(thisPart); thatPart = fixupString(thatPart); if (!thisPart.equals(thatPart)) { return false; } } } // finally, attributes must be equal (unordered) return this.equalAttrs(that); } // bridge method public boolean equals(Object o) { if (!(o instanceof Element)) { return false; } return equals((Element) o); } public int hashCode() { int hc = 0; int alen = attrLength(); for (int i = parts.length - alen; i < parts.length; i += 2) { hc += (parts[i + 0].hashCode() ^ parts[i + 1].hashCode()); } hc ^= hc << 11; hc += name.hashCode(); for (int i = 0; i < size; i++) { hc ^= hc << 7; Object p = parts[i]; if (p instanceof Element) { hc += p.hashCode(); // recursion is common case } else { hc += fixupString(p).hashCode(); } } hc ^= hc >>> 19; return hc; } /** Compare lexicographically. Earlier-spelled attrs are more sigificant. */ public int compareTo(Element that) { int r; // Primary key is element name. r = this.name.compareTo(that.name); if (r != 0) { return r; } // Secondary key is attributes, as if in normal key order. // The key/value pairs are sorted as a token sequence. int thisAlen = this.attrLength(); int thatAlen = that.attrLength(); if (thisAlen != 0 || thatAlen != 0) { r = compareAttrs(thisAlen, that, thatAlen, true); assert (assertAttrCompareOK(r, that)); if (r != 0) { return r; } } // Finally, elements should be equal and ordered, // and the first difference rules. Object[] thisParts = this.parts; Object[] thatParts = that.parts; int minSize = this.size; if (minSize > that.size) { minSize = that.size; } Comparator<Object> cc = contentOrder(); for (int i = 0; i < minSize; i++) { r = cc.compare(thisParts[i], thatParts[i]); if (r != 0) { return r; } } //if (this.size < that.size) return -1; return this.size - that.size; } private boolean assertAttrCompareOK(int r, Element that) { Element e0 = this.copyAttrsOnly(); Element e1 = that.copyAttrsOnly(); e0.sortAttrs(); e1.sortAttrs(); int r2; for (int k = 0;; k++) { boolean con0 = e0.containsAttr(k); boolean con1 = e1.containsAttr(k); if (con0 != con1) { if (!con0) { r2 = 0 - 1; break; } if (!con1) { r2 = 1 - 0; break; } } if (!con0) { r2 = 0; break; } String k0 = e0.getAttrName(k); String k1 = e1.getAttrName(k); r2 = k0.compareTo(k1); if (r2 != 0) { break; } String v0 = e0.getAttr(k); String v1 = e1.getAttr(k); r2 = v0.compareTo(v1); if (r2 != 0) { break; } } if (r != 0) { r = (r > 0) ? 1 : -1; } if (r2 != 0) { r2 = (r2 > 0) ? 1 : -1; } if (r != r2) { System.out.println("*** wrong attr compare, " + r + " != " + r2); System.out.println(" this = " + this); System.out.println(" attr->" + e0); System.out.println(" that = " + that); System.out.println(" attr->" + e1); } return r == r2; } private void badIndex(int i) { Object badRef = (new Object[0])[i]; } public Object get(int i) { if (i >= size) { badIndex(i); } return parts[i]; } public Object set(int i, Object e) { if (i >= size) { badIndex(i); } Objects.requireNonNull(e); checkNotFrozen(); Object old = parts[i]; setRaw(i, e); return old; } void setRaw(int i, Object e) { parts[i] = e; } public boolean remove(Object e) { int i = indexOf(e); if (i < 0) { return false; } close(i, 1); return true; } public Object remove(int i) { if (i >= size) { badIndex(i); } Object e = parts[i]; close(i, 1); return e; } public Object removeLast() { if (size == 0) { return null; } return remove(size - 1); } /** Remove the first element matching the given filter. * Return the filtered value. */ public Object remove(Filter f) { return findOrRemove(f, 0, true); } public Object remove(Filter f, int fromIndex) { if (fromIndex < 0) { fromIndex = 0; } return findOrRemove(f, fromIndex, true); } /** Remove the last element matching the given filter. * Return the filtered value. */ public Object removeLast(Filter f) { return findOrRemoveLast(f, size - 1, true); } public Object removeLast(Filter f, int fromIndex) { if (fromIndex >= size) { fromIndex = size - 1; } return findOrRemoveLast(f, fromIndex, true); } /** Remove all elements matching the given filter. * If there is a non-null collection given as a sink, * transfer removed elements to the given collection. * The int result is the number of removed elements. * If there is a null sink given, the removed elements * are discarded. If there is no sink given, the removed * elements are returned in an anonymous container element. */ public Element removeAll(Filter f) { Element result = new Element(); findOrRemoveAll(f, false, 0, size, result.asList(), true); return result; } public Element removeAll(Filter f, int fromIndex, int toIndex) { Element result = new Element(); findOrRemoveAll(f, true, fromIndex, toIndex, result.asList(), true); return result; } public int removeAll(Filter f, Collection<Object> sink) { return findOrRemoveAll(f, false, 0, size, sink, true); } public int removeAll(Filter f, int fromIndex, int toIndex, Collection<Object> sink) { return findOrRemoveAll(f, false, fromIndex, toIndex, sink, true); } /** Remove all elements not matching the given filter. * If there is a non-null collection given as a sink, * transfer removed elements to the given collection. * The int result is the number of removed elements. * If there is a null sink given, the removed elements * are discarded. If there is no sink given, the removed * elements are returned in an anonymous container element. */ public Element retainAll(Filter f) { Element result = new Element(); findOrRemoveAll(f, true, 0, size, result.asList(), true); return result; } public Element retainAll(Filter f, int fromIndex, int toIndex) { Element result = new Element(); findOrRemoveAll(f, true, fromIndex, toIndex, result.asList(), true); return result; } public int retainAll(Filter f, Collection<Object> sink) { return findOrRemoveAll(f, true, 0, size, sink, true); } public int retainAll(Filter f, int fromIndex, int toIndex, Collection<Object> sink) { return findOrRemoveAll(f, true, fromIndex, toIndex, sink, true); } public void add(int i, Object e) { // (The shape of this method is tweaked for common cases.) Objects.requireNonNull(e); if (hasNulls(1 + NEED_SLOP)) { // Common case: Have some slop space. if (i == size) { // Most common case: Append. setRaw(i, e); size++; return; } if (i > size) { badIndex(i); } // Second most common case: Shift right by one. open(i, 1); setRaw(i, e); return; } // Ran out of space. Do something complicated. size = expand(i, 1); setRaw(i, e); } public boolean add(Object e) { add(size, e); return true; } public Object getLast() { return size == 0 ? null : parts[size - 1]; } /** Returns the text of this Element. * All sub-elements of this Element must be of type CharSequence. * A ClassCastException is raised if there are non-character sub-elements. * If there is one sub-element, return it. * Otherwise, returns a TokenList of all sub-elements. * This results in a space being placed between each adjacent pair of sub-elements. */ public CharSequence getText() { checkTextOnly(); if (size == 1) { return parts[0].toString(); } else { return new TokenList(parts, 0, size); } } /** Provides an iterable view of this object as a series of texts. * All sub-elements of this Element must be of type CharSequence. * A ClassCastException is raised if there are non-character sub-elements. */ public Iterable<CharSequence> texts() { checkTextOnly(); return (Iterable<CharSequence>) (Iterable) this; } /** Returns an array of strings derived from the sub-elements of this object. * All sub-elements of this Element must be of type CharSequence. * A ClassCastException is raised if there are non-character sub-elements. */ public String[] toStrings() { //checkTextOnly(); String[] result = new String[size]; for (int i = 0; i < size; i++) { result[i] = ((CharSequence) parts[i]).toString(); } return result; } /** Like getText, except that it disregards non-text elements. * Non-text elements are replaced by their textual contents, if any. * Text elements which were separated only by non-text element * boundaries are merged into single tokens. * <p> * There is no corresponding setter, since this accessor does * not report the full state of the element. */ public CharSequence getFlatText() { if (size == 1) { // Simple cases. if (parts[0] instanceof CharSequence) { return parts[0].toString(); } else { return new TokenList(); } } if (isText()) { return getText(); } // Filter and merge. Element result = new Element(size); boolean merge = false; for (int i = 0; i < size; i++) { Object text = parts[i]; if (!(text instanceof CharSequence)) { // Skip, but erase this boundary. if (text instanceof Element) { Element te = (Element) text; if (!te.isEmpty()) { result.addText(te.getFlatText()); } } merge = true; continue; } if (merge) { // Merge w/ previous token. result.addText((CharSequence) text); merge = false; } else { result.add(text); } } if (result.size() == 1) { return (CharSequence) result.parts[0]; } else { return result.getText(); } } /** Return true if all sub-elements are of type CharSequence. */ public boolean isText() { for (int i = 0; i < size; i++) { if (!(parts[i] instanceof CharSequence)) { return false; } } return true; } /** Return true if at least one sub-element is of type CharSequence. */ public boolean hasText() { for (int i = 0; i < size; i++) { if (parts[i] instanceof CharSequence) { return true; } } return false; } /** Raise a ClassCastException if !isText. */ public void checkTextOnly() { for (int i = 0; i < size; i++) { ((CharSequence) parts[i]).getClass(); } } /** Clears out all sub-elements, and replaces them by the given text. * A ClassCastException is raised if there are non-character sub-elements, * either before or after the change. */ public void setText(CharSequence text) { checkTextOnly(); clear(); if (text instanceof TokenList) { // TL's contain only strings addAll(0, (TokenList) text); } else { add(text); } } /** Add text at the given position, merging with any previous * text element, but preserving token boundaries where possible. * <p> * In all cases, the new value of getText() is the string * concatenation of the old value of getText() plus the new text. * <p> * The total effect is to concatenate the given text to any * pre-existing text, and to do so efficiently even if there * are many such concatenations. Also, getText calls which * return multiple tokens (in a TokenList) are respected. * For example, if x is empty, x.addText(y.getText()) puts * an exact structural copy of y's text into x. * <p> * Internal token boundaries in the original text, and in the new * text (i.e., if it is a TokenList), are preserved. However, * at the point where new text joins old text, a StringBuffer * or new String may be created to join the last old and first * new token. * <p> * If the given text is a TokenList, add the tokens as * separate sub-elements, possibly merging the first token to * a previous text item (to avoid making a new token boundary). * <p> * If the element preceding position i is a StringBuffer, * append the first new token to it. * <p> * If the preceding element is a CharSequence, replace it by a * StringBuffer containing both its and the first new token. * <p> * If tokens are added after a StringBuffer, freeze it into a String. * <p> * Every token not merged into a previous CharSequence is added * as a new sub-element, starting at position i. * <p> * Returns the number of elements added, which is useful * for further calls to addText. This number is zero * if the input string was null, or was successfully * merged into a StringBuffer at position i-1. * <p> * By contrast, calling add(text) always adds a new sub-element. * In that case, if there is a previous string, a separating * space is virtually present also, and will be observed if * getText() is used to return all the text together. */ public int addText(int i, CharSequence text) { if (text instanceof String) { return addText(i, (String) text); } else if (text instanceof TokenList) { // Text is a list of tokens. TokenList tl = (TokenList) text; int tlsize = tl.size(); if (tlsize == 0) { return 0; } String token0 = tl.get(0).toString(); if (tlsize == 1) { return addText(i, token0); } if (mergeWithPrev(i, token0, false)) { // Add the n-1 remaining tokens. addAll(i, tl.subList(1, tlsize)); return tlsize - 1; } else { addAll(i, (Collection) tl); return tlsize; } } else { return addText(i, text.toString()); } } public int addText(CharSequence text) { return addText(size, text); } private // no reason to make this helper public int addText(int i, String text) { if (text.length() == 0) { return 0; // Trivial success. } if (mergeWithPrev(i, text, true)) { return 0; // Merged with previous token. } // No previous token. add(i, text); return 1; } // Tries to merge token with previous contents. // Returns true if token is successfully disposed of. // If keepSB is false, any previous StringBuffer is frozen. // If keepSB is true, a StringBuffer may be created to hold // the merged token. private boolean mergeWithPrev(int i, String token, boolean keepSB) { if (i == 0) // Trivial success if the token is length zero. { return (token.length() == 0); } Object prev = parts[i - 1]; if (prev instanceof StringBuffer) { StringBuffer psb = (StringBuffer) prev; psb.append(token); if (!keepSB) { parts[i - 1] = psb.toString(); } return true; } if (token.length() == 0) { return true; // Trivial success. } if (prev instanceof CharSequence) { // Must concatenate. StringBuffer psb = new StringBuffer(prev.toString()); psb.append(token); if (keepSB) { parts[i - 1] = psb; } else { parts[i - 1] = psb.toString(); } return true; } return false; } /** Trim all strings, using String.trim(). * Remove empty strings. * Normalize CharSequences to Strings. */ public void trimText() { checkNotFrozen(); int fillp = 0; int size = this.size; Object[] parts = this.parts; for (int i = 0; i < size; i++) { Object e = parts[i]; if (e instanceof CharSequence) { String tt = e.toString().trim(); if (tt.length() == 0) { continue; } e = tt; } parts[fillp++] = e; } while (size > fillp) { parts[--size] = null; } this.size = fillp; } /** Add one or more subelements at the given position. * If the object reference is null, nothing happens. * If the object is an anonymous Element, addAll is called. * If the object is a TokenList, addAll is called (to add the tokens). * Otherwise, add is called, adding a single subelement or string. * The net effect is to add zero or more tokens. * The returned value is the number of added elements. * <p> * Note that getText() can return a TokenList which preserves * token boundaries in the text source. Such a text will be * added as multiple text sub-elements. * <p> * If a text string is added adjacent to an immediately * preceding string, there will be a token boundary between * the strings, which will print as an extra space. */ public int addContent(int i, Object e) { if (e == null) { return 0; } else if (e instanceof TokenList) { return addAll(i, (Collection) e); } else if (e instanceof Element && ((Element) e).isAnonymous()) { return addAll(i, (Element) e); } else { add(i, e); return 1; } } public int addContent(Object e) { return addContent(size, e); } public Object[] toArray() { Object[] result = new Object[size]; System.arraycopy(parts, 0, result, 0, size); return result; } public Element copyContentOnly() { Element content = new Element(size); System.arraycopy(parts, 0, content.parts, 0, size); content.size = size; return content; } public void sort(Comparator<Object> c) { Arrays.sort(parts, 0, size, c); } public void sort() { sort(CONTENT_ORDER); } /** Equivalent to Collections.reverse(this.asList()). */ public void reverse() { for (int i = 0, mid = size >> 1, j = size - 1; i < mid; i++, j--) { Object p = parts[i]; parts[i] = parts[j]; parts[j] = p; } } /** Equivalent to Collections.shuffle(this.asList() [, rnd]). */ public void shuffle() { Collections.shuffle(this.asList()); } public void shuffle(Random rnd) { Collections.shuffle(this.asList(), rnd); } /** Equivalent to Collections.rotate(this.asList(), dist). */ public void rotate(int dist) { Collections.rotate(this.asList(), dist); } /** Equivalent to Collections.min(this.asList(), c). */ public Object min(Comparator<Object> c) { return Collections.min(this.asList(), c); } public Object min() { return min(CONTENT_ORDER); } /** Equivalent to Collections.max(this.asList(), c). */ public Object max(Comparator<Object> c) { return Collections.max(this.asList(), c); } public Object max() { return max(CONTENT_ORDER); } public int addAll(int i, Collection c) { if (c instanceof LView) { return addAll(i, ((LView) c).asElement()); } else { int csize = c.size(); if (csize == 0) { return 0; } openOrExpand(i, csize); int fill = i; for (Object part : c) { parts[fill++] = part; } return csize; } } public int addAll(int i, Element e) { int esize = e.size; if (esize == 0) { return 0; } openOrExpand(i, esize); System.arraycopy(e.parts, 0, parts, i, esize); return esize; } public int addAll(Collection c) { return addAll(size, c); } public int addAll(Element e) { return addAll(size, e); } public int addAllAttrsFrom(Element e) { int added = 0; for (int k = 0; e.containsAttr(k); k++) { String old = setAttr(e.getAttrName(k), e.getAttr(k)); if (old == null) { added += 1; } } // Return number of added (not merely changed) attrs. return added; } // Search. public boolean matches(Filter f) { return f.filter(this) != null; } public Object find(Filter f) { return findOrRemove(f, 0, false); } public Object find(Filter f, int fromIndex) { if (fromIndex < 0) { fromIndex = 0; } return findOrRemove(f, fromIndex, false); } /** Find the last element matching the given filter. * Return the filtered value. */ public Object findLast(Filter f) { return findOrRemoveLast(f, size - 1, false); } public Object findLast(Filter f, int fromIndex) { if (fromIndex >= size) { fromIndex = size - 1; } return findOrRemoveLast(f, fromIndex, false); } /** Find all elements matching the given filter. * If there is a non-null collection given as a sink, * transfer matching elements to the given collection. * The int result is the number of matching elements. * If there is a null sink given, the matching elements are * not collected. If there is no sink given, the matching * elements are returned in an anonymous container element. * In no case is the receiver element changed. * <p> * Note that a simple count of matching elements can be * obtained by passing a null collection argument. */ public Element findAll(Filter f) { Element result = new Element(); findOrRemoveAll(f, false, 0, size, result.asList(), false); return result; } public Element findAll(Filter f, int fromIndex, int toIndex) { Element result = new Element(name); findOrRemoveAll(f, false, fromIndex, toIndex, result.asList(), false); return result; } public int findAll(Filter f, Collection<Object> sink) { return findOrRemoveAll(f, false, 0, size, sink, false); } public int findAll(Filter f, int fromIndex, int toIndex, Collection<Object> sink) { return findOrRemoveAll(f, false, fromIndex, toIndex, sink, false); } /// Driver routines. private Object findOrRemove(Filter f, int fromIndex, boolean remove) { for (int i = fromIndex; i < size; i++) { Object x = f.filter(parts[i]); if (x != null) { if (remove) { close(i, 1); } return x; } } return null; } private Object findOrRemoveLast(Filter f, int fromIndex, boolean remove) { for (int i = fromIndex; i >= 0; i--) { Object x = f.filter(parts[i]); if (x != null) { if (remove) { close(i, 1); } return x; } } return null; } private int findOrRemoveAll(Filter f, boolean fInvert, int fromIndex, int toIndex, Collection<Object> sink, boolean remove) { if (fromIndex < 0) { badIndex(fromIndex); } if (toIndex > size) { badIndex(toIndex); } int found = 0; for (int i = fromIndex; i < toIndex; i++) { Object p = parts[i]; Object x = f.filter(p); if (fInvert ? (x == null) : (x != null)) { if (remove) { close(i--, 1); toIndex--; } found += XMLKit.addContent(fInvert ? p : x, sink); } } return found; } public void replaceAll(Filter f) { XMLKit.replaceAll(f, this.asList()); } public void replaceAll(Filter f, int fromIndex, int toIndex) { XMLKit.replaceAll(f, this.asList().subList(fromIndex, toIndex)); } /// Recursive walks. // findAllInTree(f) == findAll(findInTree(f,S)), S.toElement // findAllInTree(f,S) == findAll(findInTree(content(f,S))) // removeAllInTree(f) == replaceAll(replaceInTree(and(f,emptyF))) // removeAllInTree(f,S) == replaceAll(replaceInTree(and(content(f,S),emptyF))) // retainAllInTree(f) == removeAllInTree(not(f)) // replaceAllInTree(f) == replaceAll(replaceInTree(f)) public Element findAllInTree(Filter f) { Element result = new Element(); findAllInTree(f, result.asList()); return result; } public int findAllInTree(Filter f, Collection<Object> sink) { int found = 0; int size = this.size; // cache copy for (int i = 0; i < size; i++) { Object p = parts[i]; Object x = f.filter(p); if (x != null) { found += XMLKit.addContent(x, sink); } else if (p instanceof Element) { found += ((Element) p).findAllInTree(f, sink); } } return found; } public int countAllInTree(Filter f) { return findAllInTree(f, null); } public int removeAllInTree(Filter f, Collection<Object> sink) { if (sink == null) { sink = newCounterColl(); } replaceAll(replaceInTree(and(content(f, sink), emptyFilter()))); return sink.size(); } public Element removeAllInTree(Filter f) { Element result = new Element(); removeAllInTree(f, result.asList()); return result; } public int retainAllInTree(Filter f, Collection<Object> sink) { return removeAllInTree(not(f), sink); } public Element retainAllInTree(Filter f) { Element result = new Element(); retainAllInTree(f, result.asList()); return result; } public void replaceAllInTree(Filter f) { replaceAll(replaceInTree(f)); } /** Raise a ClassCastException if any subelements are the wrong type. */ public void checkPartsOnly(Class<?> elementClass) { for (int i = 0; i < size; i++) { elementClass.cast(parts[i]).getClass(); } } /** Return true if all sub-elements are of the given type. */ public boolean isPartsOnly(Class<?> elementClass) { for (int i = 0; i < size; i++) { if (!elementClass.isInstance(parts[i])) { return false; } } return true; } /** Provides an iterable view of this object as a series of elements. * All sub-elements of this Element must be of type Element. * A ClassCastException is raised if there are non-Element sub-elements. */ public <T> Iterable<T> partsOnly(Class<T> elementClass) { checkPartsOnly(elementClass); return (Iterable<T>) (Iterable) this; } public Iterable<Element> elements() { return partsOnly(Element.class); } /// Useful shorthands. // Finding or removing elements w/o regard to their type or content. public Element findElement() { return (Element) find(elementFilter()); } public Element findAllElements() { return findAll(elementFilter()); } public Element removeElement() { return (Element) remove(elementFilter()); } public Element removeAllElements() { return (Element) removeAll(elementFilter()); } // Finding or removing by element tag or selected attribute, // as if by elementFilter(name) or attrFilter(name, value). // Roughly akin to Common Lisp ASSOC. public Element findElement(String name) { return (Element) find(elementFilter(name)); } public Element removeElement(String name) { return (Element) remove(elementFilter(name)); } public Element findWithAttr(String key) { return (Element) find(attrFilter(name)); } public Element findWithAttr(String key, String value) { return (Element) find(attrFilter(name, value)); } public Element removeWithAttr(String key) { return (Element) remove(attrFilter(name)); } public Element removeWithAttr(String key, String value) { return (Element) remove(attrFilter(name, value)); } public Element findAllElements(String name) { return findAll(elementFilter(name)); } public Element removeAllElements(String name) { return removeAll(elementFilter(name)); } public Element retainAllElements(String name) { return retainAll(elementFilter(name)); } public Element findAllWithAttr(String key) { return findAll(attrFilter(key)); } public Element removeAllWithAttr(String key) { return removeAll(attrFilter(key)); } public Element retainAllWithAttr(String key) { return retainAll(attrFilter(key)); } public Element findAllWithAttr(String key, String value) { return findAll(attrFilter(key, value)); } public Element removeAllWithAttr(String key, String value) { return removeAll(attrFilter(key, value)); } public Element retainAllWithAttr(String key, String value) { return retainAll(attrFilter(key, value)); } public int countAll(Filter f) { return findAll(f, null); } public int countAllElements() { return countAll(elementFilter()); } public int countAllElements(String name) { return countAll(elementFilter(name)); } public int countAllWithAttr(String key) { return countAll(attrFilter(name)); } public int countAllWithAttr(String key, String value) { return countAll(attrFilter(key, value)); } public int indexOf(Object e) { for (int i = 0; i < size; i++) { if (e.equals(parts[i])) { return i; } } return -1; } public int lastIndexOf(Object e) { for (int i = size - 1; i >= 0; i--) { if (e.equals(parts[i])) { return i; } } return -1; } /** Remove the first element matching the given filter. * Return the filtered value. */ public int indexOf(Filter f) { return indexOf(f, 0); } public int indexOf(Filter f, int fromIndex) { if (fromIndex < 0) { fromIndex = 0; } for (int i = fromIndex; i < size; i++) { Object x = f.filter(parts[i]); if (x != null) { return i; } } return -1; } /** Remove the last element matching the given filter. * Return the filtered value. */ public int lastIndexOf(Filter f) { return lastIndexOf(f, size - 1); } public int lastIndexOf(Filter f, int fromIndex) { if (fromIndex >= size) { fromIndex = size - 1; } for (int i = fromIndex; i >= 0; i--) { Object x = f.filter(parts[i]); if (x != null) { return i; } } return -1; } public boolean contains(Object e) { return indexOf(e) >= 0; } // attributes private int findOrCreateAttr(String key, boolean create) { key.toString(); // null check int attrBase = parts.length; for (int i = parts.length - 2; i >= size; i -= 2) { String akey = (String) parts[i + 0]; if (akey == null) { if (!create) { return -1; } if (i == size) { break; // NEED_SLOP } parts[i + 0] = key; //parts[i+1] = ""; //caller responsibility return i; } attrBase = i; if (akey.equals(key)) { return i; } } // If we fell through, we ran into an element part. // Therefore we have run out of empty slots. if (!create) { return -1; } assert (!isFrozen()); int alen = parts.length - attrBase; expand(size, 2); // generally expands by more than 2 // since there was a reallocation, the garbage slots are really null assert (parts[size + 0] == null && parts[size + 1] == null); alen += 2; int i = parts.length - alen; parts[i + 0] = key; //parts[i+1] = ""; //caller responsibility return i; } public int attrSize() { return attrLength() >>> 1; } public int indexOfAttr(String key) { return findOrCreateAttr(key, false); } public boolean containsAttr(String key) { return indexOfAttr(key) >= 0; } public String getAttr(String key) { return getAttr(key, null); } public String getAttr(String key, String dflt) { int i = findOrCreateAttr(key, false); return (i < 0) ? dflt : (String) parts[i + 1]; } public TokenList getAttrList(String key) { return convertToList(getAttr(key)); } public Number getAttrNumber(String key) { return convertToNumber(getAttr(key)); } public long getAttrLong(String key) { return getAttrLong(key, 0); } public double getAttrDouble(String key) { return getAttrDouble(key, 0.0); } public long getAttrLong(String key, long dflt) { return convertToLong(getAttr(key), dflt); } public double getAttrDouble(String key, double dflt) { return convertToDouble(getAttr(key), dflt); } int indexAttr(int k) { int i = parts.length - (k * 2) - 2; if (i < size || parts[i] == null) { return -2; // always oob } return i; } public boolean containsAttr(int k) { return indexAttr(k) >= 0; } public String getAttr(int k) { return (String) parts[indexAttr(k) + 1]; } public String getAttrName(int k) { return (String) parts[indexAttr(k) + 0]; } public Iterable<String> attrNames() { //return asAttrMap().keySet(); return new Iterable<String>() { public Iterator<String> iterator() { return new ANItr(); } }; } // Hand-inlined replacement for asAttrMap().keySet().iterator(): class ANItr implements Iterator<String> { boolean lastRet; int cursor = -2; // pointer from end of parts public boolean hasNext() { int i = cursor + parts.length; return i >= size && parts[i] == null; } public String next() { int i = cursor + parts.length; Object x; if (i < size || (x = parts[i]) == null) { nsee(); return null; } cursor -= 2; lastRet = true; return (String) x; } public void remove() { if (!lastRet) { throw new IllegalStateException(); } Element.this.removeAttr((-4 - cursor) / 2); cursor += 2; lastRet = false; } Exception nsee() { throw new NoSuchElementException("attribute " + (-2 - cursor) / 2); } } /** Return an anonymous copy of self, but only with attributes. */ public Element copyAttrsOnly() { int alen = attrLength(); Element attrs = new Element(alen); Object[] attrParts = attrs.parts; assert (attrParts.length == NEED_SLOP + alen); System.arraycopy(parts, parts.length - alen, attrParts, NEED_SLOP, alen); return attrs; } /** Get all attributes, represented as an element with sub-elements. * The name of each sub-element is the attribute key, and the text * This is a fresh copy, and can be updated with affecting the original. * of each sub-element is the corresponding attribute value. * See also asAttrMap() for a "live" view of all the attributes as a Map. */ public Element getAttrs() { int asize = attrSize(); Element attrs = new Element(ANON_NAME, asize, NEED_SLOP + asize); for (int i = 0; i < asize; i++) { Element attr = new Element(getAttrName(i), 1, NEED_SLOP + 1); // %%% normalize attrs to token lists? attr.setRaw(0, getAttr(i)); attrs.setRaw(i, attr); } return attrs; } public void setAttrs(Element attrs) { int alen = attrLength(); clearParts(parts.length - alen, alen); if (!hasNulls(NEED_SLOP + attrs.size * 2)) { expand(size, attrs.size * 2); } addAttrs(attrs); } public void addAttrs(Element attrs) { for (int i = 0; i < attrs.size; i++) { Element attr = (Element) attrs.get(i); setAttr(attr.name, attr.getText().toString()); } } public void removeAttr(int i) { checkNotFrozen(); while ((i -= 2) >= size) { Object k = parts[i + 0]; Object v = parts[i + 1]; if (k == null) { break; } parts[i + 2] = k; parts[i + 3] = v; } parts[i + 2] = null; parts[i + 3] = null; } public void clearAttrs() { if (parts.length == 0 || parts[parts.length - 1] == null) { return; // no attrs to clear } checkNotFrozen(); if (size == 0) { // If no elements, free the parts array. parts = noPartsNotFrozen; return; } for (int i = parts.length - 1; parts[i] != null; i--) { assert (i >= size); parts[i] = null; } } public String setAttr(String key, String value) { String old; if (value == null) { int i = findOrCreateAttr(key, false); if (i >= 0) { old = (String) parts[i + 1]; removeAttr(i); } else { old = null; } } else { checkNotFrozen(); int i = findOrCreateAttr(key, true); old = (String) parts[i + 1]; parts[i + 1] = value; } return old; } public String setAttrList(String key, List<String> l) { if (l == null) { return setAttr(key, null); } if (!(l instanceof TokenList)) { l = new TokenList(l); } return setAttr(key, l.toString()); } public String setAttrNumber(String key, Number n) { return setAttr(key, (n == null) ? null : n.toString()); } public String setAttrLong(String key, long n) { return setAttr(key, (n == 0) ? null : String.valueOf(n)); } public String setAttrDouble(String key, double n) { return setAttr(key, (n == 0) ? null : String.valueOf(n)); } public String setAttr(int k, String value) { int i = indexAttr(k); String old = (String) parts[i + 1]; if (value == null) { removeAttr(i); } else { checkNotFrozen(); parts[i + 1] = value; } return old; } int attrLength() { return parts.length - attrBase(); } /** Are the attributes of the two two elements equal? * Disregards name, sub-elements, and ordering of attributes. */ public boolean equalAttrs(Element that) { int alen = this.attrLength(); if (alen != that.attrLength()) { return false; } if (alen == 0) { return true; } return compareAttrs(alen, that, alen, false) == 0; } private int compareAttrs(int thisAlen, Element that, int thatAlen, boolean fullCompare) { Object[] thisParts = this.parts; Object[] thatParts = that.parts; int thisBase = thisParts.length - thisAlen; int thatBase = thatParts.length - thatAlen; // search indexes into unmatched parts of this.attrs: int firstI = 0; // search indexes into unmatched parts of that.attrs: int firstJ = 0; int lastJ = thatAlen - 2; // try to find the mismatch with the first key: String firstKey = null; int firstKeyValCmp = 0; int foundKeys = 0; for (int i = 0; i < thisAlen; i += 2) { String key = (String) thisParts[thisBase + i + 0]; String val = (String) thisParts[thisBase + i + 1]; String otherVal = null; for (int j = firstJ; j <= lastJ; j += 2) { if (key.equals(thatParts[thatBase + j + 0])) { foundKeys += 1; otherVal = (String) thatParts[thatBase + j + 1]; // Optimization: Narrow subsequent searches when easy. if (j == lastJ) { lastJ -= 2; } else if (j == firstJ) { firstJ += 2; } if (i == firstI) { firstI += 2; } break; } } int valCmp; if (otherVal != null) { // The key was found. if (!fullCompare) { if (!val.equals(otherVal)) { return 1 - 0; //arb. } continue; } valCmp = val.compareTo(otherVal); } else { // Found the key in this but not that. // Such a mismatch puts the guy missing the key last. valCmp = 0 - 1; } if (valCmp != 0) { // found a mismatch, key present in both elems if (firstKey == null || firstKey.compareTo(key) > 0) { // found a better key firstKey = key; firstKeyValCmp = valCmp; } } } // We have located the first mismatch of all keys in this.attrs. // In general we must also look for keys in that.attrs but missing // from this.attrs; such missing keys, if earlier than firstKey, // rule the comparison. // We can sometimes prove quickly there is no missing key. if (foundKeys == thatAlen / 2) { // Exhausted all keys in that.attrs. return firstKeyValCmp; } // Search for a missing key in that.attrs earlier than firstKey. findMissingKey: for (int j = firstJ; j <= lastJ; j += 2) { String otherKey = (String) thatParts[thatBase + j + 0]; if (firstKey == null || firstKey.compareTo(otherKey) > 0) { // Found a better key; is it missing? for (int i = firstI; i < thisAlen; i += 2) { if (otherKey.equals(thisParts[thisBase + i + 0])) { continue findMissingKey; } } // If we get here, there was no match in this.attrs. return 1 - 0; } } // No missing key. Previous comparison value rules. return firstKeyValCmp; } // Binary search looking for first non-null after size. int attrBase() { // Smallest & largest possible attribute indexes: int kmin = 0; int kmax = (parts.length - size) >>> 1; // earlist possible attribute position: int abase = parts.length - (kmax * 2); // binary search using scaled indexes: while (kmin != kmax) { int kmid = kmin + ((kmax - kmin) >>> 1); if (parts[abase + (kmid * 2)] == null) { kmin = kmid + 1; } else { kmax = kmid; } assert (kmin <= kmax); } return abase + (kmax * 2); } /** Sort attributes by name. */ public void sortAttrs() { checkNotFrozen(); int abase = attrBase(); int alen = parts.length - abase; String[] buf = new String[alen]; // collect keys for (int k = 0; k < alen / 2; k++) { String akey = (String) parts[abase + (k * 2) + 0]; buf[k] = akey; } Arrays.sort(buf, 0, alen / 2); // collect values for (int k = 0; k < alen / 2; k++) { String akey = buf[k]; buf[k + alen / 2] = getAttr(akey); } // reorder keys and values int fillp = parts.length; for (int k = 0; k < alen / 2; k++) { String akey = buf[k]; String aval = buf[k + alen / 2]; fillp -= 2; parts[fillp + 0] = akey; parts[fillp + 1] = aval; } assert (fillp == abase); } /* Notes on whitespace and tokenization. On input, never split CDATA blocks. They remain single tokens. ?Try to treat encoded characters as CDATA-quoted, also? Internally, each String sub-element is logically a token. However, if there was no token-splitting on input, consecutive strings are merged by the parser. Internally, we need addToken (intervening blank) and addText (hard concatenation). Optionally on input, tokenize unquoted text into words. Between each adjacent word pair, elide either one space or all space. On output, we always add spaces between tokens. The Element("a", {"b", "c", Element("d"), "e f"}) outputs as "<a>b c<d/>e f</a>" */ /** Split strings into tokens, using a StringTokenizer. */ public void tokenize(String delims, boolean returnDelims) { checkNotFrozen(); if (delims == null) { delims = WHITESPACE_CHARS; // StringTokenizer default } for (int i = 0; i < size; i++) { if (!(parts[i] instanceof CharSequence)) { continue; } int osize = size; String str = parts[i].toString(); StringTokenizer st = new StringTokenizer(str, delims, returnDelims); int nstrs = st.countTokens(); switch (nstrs) { case 0: close(i--, 1); break; case 1: parts[i] = st.nextToken(); break; default: openOrExpand(i + 1, nstrs - 1); for (int j = 0; j < nstrs; j++) { parts[i + j] = st.nextToken(); } i += nstrs - 1; break; } } } public void tokenize(String delims) { tokenize(delims, false); } public void tokenize() { tokenize(null, false); } // views class LView extends AbstractList<Object> { Element asElement() { return Element.this; } public int size() { return Element.this.size(); } public Object get(int i) { return Element.this.get(i); } @Override public boolean contains(Object e) { return Element.this.contains(e); } @Override public Object[] toArray() { return Element.this.toArray(); } @Override public int indexOf(Object e) { return Element.this.indexOf(e); } @Override public int lastIndexOf(Object e) { return Element.this.lastIndexOf(e); } @Override public void add(int i, Object e) { ++modCount; Element.this.add(i, e); } @Override public boolean addAll(int i, Collection<? extends Object> c) { ++modCount; return Element.this.addAll(i, c) > 0; } @Override public boolean addAll(Collection<? extends Object> c) { ++modCount; return Element.this.addAll(c) > 0; } @Override public Object remove(int i) { ++modCount; return Element.this.remove(i); } @Override public Object set(int i, Object e) { ++modCount; return Element.this.set(i, e); } @Override public void clear() { ++modCount; Element.this.clear(); } // Others: toArray(Object[]), containsAll, removeAll, retainAll } /** Produce a list view of sub-elements. * (The list view does not provide access to the element's * name or attributes.) * Changes to this view are immediately reflected in the * element itself. */ public List<Object> asList() { return new LView(); } /** Produce a list iterator on all sub-elements. */ public ListIterator<Object> iterator() { //return asList().listIterator(); return new Itr(); } // Hand-inlined replacement for LView.listIterator(): class Itr implements ListIterator<Object> { int lastRet = -1; int cursor = 0; public boolean hasNext() { return cursor < size; } public boolean hasPrevious() { return cursor > 0 && cursor <= size; } public Object next() { if (!hasNext()) { nsee(); } return parts[lastRet = cursor++]; } public Object previous() { if (!hasPrevious()) { nsee(); } return parts[--cursor]; } public int nextIndex() { return cursor; } public int previousIndex() { return cursor - 1; } public void set(Object x) { parts[lastRet] = x; } public void add(Object x) { lastRet = -1; Element.this.add(cursor++, x); } public void remove() { if (lastRet < 0) { throw new IllegalStateException(); } Element.this.remove(lastRet); if (lastRet < cursor) { --cursor; } lastRet = -1; } void nsee() { throw new NoSuchElementException("element " + cursor); } } /** A PrintWriter which always appends as if by addText. * Use of this stream may insert a StringBuffer at the end * of the Element. The user must not directly modify this * StringBuffer, or use it in other data structures. * From time to time, the StringBuffer may be replaced by a * constant string as a result of using the PrintWriter. */ public PrintWriter asWriter() { return new ElemW(); } class ElemW extends PrintWriter { ElemW() { super(new StringWriter()); } final StringBuffer buf = ((StringWriter) out).getBuffer(); { lock = buf; } // synchronize on this buffer @Override public void println() { synchronized (buf) { ensureCursor(); super.println(); } } @Override public void write(int ch) { synchronized (buf) { ensureCursor(); //buf.append(ch); super.write(ch); } } @Override public void write(char buf[], int off, int len) { synchronized (buf) { ensureCursor(); super.write(buf, off, len); } } @Override public void write(String s, int off, int len) { synchronized (buf) { ensureCursor(); //buf.append(s.substring(off, off+len)); super.write(s, off, len); } } @Override public void write(String s) { synchronized (buf) { ensureCursor(); //buf.append(s); super.write(s); } } private void ensureCursor() { checkNotFrozen(); if (getLast() != buf) { int pos = indexOf(buf); if (pos >= 0) { // Freeze the pre-existing use of buf. setRaw(pos, buf.toString()); } add(buf); } } } /** Produce a map view of attributes, in which the attribute * name strings are the keys. * (The map view does not provide access to the element's * name or sub-elements.) * Changes to this view are immediately reflected in the * element itself. */ public Map<String, String> asAttrMap() { class Entry implements Map.Entry<String, String> { final int k; Entry(int k) { this.k = k; assert (((String) getKey()).toString() != null); // check, fail-fast } public String getKey() { return Element.this.getAttrName(k); } public String getValue() { return Element.this.getAttr(k); } public String setValue(String v) { return Element.this.setAttr(k, v.toString()); } @Override public boolean equals(Object o) { if (!(o instanceof Map.Entry)) { return false; } Map.Entry that = (Map.Entry) o; return (this.getKey().equals(that.getKey()) && this.getValue().equals(that.getValue())); } @Override public int hashCode() { return getKey().hashCode() ^ getValue().hashCode(); } } class EIter implements Iterator<Map.Entry<String, String>> { int k = 0; // index of pending next() attribute public boolean hasNext() { return Element.this.containsAttr(k); } public Map.Entry<String, String> next() { return new Entry(k++); } public void remove() { Element.this.removeAttr(--k); } } class ESet extends AbstractSet<Map.Entry<String, String>> { public int size() { return Element.this.attrSize(); } public Iterator<Map.Entry<String, String>> iterator() { return new EIter(); } @Override public void clear() { Element.this.clearAttrs(); } } class AView extends AbstractMap<String, String> { private transient Set<Map.Entry<String, String>> eSet; public Set<Map.Entry<String, String>> entrySet() { if (eSet == null) { eSet = new ESet(); } return eSet; } @Override public int size() { return Element.this.attrSize(); } public boolean containsKey(String k) { return Element.this.containsAttr(k); } public String get(String k) { return Element.this.getAttr(k); } @Override public String put(String k, String v) { return Element.this.setAttr(k, v.toString()); } public String remove(String k) { return Element.this.setAttr(k, null); } } return new AView(); } /** Reports number of additional elements this object can accommodate * without reallocation. */ public int getExtraCapacity() { int abase = attrBase(); return Math.max(0, abase - size - NEED_SLOP); } /** Ensures that at least the given number of additional elements * can be added to this object without reallocation. */ public void ensureExtraCapacity(int cap) { if (cap == 0 || hasNulls(cap + NEED_SLOP)) { return; } setExtraCapacity(cap); } /** * Trim excess capacity to zero, or do nothing if frozen. * This minimizes the space occupied by this Element, * at the expense of a reallocation if sub-elements or attributes * are added later. */ public void trimToSize() { if (isFrozen()) { return; } setExtraCapacity(0); } /** Changes the number of additional elements this object can accommodate * without reallocation. */ public void setExtraCapacity(int cap) { checkNotFrozen(); int abase = attrBase(); int alen = parts.length - abase; // slots allocated for attrs int nlen = size + cap + NEED_SLOP + alen; if (nlen != parts.length) { Object[] nparts = new Object[nlen]; // copy attributes System.arraycopy(parts, abase, nparts, nlen - alen, alen); // copy sub-elements System.arraycopy(parts, 0, nparts, 0, size); parts = nparts; } assert (cap == getExtraCapacity()); } // Return true if there are at least len nulls of slop available. boolean hasNulls(int len) { if (len == 0) { return true; } int lastNull = size + len - 1; if (lastNull >= parts.length) { return false; } return (parts[lastNull] == null); } // Opens up parts array at pos by len spaces. void open(int pos, int len) { assert (pos < size); assert (hasNulls(len + NEED_SLOP)); checkNotFrozen(); int nsize = size + len; int tlen = size - pos; System.arraycopy(parts, pos, parts, pos + len, tlen); size = nsize; } // Reallocate and open up at parts[pos] to at least len empty places. // Shift anything after pos right by len. Reallocate if necessary. // If pos < size, caller must fill it in with non-null values. // Returns incremented size; caller is responsible for storing it // down, if desired. int expand(int pos, int len) { assert (pos <= size); // There must be at least len nulls between elems and attrs. assert (!hasNulls(NEED_SLOP + len)); // caller responsibility checkNotFrozen(); int nsize = size + len; // length of all elements int tlen = size - pos; // length of elements in post-pos tail int abase = attrBase(); int alen = parts.length - abase; // slots allocated for attrs int nlen = nsize + alen + NEED_SLOP; nlen += (nlen >>> 1); // add new slop! Object[] nparts = new Object[nlen]; // copy head of sub-elements System.arraycopy(parts, 0, nparts, 0, pos); // copy tail of sub-elements System.arraycopy(parts, pos, nparts, pos + len, tlen); // copy attributes System.arraycopy(parts, abase, nparts, nlen - alen, alen); // update self parts = nparts; //assert(hasNulls(len)); <- not yet true, since size != nsize return nsize; } // Open or expand at the given position, as appropriate. boolean openOrExpand(int pos, int len) { if (pos < 0 || pos > size) { badIndex(pos); } if (hasNulls(len + NEED_SLOP)) { if (pos == size) { size += len; } else { open(pos, len); } return false; } else { size = expand(pos, len); return true; } } // Close up at parts[pos] len old places. // Shift anything after pos left by len. // Fill unused end of parts with null. void close(int pos, int len) { assert (len > 0); assert ((size - pos) >= len); checkNotFrozen(); int tlen = (size - pos) - len; // length of elements in post-pos tail int nsize = size - len; System.arraycopy(parts, pos + len, parts, pos, tlen); // reinitialize the unoccupied slots to null clearParts(nsize, nsize + len); // update self size = nsize; assert (hasNulls(len)); } public void writeTo(Writer w) throws IOException { new Printer(w).print(this); } public void writePrettyTo(Writer w) throws IOException { prettyPrintTo(w, this); } public String prettyString() { StringWriter sw = new StringWriter(); try { writePrettyTo(sw); } catch (IOException ee) { throw new Error(ee); // should not happen } return sw.toString(); } @Override public String toString() { StringWriter sw = new StringWriter(); try { writeTo(sw); } catch (IOException ee) { throw new Error(ee); // should not happen } return sw.toString(); } public String dump() { // For debugging only. Reveals internal layout. StringBuilder buf = new StringBuilder(); buf.append("<").append(name).append("[").append(size).append("]"); for (int i = 0; i < parts.length; i++) { Object p = parts[i]; if (p == null) { buf.append(" null"); } else { buf.append(" {"); String cname = p.getClass().getName(); cname = cname.substring(1 + cname.indexOf('/')); cname = cname.substring(1 + cname.indexOf('$')); cname = cname.substring(1 + cname.indexOf('#')); if (!cname.equals("String")) { buf.append(cname).append(":"); } buf.append(p); buf.append("}"); } } return buf.append(">").toString(); } public static java.lang.reflect.Method method(String name) { HashMap allM = allMethods; if (allM == null) { allM = makeAllMethods(); } java.lang.reflect.Method res = (java.lang.reflect.Method) allMethods.get(name); if (res == null) { throw new IllegalArgumentException(name); } return res; } private static HashMap allMethods; private static synchronized HashMap makeAllMethods() { if (allMethods != null) { return allMethods; } java.lang.reflect.Method[] methods = Element.class.getMethods(); HashMap<String, java.lang.reflect.Method> allM = new HashMap<String, java.lang.reflect.Method>(), ambig = new HashMap<String, java.lang.reflect.Method>(); for (int i = 0; i < methods.length; i++) { java.lang.reflect.Method m = methods[i]; Class[] args = m.getParameterTypes(); String name = m.getName(); assert (java.lang.reflect.Modifier.isPublic(m.getModifiers())); if (name.startsWith("notify")) { continue; } if (name.endsWith("Attr") && args.length > 0 && args[0] == int.class) // ignore getAttr(int), etc. { continue; } if (name.endsWith("All") && args.length > 1 && args[0] == Filter.class) // ignore findAll(Filter, int...), etc. { continue; } java.lang.reflect.Method pm = allM.put(name, m); if (pm != null) { Class[] pargs = pm.getParameterTypes(); if (pargs.length > args.length) { allM.put(name, pm); // put it back } else if (pargs.length == args.length) { ambig.put(name, pm); // make a note of it } } } // Delete ambiguous methods. for (Map.Entry<String, java.lang.reflect.Method> e : ambig.entrySet()) { String name = e.getKey(); java.lang.reflect.Method pm = e.getValue(); java.lang.reflect.Method m = allM.get(name); Class[] args = m.getParameterTypes(); Class[] pargs = pm.getParameterTypes(); if (pargs.length == args.length) { //System.out.println("ambig: "+pm); //System.out.println(" with: "+m); //ambig: int addAll(int,Element) // with: int addAll(int,Collection) allM.put(name, null); // get rid of } } //System.out.println("allM: "+allM); return allMethods = allM; } } static Object fixupString(Object part) { if (part instanceof CharSequence && !(part instanceof String)) { return part.toString(); } else { return part; } } public static final class Special implements Comparable<Special> { String kind; Object value; public Special(String kind, Object value) { this.kind = kind; this.value = value; } public String getKind() { return kind; } public Object getValue() { return value; } @Override public boolean equals(Object o) { if (!(o instanceof Special)) { return false; } Special that = (Special) o; return this.kind.equals(that.kind) && this.value.equals(that.value); } @Override public int hashCode() { return kind.hashCode() * 65 + value.hashCode(); } public int compareTo(Special that) { int r = this.kind.compareTo(that.kind); if (r != 0) { return r; } return ((Comparable) this.value).compareTo(that.value); } @Override public String toString() { int split = kind.indexOf(' '); String pref = kind.substring(0, split < 0 ? 0 : split); String post = kind.substring(split + 1); return pref + value + post; } } /** Supports sorting of mixed content. Sorts strings first, * then Elements, then everything else (as Comparable). */ public static Comparator<Object> contentOrder() { return CONTENT_ORDER; } private static Comparator<Object> CONTENT_ORDER = new ContentComparator(); private static class ContentComparator implements Comparator<Object> { public int compare(Object o1, Object o2) { boolean cs1 = (o1 instanceof CharSequence); boolean cs2 = (o2 instanceof CharSequence); if (cs1 && cs2) { String s1 = (String) fixupString(o1); String s2 = (String) fixupString(o2); return s1.compareTo(s2); } if (cs1) { return 0 - 1; } if (cs2) { return 1 - 0; } boolean el1 = (o1 instanceof Element); boolean el2 = (o2 instanceof Element); if (el1 && el2) { return ((Element) o1).compareTo((Element) o2); } if (el1) { return 0 - 1; } if (el2) { return 1 - 0; } return ((Comparable) o1).compareTo(o2); } } /** Used to find, filter, or transform sub-elements. * When used as a predicate, the filter returns a null * value for false, and the original object value for true. * When used as a transformer, the filter may return * null, for no values, the original object, a new object, * or an anonymous Element (meaning multiple results). */ public interface Filter { Object filter(Object value); } /** Use this to find an element, perhaps with a given name. */ public static class ElementFilter implements Filter { /** Subclasses may override this to implement better value tests. * By default, it returns the element itself, thus recognizing * all elements, regardless of name. */ public Element filter(Element elem) { return elem; // override this } public final Object filter(Object value) { if (!(value instanceof Element)) { return null; } return filter((Element) value); } @Override public String toString() { return "<ElementFilter name='*'/>"; } } private static Filter elementFilter; public static Filter elementFilter() { return (elementFilter != null) ? elementFilter : (elementFilter = new ElementFilter()); } public static Filter elementFilter(final String name) { name.toString(); // null check return new ElementFilter() { @Override public Element filter(Element elem) { return name.equals(elem.name) ? elem : null; } @Override public String toString() { return "<ElementFilter name='" + name + "'/>"; } }; } public static Filter elementFilter(final Collection nameSet) { Objects.requireNonNull(nameSet); return new ElementFilter() { @Override public Element filter(Element elem) { return nameSet.contains(elem.name) ? elem : null; } @Override public String toString() { return "<ElementFilter name='" + nameSet + "'/>"; } }; } public static Filter elementFilter(String... nameSet) { Collection<String> ncoll = Arrays.asList(nameSet); if (nameSet.length > 10) { ncoll = new HashSet<String>(ncoll); } return elementFilter(ncoll); } /** Use this to find an element with a named attribute, * possibly with a particular value. * (Note that an attribute is missing if and only if its value is null.) */ public static class AttrFilter extends ElementFilter { protected final String attrName; public AttrFilter(String attrName) { this.attrName = attrName.toString(); } /** Subclasses may override this to implement better value tests. * By default, it returns true for any non-null value, thus * recognizing any attribute of the given name, regardless of value. */ public boolean test(String attrVal) { return attrVal != null; // override this } @Override public final Element filter(Element elem) { return test(elem.getAttr(attrName)) ? elem : null; } @Override public String toString() { return "<AttrFilter name='" + attrName + "' value='*'/>"; } } public static Filter attrFilter(String attrName) { return new AttrFilter(attrName); } public static Filter attrFilter(String attrName, final String attrVal) { if (attrVal == null) { return not(attrFilter(attrName)); } return new AttrFilter(attrName) { @Override public boolean test(String attrVal2) { return attrVal.equals(attrVal2); } @Override public String toString() { return "<AttrFilter name='" + attrName + "' value='" + attrVal + "'/>"; } }; } public static Filter attrFilter(Element matchThis, String attrName) { return attrFilter(attrName, matchThis.getAttr(attrName)); } /** Use this to find a sub-element of a given class. */ public static Filter classFilter(final Class clazz) { return new Filter() { public Object filter(Object value) { return clazz.isInstance(value) ? value : null; } @Override public String toString() { return "<ClassFilter class='" + clazz.getName() + "'/>"; } }; } private static Filter textFilter; public static Filter textFilter() { return (textFilter != null) ? textFilter : (textFilter = classFilter(CharSequence.class)); } private static Filter specialFilter; public static Filter specialFilter() { return (specialFilter != null) ? specialFilter : (specialFilter = classFilter(Special.class)); } private static Filter selfFilter; /** This filter always returns its own argument. */ public static Filter selfFilter() { if (selfFilter != null) { return selfFilter; } return selfFilter = new Filter() { public Object filter(Object value) { return value; } @Override public String toString() { return "<Self/>"; } }; } /** This filter always returns a fixed value, regardless of argument. */ public static Filter constantFilter(final Object value) { return new Filter() { public Object filter(Object ignore) { return value; } @Override public String toString() { return "<Constant>" + value + "</Constant>"; } }; } private static Filter nullFilter; public static Filter nullFilter() { return (nullFilter != null) ? nullFilter : (nullFilter = constantFilter(null)); } private static Filter emptyFilter; public static Filter emptyFilter() { return (emptyFilter != null) ? emptyFilter : (emptyFilter = constantFilter(Element.EMPTY)); } /** Use this to invert the logical sense of the given filter. */ public static Filter not(final Filter f) { return new Filter() { public Object filter(Object value) { return f.filter(value) == null ? value : null; } @Override public String toString() { return "<Not>" + f + "</Not>"; } }; } /** Use this to combine several filters with logical AND. * Returns either the first null or the last non-null value. */ public static Filter and(final Filter f0, final Filter f1) { return and(new Filter[]{f0, f1}); } public static Filter and(final Filter... fs) { switch (fs.length) { case 0: return selfFilter(); // always true (on non-null inputs) case 1: return fs[0]; } return new Filter() { public Object filter(Object value) { Object res = fs[0].filter(value); if (res != null) { res = fs[1].filter(value); for (int i = 2; res != null && i < fs.length; i++) { res = fs[i].filter(value); } } return res; } @Override public String toString() { return opToString("<And>", fs, "</And>"); } }; } /** Use this to combine several filters with logical OR. * Returns either the first non-null or the last null value. */ public static Filter or(final Filter f0, final Filter f1) { return or(new Filter[]{f0, f1}); } public static Filter or(final Filter... fs) { switch (fs.length) { case 0: return nullFilter(); case 1: return fs[0]; } return new Filter() { public Object filter(Object value) { Object res = fs[0].filter(value); if (res == null) { res = fs[1].filter(value); for (int i = 2; res == null && i < fs.length; i++) { res = fs[i].filter(value); } } return res; } @Override public String toString() { return opToString("<Or>", fs, "</Or>"); } }; } /** Use this to combine several filters with logical AND, * and where each non-null result is passed as the argument * to the next filter. * Returns either the first null or the last non-null value. */ public static Filter stack(final Filter f0, final Filter f1) { return stack(new Filter[]{f0, f1}); } public static Filter stack(final Filter... fs) { switch (fs.length) { case 0: return nullFilter(); case 1: return fs[0]; } return new Filter() { public Object filter(Object value) { Object res = fs[0].filter(value); if (res != null) { res = fs[1].filter(res); for (int i = 2; res != null && i < fs.length; i++) { res = fs[i].filter(res); } } return res; } @Override public String toString() { return opToString("<Stack>", fs, "</Stack>"); } }; } /** Copy everything produced by f to sink, using addContent. */ public static Filter content(final Filter f, final Collection<Object> sink) { return new Filter() { public Object filter(Object value) { Object res = f.filter(value); addContent(res, sink); return res; } @Override public String toString() { return opToString("<addContent>", new Object[]{f, sink}, "</addContent>"); } }; } /** Look down the tree using f, collecting fx, else recursing into x. * Identities: * <code> * findInTree(f, s) == findInTree(content(f, s)) * findInTree(f) == replaceInTree(and(f, selfFilter())). * </code> */ public static Filter findInTree(Filter f, Collection<Object> sink) { if (sink != null) { f = content(f, sink); } return findInTree(f); } /** Look down the tree using f, recursing into x unless fx. */ public static Filter findInTree(final Filter f) { return new Filter() { public Object filter(Object value) { Object res = f.filter(value); if (res != null) { return res; } if (value instanceof Element) { // recurse return ((Element) value).find(this); } return null; } @Override public String toString() { return opToString("<FindInTree>", new Object[]{f}, "</FindInTree>"); } }; } /** Look down the tree using f. Replace each x with fx, else recurse. * If post filter g is given, optionally replace with gx after recursion. */ public static Filter replaceInTree(final Filter f, final Filter g) { return new Filter() { public Object filter(Object value) { Object res = (f == null) ? null : f.filter(value); if (res != null) { return res; } if (value instanceof Element) { // recurse ((Element) value).replaceAll(this); // Optional postorder traversal: if (g != null) { res = g.filter(value); } } return res; // usually null, meaning no replacement } @Override public String toString() { return opToString("<ReplaceInTree>", new Object[]{f, g}, "</ReplaceInTree>"); } }; } public static Filter replaceInTree(Filter f) { Objects.requireNonNull(f); return replaceInTree(f, null); } /** Make a filter which calls this method on the given element. * If the method is static, the first argument is passed the * the subtree value being filtered. * If the method is non-static, the receiver is the subtree value itself. * <p> * Optionally, additional arguments may be specified. * <p> * If the filtered value does not match the receiver class * (or else the first argument type, if the method is static), * the filter returns null without invoking the method. * <p> * The returned filter value is the result returned from the method. * Optionally, a non-null special false result value may be specified. * If the result returned from the method is equal to that false value, * the filter will return null. */ public static Filter methodFilter(java.lang.reflect.Method m, Object[] extraArgs, Object falseResult) { return methodFilter(m, false, extraArgs, falseResult); } public static Filter methodFilter(java.lang.reflect.Method m, Object[] args) { return methodFilter(m, args, null); } public static Filter methodFilter(java.lang.reflect.Method m) { return methodFilter(m, null, null); } public static Filter testMethodFilter(java.lang.reflect.Method m, Object[] extraArgs, Object falseResult) { return methodFilter(m, true, extraArgs, falseResult); } public static Filter testMethodFilter(java.lang.reflect.Method m, Object[] extraArgs) { return methodFilter(m, true, extraArgs, zeroArgs.get(m.getReturnType())); } public static Filter testMethodFilter(java.lang.reflect.Method m) { return methodFilter(m, true, null, zeroArgs.get(m.getReturnType())); } private static Filter methodFilter(final java.lang.reflect.Method m, final boolean isTest, Object[] extraArgs, final Object falseResult) { Class[] params = m.getParameterTypes(); final boolean isStatic = java.lang.reflect.Modifier.isStatic(m.getModifiers()); int insertLen = (isStatic ? 1 : 0); if (insertLen + (extraArgs == null ? 0 : extraArgs.length) > params.length) { throw new IllegalArgumentException("too many arguments"); } final Object[] args = (params.length == insertLen) ? null : new Object[params.length]; final Class valueType = !isStatic ? m.getDeclaringClass() : params[0]; if (valueType.isPrimitive()) { throw new IllegalArgumentException("filtered value must be reference type"); } int fillp = insertLen; if (extraArgs != null) { for (int i = 0; i < extraArgs.length; i++) { args[fillp++] = extraArgs[i]; } } if (args != null) { while (fillp < args.length) { Class param = params[fillp]; args[fillp++] = param.isPrimitive() ? zeroArgs.get(param) : null; } } final Thread curt = Thread.currentThread(); class MFilt implements Filter { public Object filter(Object value) { if (!valueType.isInstance(value)) { return null; // filter fails quickly } Object[] args1 = args; if (isStatic) { if (args1 == null) { args1 = new Object[1]; } else if (curt != Thread.currentThread()) // Dirty hack to curtail array copying in common case. { args1 = (Object[]) args1.clone(); } args1[0] = value; } Object res; try { res = m.invoke(value, args1); } catch (java.lang.reflect.InvocationTargetException te) { Throwable ee = te.getCause(); if (ee instanceof RuntimeException) { throw (RuntimeException) ee; } if (ee instanceof Error) { throw (Error) ee; } throw new RuntimeException("throw in filter", ee); } catch (IllegalAccessException ee) { throw new RuntimeException("access error in filter", ee); } if (res == null) { if (!isTest && m.getReturnType() == Void.TYPE) { // Void methods return self by convention. // (But void "tests" always return false.) res = value; } } else { if (falseResult != null && falseResult.equals(res)) { res = null; } else if (isTest) { // Tests return self by convention. res = value; } } return res; } @Override public String toString() { return "<Method>" + m + "</Method>"; } } return new MFilt(); } private static HashMap<Class, Object> zeroArgs = new HashMap<Class, Object>(); static { zeroArgs.put(Boolean.TYPE, Boolean.FALSE); zeroArgs.put(Character.TYPE, new Character((char) 0)); zeroArgs.put(Byte.TYPE, new Byte((byte) 0)); zeroArgs.put(Short.TYPE, new Short((short) 0)); zeroArgs.put(Integer.TYPE, new Integer(0)); zeroArgs.put(Float.TYPE, new Float(0)); zeroArgs.put(Long.TYPE, new Long(0)); zeroArgs.put(Double.TYPE, new Double(0)); } private static String opToString(String s1, Object[] s2, String s3) { StringBuilder buf = new StringBuilder(s1); for (int i = 0; i < s2.length; i++) { if (s2[i] != null) { buf.append(s2[i]); } } buf.append(s3); return buf.toString(); } /** Call the filter on each list element x, and replace x with the * resulting filter value e, or its parts. * If e is null, keep x. (This eases use of partial-domain filters.) * If e is a TokenList or an anonymous Element, add e's parts * to the list instead of x. * Otherwise, replace x by e. * <p> * The effect at each list position <code>n</code> may be expressed * in terms of XMLKit.addContent as follows: * <pre> * Object e = f.filter(target.get(n)); * if (e != null) { * target.remove(n); * addContent(e, target.subList(n,n)); * } * </pre> * <p> * Note: To force deletion of x, simply have the filter return * Element.EMPTY or TokenList.EMPTY. * To force null filter values to have this effect, * use the expression: <code>or(f, emptyFilter())</code>. */ public static void replaceAll(Filter f, List<Object> target) { for (ListIterator<Object> i = target.listIterator(); i.hasNext();) { Object x = i.next(); Object fx = f.filter(x); if (fx == null) { // Unliked addContent, a null is a no-op here. // i.remove(); } else if (fx instanceof TokenList) { TokenList tl = (TokenList) fx; if (tl.size() == 1) { i.set(tl); } else { i.remove(); for (String part : tl) { i.add(part); } } } else if (fx instanceof Element && ((Element) fx).isAnonymous()) { Element anon = (Element) fx; if (anon.size() == 1) { i.set(anon); } else { i.remove(); for (Object part : anon) { i.add(part); } } } else if (x != fx) { i.set(fx); } } } /** If e is null, return zero. * If e is a TokenList or an anonymous Element, add e's parts * to the collection, and return the number of parts. * Otherwise, add e to the collection, and return one. * If the collection reference is null, the result is as if * a throwaway collection were used. */ public static int addContent(Object e, Collection<Object> sink) { if (e == null) { return 0; } else if (e instanceof TokenList) { TokenList tl = (TokenList) e; if (sink != null) { sink.addAll(tl); } return tl.size(); } else if (e instanceof Element && ((Element) e).isAnonymous()) { Element anon = (Element) e; if (sink != null) { sink.addAll(anon.asList()); } return anon.size(); } else { if (sink != null) { sink.add(e); } return 1; } } static Collection<Object> newCounterColl() { return new AbstractCollection<Object>() { int size; public int size() { return size; } @Override public boolean add(Object o) { ++size; return true; } public Iterator<Object> iterator() { throw new UnsupportedOperationException(); } }; } /** SAX2 document handler for building Element trees. */ private static class Builder implements ContentHandler, LexicalHandler { /*, EntityResolver, DTDHandler, ErrorHandler*/ Collection<Object> sink; boolean makeFrozen; boolean tokenizing; Builder(Collection<Object> sink, boolean tokenizing, boolean makeFrozen) { this.sink = sink; this.tokenizing = tokenizing; this.makeFrozen = makeFrozen; } Object[] parts = new Object[30]; int nparts = 0; int[] attrBases = new int[10]; // index into parts int[] elemBases = new int[10]; // index into parts int depth = -1; // index into attrBases, elemBases // Parts is organized this way: // | name0 | akey aval ... | subelem ... | name1 | ... | // The position of the first "akey" after name0 is attrBases[0]. // The position of the first "subelem" after name0 is elemBases[0]. // The position after the last part is always nparts. int mergeableToken = -1; // index into parts of recent CharSequence boolean inCData = false; void addPart(Object x) { //System.out.println("addPart "+x); if (nparts == parts.length) { Object[] newParts = new Object[parts.length * 2]; System.arraycopy(parts, 0, newParts, 0, parts.length); parts = newParts; } parts[nparts++] = x; } Object getMergeableToken() { if (mergeableToken == nparts - 1) { assert (parts[mergeableToken] instanceof CharSequence); return parts[nparts - 1]; } else { return null; } } void clearMergeableToken() { if (mergeableToken >= 0) { // Freeze temporary StringBuffers into strings. assert (parts[mergeableToken] instanceof CharSequence); parts[mergeableToken] = parts[mergeableToken].toString(); mergeableToken = -1; } } void setMergeableToken() { if (mergeableToken != nparts - 1) { clearMergeableToken(); mergeableToken = nparts - 1; assert (parts[mergeableToken] instanceof CharSequence); } } // ContentHandler callbacks public void startElement(String ns, String localName, String name, Attributes atts) { clearMergeableToken(); addPart(name.intern()); ++depth; if (depth == attrBases.length) { int oldlen = depth; int newlen = depth * 2; int[] newAB = new int[newlen]; int[] newEB = new int[newlen]; System.arraycopy(attrBases, 0, newAB, 0, oldlen); System.arraycopy(elemBases, 0, newEB, 0, oldlen); attrBases = newAB; elemBases = newEB; } attrBases[depth] = nparts; // Collect attributes. int na = atts.getLength(); for (int k = 0; k < na; k++) { addPart(atts.getQName(k).intern()); addPart(atts.getValue(k)); } // Get ready to collect elements. elemBases[depth] = nparts; } public void endElement(String ns, String localName, String name) { assert (depth >= 0); clearMergeableToken(); int ebase = elemBases[depth]; int elen = nparts - ebase; int abase = attrBases[depth]; int alen = ebase - abase; int nbase = abase - 1; int cap = alen + (makeFrozen ? 0 : NEED_SLOP) + elen; Element e = new Element((String) parts[nbase], elen, cap); // Set up attributes. for (int k = 0; k < alen; k += 2) { e.parts[cap - k - 2] = parts[abase + k + 0]; e.parts[cap - k - 1] = parts[abase + k + 1]; } // Set up sub-elements. System.arraycopy(parts, ebase, e.parts, 0, elen); // Back out of this level. --depth; nparts = nbase; assert (e.isFrozen() == makeFrozen); assert (e.size() == elen); assert (e.attrSize() * 2 == alen); if (depth >= 0) { addPart(e); } else { sink.add(e); } } public void startCDATA() { inCData = true; } public void endCDATA() { inCData = false; } public void characters(char[] buf, int off, int len) { boolean headSpace = false; boolean tailSpace = false; int firstLen; if (tokenizing && !inCData) { // Strip unquoted blanks. while (len > 0 && isWhitespace(buf[off])) { headSpace = true; ++off; --len; } if (len == 0) { tailSpace = true; // it is all space } while (len > 0 && isWhitespace(buf[off + len - 1])) { tailSpace = true; --len; } firstLen = 0; while (firstLen < len && !isWhitespace(buf[off + firstLen])) { ++firstLen; } } else { firstLen = len; } if (headSpace) { clearMergeableToken(); } boolean mergeAtEnd = !tailSpace; // If buffer was empty, or had only ignorable blanks, do nothing. if (len == 0) { return; } // Decide whether to merge some of these chars into a previous token. Object prev = getMergeableToken(); if (prev instanceof StringBuffer) { ((StringBuffer) prev).append(buf, off, firstLen); } else if (prev == null) { addPart(new String(buf, off, firstLen)); } else { // Merge two strings. String prevStr = prev.toString(); StringBuffer prevBuf = new StringBuffer(prevStr.length() + firstLen); prevBuf.append(prevStr); prevBuf.append(buf, off, firstLen); if (mergeAtEnd && len == firstLen) { // Replace previous string with new StringBuffer. parts[nparts - 1] = prevBuf; } else { // Freeze it now. parts[nparts - 1] = prevBuf.toString(); } } off += firstLen; len -= firstLen; if (len > 0) { // Appended only the first token. clearMergeableToken(); // Add the rest as separate parts. while (len > 0) { while (len > 0 && isWhitespace(buf[off])) { ++off; --len; } int nextLen = 0; while (nextLen < len && !isWhitespace(buf[off + nextLen])) { ++nextLen; } assert (nextLen > 0); addPart(new String(buf, off, nextLen)); off += nextLen; len -= nextLen; } } if (mergeAtEnd) { setMergeableToken(); } } public void ignorableWhitespace(char[] buf, int off, int len) { clearMergeableToken(); if (false) { characters(buf, off, len); clearMergeableToken(); } } public void comment(char[] buf, int off, int len) { addPart(new Special("<!-- -->", new String(buf, off, len))); } public void processingInstruction(String name, String instruction) { Element pi = new Element(name); pi.add(instruction); addPart(new Special("<? ?>", pi)); } public void skippedEntity(String name) { } public void startDTD(String name, String publicId, String systemId) { } public void endDTD() { } public void startEntity(String name) { } public void endEntity(String name) { } public void setDocumentLocator(org.xml.sax.Locator locator) { } public void startDocument() { } public void endDocument() { } public void startPrefixMapping(String prefix, String uri) { } public void endPrefixMapping(String prefix) { } } /** Produce a ContentHandler for use with an XML parser. * The object is <em>also</em> a LexicalHandler. * Every top-level Element produced will get added to sink. * All elements will be frozen iff makeFrozen is true. */ public static ContentHandler makeBuilder(Collection<Object> sink, boolean tokenizing, boolean makeFrozen) { return new Builder(sink, tokenizing, makeFrozen); } public static ContentHandler makeBuilder(Collection<Object> sink, boolean tokenizing) { return new Builder(sink, tokenizing, false); } public static ContentHandler makeBuilder(Collection<Object> sink) { return makeBuilder(sink, false, false); } public static Element readFrom(Reader in, boolean tokenizing, boolean makeFrozen) throws IOException { Element sink = new Element(); ContentHandler b = makeBuilder(sink.asList(), tokenizing, makeFrozen); XMLReader parser; try { parser = org.xml.sax.helpers.XMLReaderFactory.createXMLReader(); } catch (SAXException ee) { throw new Error(ee); } //parser.setFastStandalone(true); parser.setContentHandler(b); try { parser.setProperty("http://xml.org/sax/properties/lexical-handler", (LexicalHandler) b); } catch (SAXException ee) { // Ignore. We will miss the comments and whitespace. } try { parser.parse(new InputSource(in)); } catch (SAXParseException ee) { throw new RuntimeException("line " + ee.getLineNumber() + " col " + ee.getColumnNumber() + ": ", ee); } catch (SAXException ee) { throw new RuntimeException(ee); } switch (sink.size()) { case 0: return null; case 1: if (sink.get(0) instanceof Element) { return (Element) sink.get(0); } // fall through default: if (makeFrozen) { sink.shallowFreeze(); } return sink; } } public static Element readFrom(Reader in, boolean tokenizing) throws IOException { return readFrom(in, tokenizing, false); } public static Element readFrom(Reader in) throws IOException { return readFrom(in, false, false); } public static void prettyPrintTo(OutputStream out, Element e) throws IOException { prettyPrintTo(new OutputStreamWriter(out), e); } public static void prettyPrintTo(Writer out, Element e) throws IOException { Printer pr = new Printer(out); pr.pretty = true; pr.print(e); } static class Outputter { ContentHandler ch; LexicalHandler lh; Outputter(ContentHandler ch, LexicalHandler lh) { this.ch = ch; this.lh = lh; } AttributesImpl atts = new AttributesImpl(); // handy void output(Object x) throws SAXException { // Cf. jdom.org/jdom-b8/src/java/org/jdom/output/SAXOutputter.java if (x instanceof Element) { Element e = (Element) x; atts.clear(); for (int asize = e.attrSize(), k = 0; k < asize; k++) { String key = e.getAttrName(k); String val = e.getAttr(k); atts.addAttribute("", "", key, "CDATA", val); } ch.startElement("", "", e.getName(), atts); for (int i = 0; i < e.size(); i++) { output(e.get(i)); } ch.endElement("", "", e.getName()); } else if (x instanceof Special) { Special sp = (Special) x; if (sp.kind.startsWith("<!--")) { char[] chars = sp.value.toString().toCharArray(); lh.comment(chars, 0, chars.length); } else if (sp.kind.startsWith("<?")) { Element nameInstr = (Element) sp.value; ch.processingInstruction(nameInstr.name, nameInstr.get(0).toString()); } else { // drop silently } } else { char[] chars = x.toString().toCharArray(); ch.characters(chars, 0, chars.length); } } } public static class Printer { public Writer w; public boolean tokenizing; public boolean pretty; public boolean abbreviated; // nonstandard format cuts down on noise int depth = 0; boolean prevStr; int tabStop = 2; public Printer(Writer w) { this.w = w; } public Printer() { StringWriter sw = new StringWriter(); this.w = sw; } public String nextString() { StringBuffer sb = ((StringWriter) w).getBuffer(); String next = sb.toString(); sb.setLength(0); // reset return next; } void indent(int depth) throws IOException { if (depth > 0) { w.write("\n"); } int nsp = tabStop * depth; while (nsp > 0) { String s = " "; String t = s.substring(0, nsp < s.length() ? nsp : s.length()); w.write(t); nsp -= t.length(); } } public void print(Element e) throws IOException { if (e.isAnonymous()) { printParts(e); return; } printRecursive(e); } public void println(Element e) throws IOException { print(e); w.write("\n"); w.flush(); } public void printRecursive(Element e) throws IOException { boolean indented = false; if (pretty && !prevStr && e.size() + e.attrSize() > 0) { indent(depth); indented = true; } w.write("<"); w.write(e.name); for (int asize = e.attrSize(), k = 0; k < asize; k++) { String key = e.getAttrName(k); String val = e.getAttr(k); w.write(" "); w.write(key); w.write("="); if (val == null) { w.write("null"); // Should not happen.... } else if (val.indexOf("\"") < 0) { w.write("\""); writeToken(val, '"', w); w.write("\""); } else { w.write("'"); writeToken(val, '\'', w); w.write("'"); } } if (e.size() == 0) { w.write("/>"); } else { ++depth; if (abbreviated) { w.write("/"); } else { w.write(">"); } prevStr = false; printParts(e); if (abbreviated) { w.write(">"); } else { if (indented && !prevStr) { indent(depth - 1); } w.write("</"); w.write(e.name); w.write(">"); } prevStr = false; --depth; } } private void printParts(Element e) throws IOException { for (int i = 0; i < e.size(); i++) { Object x = e.get(i); if (x instanceof Element) { printRecursive((Element) x); prevStr = false; } else if (x instanceof Special) { w.write(((Special) x).toString()); prevStr = false; } else { String s = String.valueOf(x); if (pretty) { s = s.trim(); if (s.length() == 0) { continue; } } if (prevStr) { w.write(' '); } writeToken(s, tokenizing ? ' ' : (char) -1, w); prevStr = true; } if (pretty && depth == 0) { w.write("\n"); prevStr = false; } } } } public static void output(Object e, ContentHandler ch, LexicalHandler lh) throws SAXException { new Outputter(ch, lh).output(e); } public static void output(Object e, ContentHandler ch) throws SAXException { if (ch instanceof LexicalHandler) { output(e, ch, (LexicalHandler) ch); } else { output(e, ch, null); } } public static void writeToken(String val, char quote, Writer w) throws IOException { int len = val.length(); boolean canUseCData = (quote != '"' && quote != '\''); int vpos = 0; for (int i = 0; i < len; i++) { char ch = val.charAt(i); if ((ch == '<' || ch == '&' || ch == '>' || ch == quote) || (quote == ' ' && isWhitespace(ch))) { if (canUseCData) { assert (vpos == 0); writeCData(val, w); return; } else { if (vpos < i) { w.write(val, vpos, i - vpos); } String esc; switch (ch) { case '&': esc = "&"; break; case '<': esc = "<"; break; case '\'': esc = "'"; break; case '"': esc = """; break; case '>': esc = ">"; break; default: esc = "&#" + (int) ch + ";"; break; } w.write(esc); vpos = i + 1; // skip escaped char } } } // write the unquoted tail w.write(val, vpos, val.length() - vpos); } public static void writeCData(String val, Writer w) throws IOException { String begCData = "<![CDATA["; String endCData = "]]>"; w.write(begCData); for (int vpos = 0, split;; vpos = split) { split = val.indexOf(endCData, vpos); if (split < 0) { w.write(val, vpos, val.length() - vpos); w.write(endCData); return; } split += 2; // bisect the "]]>" goo w.write(val, vpos, split - vpos); w.write(endCData); w.write(begCData); } } public static TokenList convertToList(String str) { if (str == null) { return null; } return new TokenList(str); } /** If str is null, empty, or blank, returns null. * Otherwise, return a Double if str spells a double value and contains '.' or 'e'. * Otherwise, return an Integer if str spells an int value. * Otherwise, return a Long if str spells a long value. * Otherwise, return a BigInteger for the string. * Otherwise, throw NumberFormatException. */ public static Number convertToNumber(String str) { if (str == null) { return null; } str = str.trim(); if (str.length() == 0) { return null; } if (str.indexOf('.') >= 0 || str.indexOf('e') >= 0 || str.indexOf('E') >= 0) { return Double.valueOf(str); } try { long lval = Long.parseLong(str); if (lval == (int) lval) { // Narrow to Integer, if possible. return new Integer((int) lval); } return new Long(lval); } catch (NumberFormatException ee) { // Could not represent it as a long. return new java.math.BigInteger(str, 10); } } public static Number convertToNumber(String str, Number dflt) { Number n = convertToNumber(str); return (n == null) ? dflt : n; } public static long convertToLong(String str) { return convertToLong(str, 0); } public static long convertToLong(String str, long dflt) { Number n = convertToNumber(str); return (n == null) ? dflt : n.longValue(); } public static double convertToDouble(String str) { return convertToDouble(str, 0); } public static double convertToDouble(String str, double dflt) { Number n = convertToNumber(str); return (n == null) ? dflt : n.doubleValue(); } // Testing: public static void main(String... av) throws Exception { Element.method("getAttr"); //new org.jdom.input.SAXBuilder().build(file).getRootElement(); //jdom.org/jdom-b8/src/java/org/jdom/input/SAXBuilder.java //Document build(InputSource in) throws JDOMException int reps = 0; boolean tokenizing = false; boolean makeFrozen = false; if (av.length > 0) { tokenizing = true; try { reps = Integer.parseInt(av[0]); } catch (NumberFormatException ee) { } } Reader inR = new BufferedReader(new InputStreamReader(System.in)); String inS = null; if (reps > 1) { StringWriter inBufR = new StringWriter(1 << 14); char[] cbuf = new char[1024]; for (int nr; (nr = inR.read(cbuf)) >= 0;) { inBufR.write(cbuf, 0, nr); } inS = inBufR.toString(); inR = new StringReader(inS); } Element e = XMLKit.readFrom(inR, tokenizing, makeFrozen); System.out.println("transform = " + e.findAll(methodFilter(Element.method("prettyString")))); System.out.println("transform = " + e.findAll(testMethodFilter(Element.method("hasText")))); long tm0 = 0; int warmup = 10; for (int i = 1; i < reps; i++) { inR = new StringReader(inS); readFrom(inR, tokenizing, makeFrozen); if (i == warmup) { System.out.println("Start timing..."); tm0 = System.currentTimeMillis(); } } if (tm0 != 0) { long tm1 = System.currentTimeMillis(); System.out.println((reps - warmup) + " in " + (tm1 - tm0) + " ms"); } System.out.println("hashCode = " + e.hashCode()); String eStr = e.toString(); System.out.println(eStr); Element e2 = readFrom(new StringReader(eStr), tokenizing, !makeFrozen); System.out.println("hashCode = " + e2.hashCode()); if (!e.equals(e2)) { System.out.println("**** NOT EQUAL 1\n" + e2); } e = e.deepCopy(); System.out.println("hashCode = " + e.hashCode()); if (!e.equals(e2)) { System.out.println("**** NOT EQUAL 2"); } e2.shallowFreeze(); System.out.println("hashCode = " + e2.hashCode()); if (!e.equals(e2)) { System.out.println("**** NOT EQUAL 3"); } if (false) { System.out.println(e); } else { prettyPrintTo(new OutputStreamWriter(System.out), e); } System.out.println("Flat text:|" + e.getFlatText() + "|"); { System.out.println("<!--- Sorted: --->"); Element ce = e.copyContentOnly(); ce.sort(); prettyPrintTo(new OutputStreamWriter(System.out), ce); } { System.out.println("<!--- Trimmed: --->"); Element tr = e.deepCopy(); findInTree(testMethodFilter(Element.method("trimText"))).filter(tr); System.out.println(tr); } { System.out.println("<!--- Unstrung: --->"); Element us = e.deepCopy(); int nr = us.retainAllInTree(elementFilter(), null); System.out.println("nr=" + nr); System.out.println(us); } { System.out.println("<!--- Rollup: --->"); Element ru = e.deepCopy(); Filter makeAnonF = methodFilter(Element.method("setName"), new Object[]{ANON_NAME}); Filter testSizeF = testMethodFilter(Element.method("size")); Filter walk = replaceInTree(and(not(elementFilter()), emptyFilter()), and(testSizeF, makeAnonF)); ru = (Element) walk.filter(ru); //System.out.println(ru); prettyPrintTo(new OutputStreamWriter(System.out), ru); } } static boolean isWhitespace(char c) { switch (c) { case 0x20: case 0x09: case 0x0D: case 0x0A: return true; } return false; } }