added a convenience constructor to XML.java
[org.ibex.util.git] / src / org / ibex / util / XML.java
index d0775c7..64940c2 100644 (file)
@@ -1,29 +1,35 @@
-// Copyright (C) 2003 Adam Megacz <adam@ibex.org> all rights reserved.
-//
-// You may modify, copy, and redistribute this code under the terms of
-// the GNU Library Public License version 2.1, with the exception of
-// the portion of clause 6a after the semicolon (aka the "obnoxious
-// relink clause")
+// Copyright 2000-2005 the Contributors, as shown in the revision logs.
+// Licensed under the Apache Public Source License 2.0 ("the License").
+// You may not use this file except in compliance with the License.
 
 package org.ibex.util;
 
-import java.io.Reader;
-import java.io.IOException;
 import java.io.EOFException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.Writer;
+import java.io.Serializable;
 
 /**
- * An Event-Driving, Non-Validating XML Parser with Namespace support.
+ * An non-validating XML Parser with Namespace support.
+ *
+ * <h3>SAX-like usage</h3> 
  *
- * A subclass can implement the abstract functions for receiving details
- * about an xml file as it is parsed. To initate a parse, use the parse()
- * function. 
+ * <p>Subclass XML and implement the four abstract functions. Call
+ * <tt>parse()</tt> to begin synchronously processing reader input.
+ * Any number of documents may be <tt>parse()</tt>ed.</p>
+ *
+ * <h3>DOM-like usage</h3>
+ *
+ * <p>Instansiate <tt>XML.Document</tt> and call <tt>parse()</tt>. The
+ * root of the document tree can be accessed by calling <tt>getRoot()</tt>.
+ * See the public interface <tt>XML.Element</tt> for tree traversal.</p>
+ *
+ * <p>Only one document may be <tt>parse()</tt>ed per <tt>XML.Document</tt>
+ * instance.</p>
  *
  * <h3>Implementation Notes</h3>
- * <p>As the parser traverses into an element, it adds it to the linked list
- * called <tt>elements</tt>. However, <tt>elements</tt> has been pre-filled
- * with instances of the Element inner class. So in the vast majority of
- * cases, the pointer current is moved along one, and the values for the
- * new element are filled into the current object.</p>
  *
  * <p>This parser supports all the unicode ranges required by the XML
  * Specification. However, it is optimised for well-formed ASCII documents.
@@ -43,20 +49,22 @@ import java.io.EOFException;
  *   Violation and is not recoverable</li>
  * </ul> 
  *
- * @author David Crawshaw 
+ * @author crawshaw@ibex.org
  * @see <a href="http://w3.org/TR/REC-xml">XML Specification</a> 
  * @see <a href="http://w3.org/TR/REC-xml-names">XML Namespaces</a>
  */
 public abstract class XML
 {
-    /////////////////////////////////////////////////////////////////////////////////////////////
-    // XML Parser
-    /////////////////////////////////////////////////////////////////////////////////////////////
+    // XML Parser /////////////////////////////////////////////////////////////
+
+    /** Default initial buffer size. */
+    public static final int BUFFER_SIZE = 256;
 
-    public static final int BUFFER_SIZE = 255;
+    private static final int DEFAULT_ATTR_COUNT = 2;
+    private static final int DEFAULT_PFX_COUNT  = 2;
 
-    /** static pool of XML.Element instances shared by all XML Parsers. */
-    private static final Queue elements = new Queue(30);
+    /** static pool of XML.Elem instances shared by all XML Parsers. */
+    private static final Basket.List elements = new Basket.Array();
 
     private static final char[] single_amp  = new char[] { '&'  };
     private static final char[] single_apos = new char[] { '\'' };
@@ -64,6 +72,8 @@ public abstract class XML
     private static final char[] single_lt   = new char[] { '<'  };
     private static final char[] single_quot = new char[] { '"'  };
 
+    private final boolean poolElements;
+
     private int line;
     private int col;
 
@@ -73,19 +83,25 @@ public abstract class XML
     private int    base;  // base+off == distance into the stream
     private int    len;
 
-    private Element current;
+    private Elem current;
 
     // used in readEntity() to process a single character without creating a new array
     private char[] singlechar = new char[1];
 
 
-    public XML() { this(BUFFER_SIZE); }
+    /** Creates a new XML parser with that has a default initial
+     *  buffer size and reuses its signal objects. */
+    protected XML() { this(BUFFER_SIZE, true); }
 
-    public XML(int bSize) {
+    /** Creates a new XML parser.
+     *  @param bSize initial buffer size.
+     *  @param poolElements if true the objects passed to the signal functions are reused.
+     */
+    protected XML(int bSize, boolean poolElements) {
         buf = new char[bSize];
+        this.poolElements = poolElements;
 
-        current = (Element)elements.remove(false);
-        if (current == null) current = new Element();
+        current = element();
     }
 
     /** Returns the line number at the beginning of the last process call. */
@@ -97,44 +113,102 @@ public abstract class XML
     /** Returns the global file offset at the beginning of the last process call. */
     public int getGlobalOffset() { return base + off; }
 
-    /**
-     * Parse given input and call the abstract event functions.
-     *
-     * Careful with threading, as this function is not synchronized.
-     */ 
+    /** Set the reader used as a data source by the XML parser. */
+    public void setReader(Reader reader) { in = reader; }
+
+    /** Parse given input and call the abstract event functions.
+     *  Equivalent to calling <tt>setReader(reader); parse();</tt>. */
     public final void parse(Reader reader) throws IOException, Exn {
-        in  = reader;
+        setReader(reader); parse();
+    }
+
+    /** Parse given input and call the abstract event functions.
+     *
+     * <p>This function is synchronous with event functions, meaning it
+     * will only return <i>after</i> it has finished calling all signal
+     * functions.</p>
+     */
+    public final void parse() throws IOException, Exn {
         off = len = 0;
         line = col = 1;
-
-        clear(); // clean up possible mid-way linked-list element
+        current = null;
 
         try {
             // process the stream
             while (true) {
                 if (!buffer(1)) {
-                    if (current.qName == null) break;
-                    throw new Exn("reached eof without closing <"+current.qName+"> element", Exn.WFC, getLine(), getCol());
+                    if (current == null) break;
+                    throw new Exn("reached eof without closing <"+current.qName+"> element",
+                                  Exn.WFC, getLine(), getCol());
                 }
 
-                if (buf[off] == '<') readTag();
-                readChars(current.qName != null);
+                if (buf[off] == '<') {
+                    if (current == null) current = element();
+                    readTag();
+                }
+                readChars(current != null);
             }
         } finally { clear(); } // clean up elements
     }
 
-    /** remove any leftover elements from the linked list and queue them */
-    private final void clear() {
-        for (Element last = current; current.parent != null; ) {
-            current = current.parent;
-            last.clear();
-            elements.append(last);
+    /** Parses the next tag or block of character data, calling the
+     *  abstract event functions to process the data.
+     *
+     *  @return True if successfully processed a block of data.
+     */
+    public boolean parseNext() throws IOException, Exn {
+        if (!buffer(1)) {
+            if (current == null) return false;
+            throw new Exn("reached eof without closing <"+current.qName+"> element",
+                          Exn.WFC, getLine(), getCol());
+        }
+
+        // move through meaningless data
+        if (current != null) readChars(false);
+
+        if (buf[off] == '<') {
+            // proecess and return a tag
+            if (current == null) current = element();
+            readTag();
+        } else {
+            // processes a block of character data
+            readChars(true);
+        }
+
+        return true;
+    }
+
+    /** Returns the current Tree.Element, or null if outside the root node. */
+    public Tree.Element current() { return current; }
+
+    /** Empty the linked list. */
+    private void clear() {
+        while (current != null) {
+            Elem l = current;
+            current = (Elem)current.parent;
+            element(l);
         }
-        current.clear();
     }
 
-    /** reads in a tag. expects <tt>buf[off] == '&#60;'</tt> */
-    private final void readTag() throws IOException, Exn {
+    /** Provides a fresh element. */
+    private Elem element() {
+        Elem e = null;
+        if (poolElements) synchronized (elements) {
+            if (elements.size() > 0) e = (Elem)elements.remove(elements.size() - 1);
+        }
+        if (e == null) e = new Elem();
+        return e;
+    }
+
+    /** Frees a used element. */
+    private void element(Elem e) { 
+        if (e == null || !poolElements) return;
+        e.clear(); synchronized (elements) { elements.add(e); }
+    }
+
+
+    /** Reads in a tag. Expects <tt>buf[off] == '&#60;'</tt>. */
+    private void readTag() throws IOException, Exn {
         // Start Tag    '<' Name (S Attribute)* S? '>'
         boolean starttag  = true;
 
@@ -296,9 +370,7 @@ public abstract class XML
                 // create the in-memory element representation of this beast
                 // if current.qName == null then this is the root element we're dealing with
                 if (current.qName != null) {
-                    Element next = (Element)elements.remove(false);
-                    if (next == null) next = new Element();
-                    //next.clear(); // TODO: remove as elements now checked as they're added to the queue
+                    Elem next = element();
                     next.parent = current;
                     current = next;
                 }
@@ -323,9 +395,10 @@ public abstract class XML
                 }
 
                 // work out the uri of this element
-                current.uri = current.getUri(current.getPrefix()); 
-                if (current.getUri().equals("") && current.getPrefix() != null)
-                    current.addError(new Exn("undefined prefix '"+current.getPrefix()+"'", Exn.NC, getLine(), getCol()));
+                String p = current.getPrefix();
+                String uri = current.uri(p);
+                if (uri == null && p != null && !p.equals("")) error(new Exn("undefined prefix '"+current.getPrefix()+"'", Exn.NC, getLine(), getCol()));
+                else current.uri = uri;
 
             } else {
                 // this is an end-of-element tag
@@ -355,16 +428,9 @@ public abstract class XML
             if (endtag) {
                 endElement(current);
 
-                // we just closed an element, so remove it from the element 'stack'
-                if (current.getParent() == null) {
-                    // we just finished the root element
-                    current.clear(); 
-                } else {
-                    Element last = current;
-                    current = current.parent;
-                    last.clear();
-                    elements.append(last);
-                }
+                Elem l = current;
+                current = (Elem)current.parent;
+                element(l);
             }
         }
     }
@@ -438,24 +504,24 @@ public abstract class XML
 
         // process attribute
         if (p != null && p.equals("xmlns")) {
-            current.addUri(n, v);
+            current.addPrefix(n, v);
         } else if (n.equals("xmlns")) {
-            if (current.getUri().equals("")) {
-                current.addUri("", v);
+            if (current.getUri() == null || current.getUri().equals("")) {
+                current.addPrefix("", v);
             } else {
-                current.addError(new Exn("default namespace definition repeated", Exn.NC, getLine(), getCol()));
+                error(new Exn("default namespace definition repeated", Exn.NC, getLine(), getCol()));
             }
         } else {
             // find attribute uri
-            u = current.getUri(p); 
-            if (p != null && u.equals("")) current.addError(new Exn("undefined attribute prefix '"+p+"'", Exn.NC, getLine(), getCol()));
+            u = current.uri(p); 
+            if (u == null && p != null) error(new Exn("undefined attribute prefix '"+p+"'", Exn.NC, getLine(), getCol()));
 
             // check to see if attribute is a repeat
-            for (int i=0; current.len > i; i++) if (n.equals(current.getAttrKey(i)) && u.equals(current.getAttrUri(i))) throw new Exn(
+            for (int i=0; current.attrSize() > i; i++) if (n.equals(current.getKey(i)) && u.equals(current.getUri(i))) throw new Exn(
                 "attribute name '"+n+"' may not appear more than once in the same element tag", Exn.WFC, getLine(), getCol()
             );
 
-            current.addAttr(n, v, u); 
+            current.addAttr(n, v, u, p); 
         }
     }
 
@@ -590,18 +656,24 @@ public abstract class XML
     }
 
     /**
-     * reads until a <tt>&#60;</tt> symbol is encountered
+     * Reads until a <tt>&#60;</tt> symbol is encountered.
      * @param p If true call the characters(char[],int,int) funciton for the processed characters 
      */
     private final void readChars(boolean p) throws IOException, Exn {
+        boolean lastWhite = false;
         int ref;
 
         for (boolean more = true; more;) {
             if (!buffer(1)) return;
+            boolean readWhite = false;
 
             buf: for (ref = 0; ref < len; ref++) {
+
                 switch (buf[off+ref]) {
                     case '\r': // windows or macos9 newline
+                        if (lastWhite) { readWhite = true; break buf; }
+                        lastWhite = true;
+
                         // normalise and process
                         buf[off+ref] = '\n'; ref++;
                         if (p) characters(buf, off, ref);
@@ -614,28 +686,49 @@ public abstract class XML
                         break;
 
                     case '\n': // unix newline
+                        if (lastWhite) { readWhite = true; break buf; }
+                        lastWhite = true;
+
                         ref++;
                         if (p) characters(buf, off, ref);
                         off += ref; len -= ref; ref = -1;
                         line++; col = 1;
+                        if (buffer(1) && S(buf[off])) {
+                            readWhite = true; break buf;
+                        }
                         break;
 
+                    case ' ':
+                    case '\t':
+                        if (lastWhite) { readWhite = true; break buf; }
+                        lastWhite = true;
+                        break;
+
+                    case '<':  // end of chars section
+                        more = false;
+                        break buf;
+
                     case '&':  // entity
                         if (p) {
                             if (ref > 0) characters(buf, off, ref);
                             off += ref; len -= ref; ref = -1;
                             readEntity();
                         }
-                        break;
-
-                    case '<':  // end of chars section
-                        more = false;
-                        break buf;
+                    default:
+                        lastWhite = false;
                 }
             }
 
-            if (p && ref > 0) characters(buf, off, ref);
-            off += ref; len -= ref; col += ref;
+            if (ref > 0) {
+                if (p) characters(buf, off, ref);
+                off += ref; len -= ref; col += ref;
+            }
+
+            if (readWhite) {
+                readWhitespace();
+                more = buffer(1) && !(buf[off] == '<');
+                readWhite = false;
+            }
         }
     }
 
@@ -656,8 +749,7 @@ public abstract class XML
                         line++; col = 1;
 
                         // windows double-char newline; skip the next char
-                        if (!buffer(1)) return;
-                        if (buf[off] == '\n') { off++; len--; }
+                        if (buffer(1) && buf[off] == '\n') { off++; len--; }
                         break;
 
                     case '\n': // unix newline
@@ -676,12 +768,15 @@ public abstract class XML
                 }
             }
 
-            off += ref; len -= ref; col += ref;
+            if (ref > 0) {
+                whitespace(buf, off, ref);
+                off += ref; len -= ref; col += ref;
+            }
         }
     }
 
     /**
-     * attempt to fill the buffer.
+     * Attempt to fill the buffer.
      *
      * @param min Minimum number of characters to read (even if we have to block to do it).
      * @return return false if min can't be reached.
@@ -715,20 +810,17 @@ public abstract class XML
     }
 
 
-    /////////////////////////////////////////////////////////////////////////////////////////////
-    // Abstract SAX-Like Interface
-    /////////////////////////////////////////////////////////////////////////////////////////////
+    // SAX-like Interface /////////////////////////////////////////////////////
 
-    /**
-     * Called when the start of an element is processed.
+    /** Called when the start of an element is processed.
      *
-     * <p><b>DO NOT</b> store a reference to the Element object, as
-     * they are reused by XML Parser.</p>
+     * <p>If poolElements == true (default), <b>DO NOT</b> store a
+     * reference to the Element object, as they are reused by
+     * XML Parser.</p>
      */ 
-    public abstract void startElement(Element e) throws Exn;
+    public abstract void startElement(Tree.Element e) throws Exn;
 
-    /**
-     * Represents up to a line of character data. 
+    /** Called when up to a line of character data is processed. 
      *
      * <p>Newlines are all normalised to the Unix \n as per the XML Spec,
      * and a newline will only appear as the last character in the passed
@@ -740,168 +832,333 @@ public abstract class XML
      */
     public abstract void characters(char[] ch, int start, int length) throws Exn, IOException;
 
-    /** Represents up to a line of ignorable whitespace. */
-    public abstract void whitespace(char[] ch, int start, int length) throws Exn, IOException;
+    /** Called when the end of an Tree.Element is processed. */
+    public abstract void endElement(Tree.Element e) throws Exn, IOException;
 
-    /** Represents the end of an Element. */
-    public abstract void endElement(Element e) throws Exn, IOException;
+    /** Optional callback; called when when up to a line of ignorable whitespace is processed. */
+    public void whitespace(char[] ch, int start, int length) throws Exn, IOException {}
 
+    /** Optonal callback; called when a recoverable parsing error has been encountered. */
+    public void error(Exn e) throws Exn, IOException {}
 
-    /////////////////////////////////////////////////////////////////////////////////////////////
-    // Inner Classes for Parser Support
-    /////////////////////////////////////////////////////////////////////////////////////////////
+    // DOM-like Interface /////////////////////////////////////////////////////
 
-    /**
-     * Represents an element in an XML document. Stores a reference to its
-     * parent, forming a one-way linked list.
+    /** A Document Object Model extension to the XML Parser.
      *
-     * Element objects are reused, so client code making use of them must
-     * drop their references after the specific element process function
-     * has returned.
+     *  <p>To use, instaniate XML.Document and call parse(Reader).The
+     *  full Block tree can then be accessed starting from the root
+     *  element by calling getRoot().</p>
      */
-    public static final class Element {
+    public static class Document {
+        private final DXML xml;
+        private Tree.Element root = null;
 
-        private static final int DEFAULT_ATTR_SIZE = 10;
+        /** Creates a new XML.Document. Default initial buffer size is used. */
+        public Document() { this(BUFFER_SIZE); }
 
-        protected Element parent = null;
+        /** Creates a new XML.Document with a sepcified initial buffer size. */
+        public Document(int bSize) { xml = new DXML(bSize, false); }
 
-        protected String uri = null;
-        protected String localName = null;
-        protected String qName = null;
-        protected String prefix = null;
+        /** Returns the root Tree.Element of the parsed xml document. */
+        public Tree.Element getRoot() { return root; }
 
-        protected Hash urimap = new Hash(3,3);
+        /** Sets the root element of this document. */
+        public void setRoot(Tree.Element e) { root = e; }
 
-        protected String[] keys = new String[DEFAULT_ATTR_SIZE];
-        protected String[] vals = new String[DEFAULT_ATTR_SIZE];
-        protected String[] uris = new String[DEFAULT_ATTR_SIZE];
-        protected int len = 0;
+        /** Parse given input create the document model. */
+        public void parse(Reader r) throws IOException, Exn { xml.parse(r); }
 
-        protected Exn[] errors = new Exn[] {};
+        /** Returns a character representation of this document. */
+        /*public String toXML() throws IOException { FIXME
+            StringWriter w = new StringWriter(); toXML(w); return w.toString();
+        }*/
+        
+        /** Writes the character representation of this document to
+         *  the given writer. Calls <tt>root.toXML(Writer)</tt>.*/
+        //public void toXML(Writer w) throws IOException { if (root == null) return; root.toXML(w); }
 
-        /** Parent of current element. */
-        public Element getParent() { return parent; }
+        /** Used to hide implementation from public interface. */
+        private final class DXML extends XML {
+            private StringBuffer chars = null;
 
-        /** Qualified Name of current element.  XML Namespace Spec 14-Jan-1999 [6] */
-        public String getQName() { return qName; }
-
-        /** LocalPart of current element. XML Namespace Spec 14-Jan-1999 [8] */
-        public String getLocalName() { return localName; }
-
-        /** Prefix of current element. Substring of qName. XML Namespace Spec 14-Jan-1999 [7] */
-        public String getPrefix() { return prefix; }
+            private DXML(int b, boolean r) { super(b, r); }
 
-        // HACK
-        public Hash getUriMap() {
-            Hash map = new Hash();
-            for (Element e = this; e != null; e = e.getParent()) {
-                java.util.Enumeration en = e.urimap.keys();
-                while(en.hasMoreElements()) {
-                    String key = (String)en.nextElement();
-                    String val = getUri(key);
-                    map.put(key, val);
+            public void startElement(Tree.Element e) {
+                if (root == null) root = e;
+                else {
+                    if (chars != null) addText((Tree.Element)e.getParent());
+                    e.getParent().getChildren().add(e);
                 }
             }
-            return map;
-        }
-
-        /** URI of current tag. XML Namespace Spec 14-Jan-1999 section 1 */
-        public String getUri() { return getUri(prefix); }
+            public void characters(char[] ch, int s, int l) {
+                if (chars == null) chars = new StringBuffer();
+                chars.append(ch, s, l);
+            }
+            public void endElement(Tree.Element e) { if (chars != null) addText(e); }
 
-        /** URI of a given prefix. Never returns null, instead gives "". */
-        public String getUri(String p) {
-            String ret = null;
-            for (Element e = this; e != null && ret == null; e = e.getParent()) {
-                ret = (String)e.urimap.get(p == null ? "" : p);
+            private void addText(Tree.Element e) {
+                e.getChildren().add(new Text(e, chars.toString())); chars = null;
             }
-            return ret == null ? "" : ret;
         }
+    } 
 
-        /** An array of attribute names. */
-        public String getAttrKey(int pos) { return len > pos ? keys[pos] : null; }
+    // Pull Interface /////////////////////////////////////////////////////////
 
-        /** An array of attribute values. */
-        public String getAttrVal(int pos) { return len > pos ? vals[pos] : null; }
+    public static class Stream implements Tree.Stream {
+        private final SXML xml;
+        private int depth = -1;
 
-        /** An array of attribute uris. */
-        public String getAttrUri(int pos) { return len > pos ? uris[pos] : null; }
+        /** Creates a new XML.Stream. Default initial buffer size is used. */
+        public Stream() { this(null, BUFFER_SIZE); }
 
-        /** Current number of attributes in the element. */
-        public int getAttrLen() { return len; }
+        public Stream(Reader in) { this(in, BUFFER_SIZE); }
 
-        /** Poor performance, but easier to use when speed is not a concern */
-        public Hash getAttrHash() {
-            Hash ret = new Hash(getAttrLen() * 2, 3);
-            for(int i=0; i<len; i++)
-                ret.put(getAttrKey(i), getAttrVal(i));
-            return ret;
+        /** Creates a new XML.Stram with a sepcified character source and
+         *  initial buffer size. */
+        public Stream(Reader in, int bSize) {
+            xml = new SXML(bSize); setReader(in);
         }
 
-        /** Poor performance, but easier to use */
-        public String getAttrVal(String key) {
-            for(int i=0; i<len; i++) if (keys[i].equals(key)) return vals[i];
-            return null;
+        public int available() { return 0; }
+        public int getDepth() { return depth; }
+
+        /** Set the current character source. */
+        public void setReader(Reader in) { xml.setReader(in); }
+
+        public Tree.Leaf next() throws IOException { return xml.next(); }
+
+        public void skip() throws IOException {
+            int d = depth;
+            do { next(); } while (d != depth);
         }
+        /** Used to hide implementation from public interface. */
+        private final class SXML extends XML {
+            private StringBuffer chars = new StringBuffer();
+            private Tree.Element element = null;
+
+            private SXML(int b) { super(b, false); }
+            public void startElement(Tree.Element e) { depth++; element = e; }
+            public void endElement(Tree.Element e)   { depth--; element = e; }
+            public void characters(char[] ch, int s, int l) { chars.append(ch, s, l); }
+            private Tree.Leaf next() throws IOException, Exn {
+                if (!parseNext()) return null;
+                if (element != null) {
+                    Tree.Element e = element; element = null; return e;
+                } else if (chars.length() > 0) {
+                    Tree.Leaf l = new Text(current(), chars.toString());
+                    chars = new StringBuffer(); return l;
+                }
+                return null;
+            }
+        }
+    }
+
+    // Public Interfaces //////////////////////////////////////////////////////
+
+
+    // Support Classes ////////////////////////////////////////////////////////
+
+    /** Represents a block of text in an XML.Document model. */
+    private static final class Text implements Tree.Leaf, Serializable {
+        private Tree.Node p;
+        private String t;
+
+        Text() {}
+        Text(Tree.Node p, String t) { this.t = t; this.p = p; }
+
+        public Tree.Node getParent() { return p; }
+        public void setParent(Tree.Node parent) { p = parent; }
+        public void out(Writer out) throws IOException { out.write(t); }
+        public void out(OutputStream out) { throw new UnsupportedOperationException(); }
+    }
+
+    private static final class Elem
+            implements Tree.Element, Tree.Attributes, Tree.Prefixes, Serializable {
+        private String uri = null;
+        private String localName = null;
+        private String qName = null;
+        private String prefix = null;
+
+        private Tree.Node parent = null;
+        private Basket.List children = null;
+
+        private Tree.Attributes a = this;
+        private Tree.Prefixes p = this;
+
+        Elem() { }
+
+        public Tree.Node getParent() { return parent; }
+        public void setParent(Tree.Node p) { parent = p; }
+        public Basket.List getChildren() { return children == null ? children = new Basket.Array() : children; }
 
-        /** An array of non-fatal errors related to this element. */
-        public Exn[] getErrors() { return errors; }
+        public Tree.Attributes getAttributes() { return a; }
+        public Tree.Prefixes getPrefixes() { return p; }
 
-        protected Element() { }
+        public void setAttributes(Tree.Attributes a) { this.a = a; }
+        public void setPrefixes(Tree.Prefixes p) { this.p = p; }
 
-        /** Add (replace if exists in current element) a Namespace prefix/uri map. */
-        public void addUri(String name, String value) {
-            urimap.put(name, value);
+        public String getQName() { return qName; }
+        public String getLocalName() { return localName; }
+        public String getPrefix() { return prefix; }
+        public String getUri() { return uri; }
+
+        // Attributes
+
+        /** attr[index + 0] // localName
+         *  attr[index + 1] // value
+         *  attr[index + 2] // uri
+         *  attr[index + 3] // prefix */ 
+        private String[] attr = null;
+        private int attrSize = 0;
+
+        void addAttr(String key, String val, String uri, String pfx) {
+            if (attr == null) {
+                attr = new String[DEFAULT_ATTR_COUNT * 4];
+            } else if (attrSize == attr.length / 4) {
+                String[] newattr = new String[attr.length * 2];
+                System.arraycopy(attr, 0, newattr, 0, attr.length);
+                attr = newattr;
+            }
+
+            attr[attrSize * 4 + 0] = key;
+            attr[attrSize * 4 + 1] = val;
+            attr[attrSize * 4 + 2] = uri;
+            attr[attrSize * 4 + 3] = pfx;
+            attrSize++;
+        }
+
+        public int getIndex(String qName) {
+            if (qName == null || qName.length() == 0) return -1;
+            int pos = qName.indexOf(':');
+            if (pos < 1) return getIndex(null, qName);
+            return getIndex(uri(qName.substring(0, pos)), qName.substring(pos+1));
+        }
+
+        public int getIndex(String uri, String key) {
+            if (attr != null) for(int i=0; i < attrSize; i++)
+                if (attr[i*4].equals(key) && attr[i*4+1].equals(uri)) return i;
+            return -1;
         }
 
-        /** Add an attribute. */
-        protected void addAttr(String key, String val, String uri) {
-            if (len == keys.length) {
-                // increase the size of the attributes arrays
-                String[] newkeys = new String[keys.length*2];
-                String[] newvals = new String[vals.length*2];
-                String[] newuris = new String[uris.length*2];
-                System.arraycopy(keys, 0, newkeys, 0, keys.length);
-                System.arraycopy(vals, 0, newvals, 0, vals.length);
-                System.arraycopy(uris, 0, newuris, 0, uris.length);
-                keys = newkeys; vals = newvals; uris = newuris;
+        public String getKey(int pos) { return attr != null && attrSize > pos && pos >= 0 ? attr[pos * 4] : null; }
+        public String getVal(int pos) { return attr != null && attrSize > pos && pos >= 0 ? attr[pos * 4 + 1] : null; }
+        public String getUri(int pos) { return attr != null && attrSize > pos && pos >= 0 ? attr[pos * 4 + 2] : null; }
+        public String getPrefix(int pos) { return attr != null && attrSize > pos && pos >= 0 ? attr[pos * 4 + 3] : null; }
+        public String getQName(int pos) { String pfx = getPrefix(pos); return (pfx != null ? pfx + ":" : "") + getKey(pos); }
+        public int attrSize() { return attrSize; }
+
+        // Prefixes
+
+        /** pfx[index + 0]  // prefix 
+         *  pfx[index + 1]  // uri  */
+        private String[] pfx = null;
+        private int pfxSize = 0;
+
+        void addPrefix(String prefix, String uri) {
+            if (pfx == null) {
+                pfx = new String[DEFAULT_PFX_COUNT * 2];
+            } else if (pfxSize == pfx.length / 4) {
+                String[] newpfx = new String[pfx.length * 2];
+                System.arraycopy(pfx, 0, newpfx, 0, pfx.length);
+                pfx = newpfx;
             }
 
-            keys[len] = key;
-            vals[len] = val;
-            uris[len] = uri;
-            len++;
+            pfx[pfxSize * 2 + 0] = prefix == null ? "" : prefix;
+            pfx[pfxSize * 2 + 1] = uri == null ? "" : uri;
+            pfxSize++;
         }
 
-        /** Add an error. */
-        protected void addError(Exn e) {
-            // it doesn't really matter about continually expanding the array, as this case is quite rare
-            Exn[] newe = new Exn[errors.length+1];
-            System.arraycopy(errors, 0, newe, 0, errors.length);
-            newe[errors.length] = e;
-            errors = newe;
+        public int getPrefixIndexKey(String p) {
+            if (p == null) p = ""; for (int i=0; i < pfxSize; i++) if (p.equals(pfx[i * 2])) return i;
+            return -1;
+        }
+        public int getPrefixIndexVal(String u) {
+            if (u == null) u = ""; for (int i=0; i < pfxSize; i++) if (u.equals(pfx[i * 2 + 1])) return i;
+            return -1;
+        }
+        public String getPrefixKey(int i) {
+            if (i >= pfxSize) throw new IndexOutOfBoundsException(
+                "index "+i+" exceeds boundary "+pfxSize);
+            return pfx[i * 2];
+        }
+        public String getPrefixVal(int i) {
+            if (i >= pfxSize) throw new IndexOutOfBoundsException(
+                "index "+i+" exceeds boundary "+pfxSize);
+            return pfx[i * 2 + 1];
         }
+        public int pfxSize() { return pfxSize; }
 
-        /** Empty out all the data from the Element. */
-        protected void clear() {
-            parent = null;
-            uri = localName = qName = prefix = null;
-            urimap.clear();
+        // Output
+
+        public void out(OutputStream out) throws IOException { throw new UnsupportedOperationException(); }
+        public void out(Writer w) throws IOException {
+            Tree.Attributes a = getAttributes();
+            Tree.Prefixes p = getPrefixes();
+
+            w.write('<'); w.write(getQName());
 
-            if (keys.length != vals.length || vals.length != uris.length) {
-                keys = new String[DEFAULT_ATTR_SIZE];
-                vals = new String[DEFAULT_ATTR_SIZE];
-                uris = new String[DEFAULT_ATTR_SIZE];
+            for (int i=0; i < p.pfxSize(); i++) {
+                w.write(" xmlns");
+                String pfx = p.getPrefixKey(i);
+                if (pfx != null && pfx.length() > 0) {
+                    w.write(':'); w.write(pfx);
+                }
+                w.write("=\"");
+                w.write(p.getPrefixVal(i));
+                w.write('\"');
+            }
+
+            for (int i=0; i < a.attrSize(); i++) {
+                w.write(' ');
+                String pfx = a.getPrefix(i);
+                if (pfx != null && pfx.length() > 0) {
+                    w.write(pfx); w.write(':');
+                }
+                w.write(a.getKey(i));
+                w.write("=\"");
+                w.write(a.getVal(i));
+                w.write('\"');
+            }
+
+            Basket.List c = getChildren();
+            if (c == null || c.size() == 0) {
+                w.write(" />");
             } else {
-                for (int i=0; keys.length > i; i++) { keys[i] = null; vals[i] = null; uris[i] = null; };
+                w.write('>');
+
+                for (int i=0; i < c.size(); i++) ((Tree.Leaf)c.get(i)).out(w);
+
+                w.write("</");
+                w.write(getQName());
+                w.write('>');
             }
-            len = 0;
+        }
+
+        // Helper
+
+        void clear() {
+            if (children != null) children.clear();
+            if (attrSize > 0) { attr = null; attrSize = 0; }
+            if (pfxSize > 0) { pfx = null; pfxSize = 0; }
+
+            uri = localName = qName = prefix = null;
+            parent = null;
+            children = null;
+        }
 
-            errors = new Exn[] {};
+        String uri(String prefix) {
+            String uri = null;
+            for (Tree.Element e = this; e != null; e = (Tree.Element)e.getParent()) {
+                int u = e.getPrefixes().getPrefixIndexKey(prefix);
+                if (u >= 0) { uri = e.getPrefixes().getPrefixVal(u); break; }
+            }
+            return uri;
         }
     }
 
     /** Parse or Structural Error */
-    public static class Exn extends Exception {
+    public static class Exn extends IOException implements Serializable {
         /** Violation of Markup restrictions in XML Specification - Fatal Error */
         public static final int MARKUP = 1;
 
@@ -928,18 +1185,21 @@ public abstract class XML
             this.col   = col;
         }
 
-        public int getType() { return this.type; }
-        public int getLine() { return this.line; }
-        public int getCol()  { return this.col;  }
-        public String getMessage() { return this.error + (line >= 0 && col >= 0 ? " at " + line + ":" + col: ""); }
+        public String getError() { return error; }
+        public int getType() { return type; }
+        public int getLine() { return line; }
+        public int getCol()  { return col;  }
+        public String getMessage() { return error + (line >= 0 && col >= 0 ? " at " + line + ":" + col: ""); }
+        public String toString() { return "XML.Exn: " + getMessage(); }
     }
 
 
-    /////////////////////////////////////////////////////////////////////////////////////////////
-    // Static Support Functions for the XML Specification 
-    /////////////////////////////////////////////////////////////////////////////////////////////
+    // Static Support Functions for the XML Specification /////////////////////
  
-    // attempt to avoid these functions unless you *expect* the input to fall in the given range.
+    // For effeciency's sake, attempt to avoid these functions unless you
+    // *expect* the input to fall in the given range. Thanks to unicode,
+    // even range matching can result in hundreds of comparisons to return
+    // a negative.
 
     /** First Character of Name - XML Specification 1.0 [5] */
     private static final boolean Name(char c) {