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

package org.apache.xmlbeans.impl.store;

import org.apache.xmlbeans.impl.common.Chars;
import org.apache.xmlbeans.impl.common.EncodingMap;
import org.apache.xmlbeans.impl.common.GenericXmlInputStream;
import org.apache.xmlbeans.impl.common.ValidatorListener;
import org.apache.xmlbeans.impl.common.XmlEventBase;
import org.apache.xmlbeans.impl.common.XmlNameImpl;
import org.apache.xmlbeans.impl.common.QNameHelper;
import org.apache.xmlbeans.impl.store.Splay.Container;
import org.apache.xmlbeans.impl.store.Splay.Xmlns;
import org.apache.xmlbeans.impl.values.NamespaceManager;
import org.apache.xmlbeans.XmlCursor;
import org.apache.xmlbeans.XmlOptions;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.ConcurrentModificationException;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.ContentHandler;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
import org.apache.xmlbeans.xml.stream.Attribute;
import org.apache.xmlbeans.xml.stream.AttributeIterator;
import org.apache.xmlbeans.xml.stream.ChangePrefixMapping;
import org.apache.xmlbeans.xml.stream.CharacterData;
import org.apache.xmlbeans.xml.stream.Comment;
import org.apache.xmlbeans.xml.stream.EndDocument;
import org.apache.xmlbeans.xml.stream.EndElement;
import org.apache.xmlbeans.xml.stream.EndPrefixMapping;
import org.apache.xmlbeans.xml.stream.Location;
import org.apache.xmlbeans.xml.stream.ProcessingInstruction;
import org.apache.xmlbeans.xml.stream.StartDocument;
import org.apache.xmlbeans.xml.stream.StartElement;
import org.apache.xmlbeans.xml.stream.StartPrefixMapping;
import org.apache.xmlbeans.xml.stream.XMLEvent;
import org.apache.xmlbeans.xml.stream.XMLName;
import org.apache.xmlbeans.xml.stream.XMLStreamException;

public abstract class Saver implements NamespaceManager
{
    //
    //
    //
    
    private final Object monitor()
    {
        return _root;
    }

    Saver ( Root r, Splay s, int p, XmlOptions options )
    {
        assert Root.dv > 0 || s.getRootSlow() == r;

        // Input s and p must be normalized already
        assert p < s.getEndPos();

        _root = r;
        _top = _splay = s;
        _pos = p;
        _version = r.getVersion();
        _sb = new StringBuffer();
        _attrs = new LinkedHashMap();
        _attrNames = new HashSet();
        _firstPush = true;
        

        // Initialize the state of the namespaces
        _namespaceStack = new ArrayList();
        _uriMap = new HashMap();
        _prefixMap = new HashMap();

        // Stops the synthesis of this namspace and make for better
        // roundtripping 
        addMapping( "xml", Splay._xml1998Uri );

        // Check for implicit namespaces
        
        options = XmlOptions.maskNull( options );

        if (options.hasOption( XmlOptions.SAVE_IMPLICIT_NAMESPACES ))
        {
            Map m = (Map) options.get( XmlOptions.SAVE_IMPLICIT_NAMESPACES );
            
            for ( Iterator i = m.keySet().iterator() ; i.hasNext() ; )
            {
                String prefix = (String) i.next();
                addMapping( prefix, (String) m.get( prefix ) );
            }
        }

        if (options.hasOption( XmlOptions.SAVE_SUGGESTED_PREFIXES ))
            _suggestedPrefixes = (Map) options.get( XmlOptions.SAVE_SUGGESTED_PREFIXES);
        
        // If the default prefix has not been mapped, do so now
        
        if (getNamespaceForPrefix( "" ) == null)
        {
            _initialDefaultUri = new String( "" );
            addMapping( "", _initialDefaultUri );
        }

        _saveNamespacesFirst = options.hasOption( XmlOptions.SAVE_NAMESPACES_FIRST );

        if (_prettyPrint = options.hasOption( XmlOptions.SAVE_PRETTY_PRINT ))
        {
            _prettyIndent = 2;

            if (options.hasOption( XmlOptions.SAVE_PRETTY_PRINT_INDENT ))
            {
                _prettyIndent =
                    ((Integer) options.get( XmlOptions.SAVE_PRETTY_PRINT_INDENT )).intValue();
            }

            if (options.hasOption( XmlOptions.SAVE_PRETTY_PRINT_OFFSET ))
            {
                _prettyOffset =
                    ((Integer) options.get( XmlOptions.SAVE_PRETTY_PRINT_OFFSET )).intValue();
            }
        }

        if (options.hasOption( XmlOptions.SAVE_AGGRESSIVE_NAMESPACES ) &&
                !(this instanceof SynthNamespaceSaver))
        {
            SynthNamespaceSaver saver =
                new SynthNamespaceSaver( r, s, p, options );

            while ( saver.process() )
                ;

            if (!saver._synthNamespaces.isEmpty())
                _preComputedNamespaces = saver._synthNamespaces;
        }
        
        _useDefaultNamespace =
            options.hasOption( XmlOptions.SAVE_USE_DEFAULT_NAMESPACE );

        if (options.hasOption( XmlOptions.SAVE_FILTER_PROCINST ))
        {
            _filterProcinst =
                (String) options.get( XmlOptions.SAVE_FILTER_PROCINST );
        }

        if (options.hasOption( XmlOptions.SAVE_USE_OPEN_FRAGMENT ))
            _fragment = Splay._openuriFragment;
        else
            _fragment = Splay._xmlFragment;

        // Outer overrides inner
        
        _inner =
            options.hasOption( XmlOptions.SAVE_INNER ) &&
                !options.hasOption( XmlOptions.SAVE_OUTER );
        
        if (_inner && !_top.isDoc())
            _synthElem = _fragment;
        
        else if (options.hasOption( XmlOptions.SAVE_SYNTHETIC_DOCUMENT_ELEMENT ))
        {
            _fragment = _synthElem =
                (QName) options.get( XmlOptions.SAVE_SYNTHETIC_DOCUMENT_ELEMENT );

            if (_synthElem == null)
                throw new IllegalArgumentException( "Null synthetic element" );
        }
        
        _preProcess = true;
    }

    protected final void checkVersion ( )
    {
        if (_version != _root.getVersion())
            throw new ConcurrentModificationException( "Document changed during save" );
    }

    protected final Root getRoot ( ) { return _root; }
    protected final Map  getUriMap ( ) { return _uriMap; }
    protected final Map  getPrefixMap ( ) { return _prefixMap; }

    // emitContainer will process leaf contents and the text after a
    // start.  All other text is processed with emitTextAfter.

    protected abstract void emitXmlnsFragment ( Splay s );
    protected abstract void emitAttrFragment ( Splay s );
    protected abstract void emitTextFragment ( Splay s, int p, int cch );
    protected abstract void emitCommentFragment ( Splay s );
    protected abstract void emitProcinstFragment ( Splay s );

    protected abstract void emitDocType(
        String doctypeName, String publicID, String systemID );
    
    protected abstract void emitComment ( Splay s );
    protected abstract void emitTextAfter ( Splay s, int p, int cch );
    protected abstract void emitEnd ( Splay s, QName name );
    protected abstract void emitProcinst ( Splay s );
    protected abstract void emitContainer ( Container c, QName name );

    // Called when a synthetic prefix is created.
    
    protected void syntheticNamespace (
        String prefix, String uri, boolean considerCreatingDefault ) { }

    /*
     * It is vital that the saver does not modify the tree in the process of
     * saving.  So, when there is invalid content or attr values which need
     * new namespace/prefix mappings, I compute the content/values by passing
     * the NamespaceManager implemented by the saver which can cons up mappings
     * just for the purposes of saving without modifying the tree by adding
     * Xmlns splays.
     *
     * Thus, I must not, as a byproduct of saving, validate any content/values
     * into the tree which call back for namespace mappings which need to be
     * created.
     *
     * Also, I need to compute the valid text (if a splay is invalid) before
     * I have closed the attribute list of the enclosing container because
     * I will have to persist out these temporary meppings before they are
     * referenced by the values which need them.
     */

    final String text ( )
    {
        assert _text != null;
        
        return _text.toString();
    }

    final boolean noText ( )
    {
        assert _text != null || _sb.length() == 0;
        assert _text == null || _text == _sb;
        
        return _text == null;
    }

    final void clearText ( )
    {
        _text = null;
        _sb.delete( 0, _sb.length() );
    }

    final static void spaces ( StringBuffer sb, int offset, int count )
    {
        while ( count-- > 0 )
            sb.insert( offset, ' ' );
    }

    final static void trim ( StringBuffer sb )
    {
        int i;

        for ( i = 0 ; i < sb.length() ; i++ )
            if (!Splay.isWhiteSpace( sb.charAt( i ) ))
                break;

        sb.delete( 0, i );

        for ( i = sb.length() ; i > 0 ; i-- )
            if (!Splay.isWhiteSpace( sb.charAt( i - 1 ) ))
                break;

        sb.delete( i, sb.length() );
    }

    // Call process until it returns false to save everything.

    protected final boolean process ( )
    {
        synchronized (monitor())
        {
            checkVersion();
    
            if (_preProcess)
            {
                _preProcess = false;
    
                Splay s = _splay;
                int   p = _pos;
    
                _splay = null;
                _pos = 0;
    
                // Check for position right before end token.  Effectively there
                // is nothing to save here, so save out the empty fragment.
    
                if ((p == 0 && s.isFinish()) ||
                      (s.isLeaf() && p == s.getPosLeafEnd()))
                {
                    assert _splay == null;
                    processTextFragment( null, 0, 0 );
                    return true;
                }
    
                // Here, if p > 0, then we're saving some text
    
                if (p > 0)
                {
                    assert !s.isLeaf() || p != s.getPosLeafEnd();
                    processTextFragment( s, p, s.getPostCch( p ) );
                    assert _splay == null;
                    return true;
                }
    
                if (_inner && (s.isAttr() || s.isComment() || s.isProcinst()))
                {
                    processTextFragment( s, 0, s.getCchValue() );
                    return true;
                }
    
                if (s.isXmlns())
                {
                    processXmlnsFragment( s );
                    return true;
                }
                
                if (s.isAttr())
                {
                    processAttrFragment( s );
                    return true;
                }
                
                if (s.isComment())
                {
                    processCommentFragment( s );
                    return true;
                }
                
                if (s.isProcinst())
                {
                    if (_filterProcinst != null &&
                          s.getLocal().equals( _filterProcinst ))
                    {
                        processTextFragment( null, 0, 0 );
                        return true;
                    }
                    else
                        processProcinstFragment( s );
    
                    return true;
                }
    
                assert s.isContainer();
    
                _splay = s;
                _endSplay = s.isContainer() ? s.getFinishSplay() : s;
            }
    
            // I need to break the processing of a splay into two parts.  The
            // first part is the processing of the splay and the second part is
            // the post processing of the splay.  In particular, the post
            // processing, among other things, pops the mapping stack.  This
            // is done so stuff 'saved' during the first part (the pre-process)
            // can use the mapping stack.  When the next process comes around,
            // the mapping stack will then be updated.
    
            if (_postPop)
            {
                popMappings();
                _postPop = false;
            }
    
            if (_postProcess)
            {
                assert _splay != null;
    
                boolean emitted = false;
    
                if (_splay == _endSplay)
                    _splay = null;
                else
                {
                    Splay s = _splay;
    
                    _splay = _splay.nextNonAttrSplay();
    
                    if (_skipContainerFinish)
                    {
                        assert s.isBegin() && !s.isLeaf() && s.getCchAfter() ==0;
                        assert _splay.isFinish();
    
                        _splay = _splay == _endSplay ? null : _splay.nextSplay();
                    }
    
                    if (_skipContainerFinish || s.isLeaf())
                    {
                        assert !_postPop;
                        _postPop = true;
                    }
    
                    if (!s.isDoc())
                    {
                        assert noText();
    
                        int cchAfter = s.getCchAfter();
    
                        if (_prettyPrint)
                        {
                            _text = _sb;
    
                            if (cchAfter > 0)
                            {
                                Root r = getRoot();
    
                                r._text.fetch(
                                    _text,
                                    s.getCpForPos( r, s.getPosAfter() ), cchAfter );
    
                                trim( _text );
                            }
    
                            Container stop = (Container) _top;
    
                            if (!stop.isDoc())
                                stop = stop.getContainer();
    
                            Container c = s.getContainer( s.getPosAfter() );
    
                            if (_text.length() > 0)
                            {
                                Container p = c;
    
                                for ( ; p != stop ; p = p.getContainer() )
                                    spaces( _text, 0, _prettyIndent );
    
                                if (_prettyIndent >= 0)
                                {
                                    _text.insert( 0, _newLine );
                                    spaces( _text, 1, _prettyOffset );
                                }
                            }
    
                            if (_prettyIndent >= 0)
                            {
                                _text.append( _newLine );
                                spaces( _text, _text.length(), _prettyOffset );
                            }
    
                            Container p = c;
    
                            if (s.nextNonAttrSplay().isEnd())
                                p = p.getContainer();
    
                            for ( ; p != null && p != stop; p = p.getContainer() )
                                spaces( _text, _text.length(), _prettyIndent );
                        }
    
                        if (_text == null ? cchAfter > 0 : _text.length() > 0)
                        {
                            emitTextAfter( s, s.getPosAfter(), cchAfter );
                            emitted = true;
                        }
    
                        clearText();
                    }
                }
    
                _postProcess = false;
    
                // Make sure I only return false if there is *really* nothing more
                // to process
    
                if (emitted)
                    return true;
    
                if (_postPop)
                {
                    popMappings();
                    _postPop = false;
                }
            }
    
            if (_splay == null)
                return false;
    
            if (_version != getRoot().getVersion())
                throw new IllegalStateException( "Document changed" );
    
            _skipContainerFinish = false;
    
            switch ( _splay.getKind() )
            {
            case Splay.DOC :
            case Splay.BEGIN :
            {
                processContainer( (Container) _splay );
                break;
            }
            case Splay.ROOT :
            case Splay.END  :
            {
                processEnd( _splay );
                break;
            }
            case Splay.COMMENT :
            {
                emitComment( _splay );
                break;
            }
            case Splay.PROCINST :
            {
                if (_filterProcinst == null ||
                        !_splay.getLocal().equals( _filterProcinst ))
                {
                    emitProcinst( _splay );
                }
                break;
            }
            case Splay.ATTR :
            default :
            {
                assert false: "Unexpected splay kind " + _splay.getKind();
                return false;
            }
            }
    
            _postProcess = true;
    
            return true;
        }
    }

    private final void processContainer ( Container c )
    {
        assert c.isDoc() || c.isBegin();

        QName name =
            _synthElem != null && c == _top
                ? _synthElem
                : c.isBegin() ? c.getName() : null;

        String nameUri = name == null ? null : name.getNamespaceURI();

        // See if there is a doctype to save out

        if (c.isDoc() )
        {
            String systemId = _root._props.getDoctypeSystemId();
            String docTypeName = _root._props.getDoctypeName();
            
            if (systemId != null || docTypeName != null)
            {
                if (docTypeName == null && name != null)
                    docTypeName = name.getLocalPart();

                if (docTypeName == null)
                {
                    XmlCursor xc = _root.createCursor();
                    
                    if (xc.toFirstChild())
                        docTypeName = xc.getName().getLocalPart();
                    
                    xc.dispose();
                }

                if (docTypeName == null && _fragment != null)
                    docTypeName = _fragment.getLocalPart();

                String publicId = _root._props.getDoctypePublicId();
                
                emitDocType( docTypeName, publicId, systemId );
            }
        }

        // Add a new entry to the frontier.  If this element has a name
        // which has no namespace, then we must make sure that pushing
        // the mappings causes the default namespace to be empty

        boolean ensureDefaultEmpty = name != null && nameUri.length() == 0;

        pushMappings( c, ensureDefaultEmpty );

        //
        // There are four things which use mappings:
        //
        //   1) The element name
        //   2) The element value (qname based)
        //   3) Attribute names
        //   4) The attribute values (qname based)
        //

        // 1) The element name (not for starts)

        if (name != null)
            ensureMapping( nameUri, null, !ensureDefaultEmpty, false );

        assert noText();

        if (c.isInvalid())
        {
            // 2) The element value
            _text = _sb.append( c.peekType().build_text( this ) );
        }

        _attrs.clear();
        _attrNames.clear();

        for ( Splay s = c.nextSplay() ; s.isAttr() ; s = s.nextSplay() )
        {
            if (s.isNormalAttr() &&
                    (_wantDupAttrs || !_attrNames.contains( s.getName() )))
            {
                _attrNames.add( s.getName() );
                
                // 3) Attribute name
                ensureMapping( s.getUri(), null, false, true );

                String invalidValue = null;
                
                if (s.isInvalid())
                    invalidValue = s.peekType().build_text( this ); // #4

                _attrs.put( s, invalidValue );
            }
        }

        // emitContainer handles text only for leaves and starts

        if (_prettyPrint && (c.isDoc() || c.isLeaf()))
        {
            if (_text == null)
            {
                Root r = getRoot();

                r._text.fetch(
                    _text = _sb,
                    r.getCp( c ),
                    c.isLeaf() ? c.getCchValue() : c.getCch() );
            }

            trim( _text );

            if (c.isDoc())
            {
                spaces( _text, 0, _prettyOffset );

                if (_text.length() > _prettyOffset)
                {
                    if (_prettyIndent >= 0)
                    {
                        _text.insert( 0, _newLine );
                        spaces( _text, 1, _prettyOffset );
                        _text.append( _newLine );
                        spaces( _text, _text.length(), _prettyOffset );
                    }
                }
            }
        }

        // derived savers may want to test the stuff being stored to
        // see if is well formed or not in order to save out a
        // fragment.  Do this here when emitting the first container if
        // that container is the root (if not the root, then we are
        // well formed)

        if (_wantFragTest && name == null)
        {
            if ((_text != null && !Splay.isWhiteSpace( _text )) ||
                  !c.isAfterWhiteSpace( getRoot() ))
            {
                _needsFrag = true;
            }
            else
            {
                assert !c.isLeaf();
                
                Splay s = c.nextSplay();

                // Check for leaf anyways -- sometimes it happens!
                if (c.isLeaf() || s.isAttr() || hasMappings())
                    _needsFrag = true;
                else
                {
                    boolean sawBegin = false;
                    Splay cEnd = c.getFinishSplay();

                    for ( ; s != cEnd ; s = s.nextSplay() )
                    {
                        if (s.isBegin())
                        {
                            if (sawBegin)
                            {
                                _needsFrag = true;
                                break;
                            }

                            sawBegin = true;
                            s = s.getFinishSplay();
                        }

                        if (!s.isAfterWhiteSpace( getRoot() ))
                        {
                            _needsFrag = true;
                            break;
                        }
                    }

                    if (!sawBegin)
                        _needsFrag = true;
                }
            }
        }

        // If I am doing aggressive namespaces and we're emitting a
        // container which can contain content, add the namespaces
        // we've computed.  Basically, I'm making sure the pre-computed
        // namespaces are mapped on the first container which has a name.

        if (_preComputedNamespaces != null && (name != null || _needsFrag))
        {
            for ( Iterator i = _preComputedNamespaces.keySet().iterator() ; i.hasNext() ; )
            {
                String uri = (String) i.next();
                
                ensureMapping(
                    uri, null,
                    _preComputedNamespaces.get( uri ) != null && !ensureDefaultEmpty, false );
            }

            // Set to null so we do this once at the top
            _preComputedNamespaces = null;
        }

        if (_wantFragTest)
        {
            // See if I need to gen a fragment for the document

            assert name != null || (c.isDoc() && _synthElem == null);

            if (name == null)
            {
                if (_needsFrag)
                {
                    name = _fragment;
                    ensureFragmentNamespace();
                    _docElem = name;
                }
            }
            else if (c.isDoc())
                _docElem = name;
        }

        emitContainer( c, name );

        clearText();
    }

    private void processEnd ( Splay s )
    {
        Container c = s.getContainer();
        
        QName name =
            _synthElem != null && c == _top
                ? _synthElem
                : c.isBegin() ? c.getName() : null;
        
        if (_wantFragTest && name == null)
        {
            boolean isRoot = s.isRoot();

            if (!isRoot || _docElem != null)
            {
                name = 
                    isRoot
                        ? _docElem
                        : (s.isEnd() ? s.getContainer() : s ).getName();
            }
        }

        emitEnd( _splay, name );

        assert !_postPop;

        _postPop = true;
    }

    private final void pushFragmentMappings ( Splay s )
    {
        // mask the initial frame to hide default mappings
        
        pushMappings(
            s == null ? null : s.getContainer(),
            _fragment.getNamespaceURI().length() == 0 );
        
        ensureFragmentNamespace();
    }

    private final void processXmlnsFragment ( Splay s )
    {
        // Saving a default xmlns mapping is dangerous, just spit out empty
        // text
        
        if (s.getLocal().length() == 0)
        {
            pushFragmentMappings( null );
            _text = _sb;
            emitTextFragment( s, 0, 0 );
        }
        else
        {
            pushFragmentMappings( null );
            ensureMapping( s.getUri(), s.getLocal(), false, true );
            emitXmlnsFragment( s );
        }
    }

    private final void processCommentFragment ( Splay s )
    {
        pushFragmentMappings( null );
        emitCommentFragment( s );
    }

    private final void processProcinstFragment ( Splay s )
    {
        pushFragmentMappings( null );
        emitProcinstFragment( s );
    }
    
    /**
     * This is called only when a single attr is being saved.  It is not called
     * as a consequence of saving out a container.
     */

    private final void processAttrFragment ( Splay s )
    {
        assert s.isNormalAttr();

        pushFragmentMappings( s );

        ensureMapping( s.getUri(), null, false, true );

        assert noText();

        if (s.isInvalid())
        {
            // TODO - pass StringBuffer to buildText to save object creation
            _text = _sb.append( s.peekType().build_text( this ) );
        }

        emitAttrFragment( s );

        clearText();
    }

    /**
     * This is called only when a single chunk of text is to be saved.
     * It is not called as a consequence of saving out a container.
     */

    private final void processTextFragment ( Splay s, int p, int cch )
    {
        // Only need to save ancestor namespace mappings if there could be a
        // qname here.  If the text is of length 0, then there is no qname.

        Splay c = null;
        
        if ((_text != null && _text.length() > 0) || (s != null && cch > 0))
            c = s.getContainer( p );
        
        pushFragmentMappings( c );

        emitTextFragment( s, p, cch );
    }

    //
    // Layout of namespace stack:
    //
    //    URI Undo
    //    URI Rename
    //    Prefix Undo
    //    Mapping
    //

    boolean hasMappings ( )
    {
        int i = _namespaceStack.size();

        return i > 0 && _namespaceStack.get( i - 1 ) != null;
    }

    void iterateMappings ( )
    {
        _currentMapping = _namespaceStack.size();

        while ( _currentMapping > 0 &&
                  _namespaceStack.get( _currentMapping - 1 ) != null )
        {
            _currentMapping -= 8;
        }
    }

    boolean hasMapping ( )
    {
        return _currentMapping < _namespaceStack.size();
    }

    void nextMapping ( )
    {
        _currentMapping += 8;
    }

    String mappingPrefix ( )
    {
        assert hasMapping();
        return (String) _namespaceStack.get( _currentMapping + 6 );
    }

    String mappingUri ( )
    {
        assert hasMapping();
        return (String) _namespaceStack.get( _currentMapping + 7 );
    }

    String mappingPrevPrefixUri ( )
    {
        assert hasMapping();
        return (String) _namespaceStack.get( _currentMapping + 5 );
    }

    //
    //
    //

    private final void pushMappings ( Container c, boolean ensureDefaultEmpty )
    {
        _namespaceStack.add( null );

        for ( ; c != null ; c = c.getContainer() )
        {
            namespaces:
            for ( Splay s = c.nextSplay() ; s.isAttr() ; s = s.nextSplay() )
            {
                if (s.isXmlns())
                {
                    Xmlns x = (Xmlns) s;
                    String prefix = x.getLocal();
                    String uri = x.getUri();

                    if (ensureDefaultEmpty &&
                            prefix.length() == 0 && uri.length() > 0)
                    {
                        continue;
                    }

                    // Make sure the prefix is not already mapped in
                    // this frame

                    for ( iterateMappings() ; hasMapping() ; nextMapping() )
                        if (mappingPrefix().equals( prefix ))
                            continue namespaces;

                    addMapping( prefix, uri );
                }
            }

            // Push all ancestors the first time
            
            if (!_firstPush)
                break;
        }

        if (ensureDefaultEmpty)
        {
            String defaultUri = (String) _prefixMap.get( "" );

            // I map the default to "" at the very beginning
            assert defaultUri != null;

            if (defaultUri.length() > 0)
                addMapping( "", "" );
        }

        _firstPush = false;
    }

    private final void dumpMappings ( )
    {
        for ( int i = _namespaceStack.size() ; i > 0 ; )
        {
            if (_namespaceStack.get( i - 1 ) == null)
            {
                System.out.println( "----------------" );
                i--;
                continue;
            }

            System.out.print( "Mapping: " );
            System.out.print( _namespaceStack.get( i - 2 ) );
            System.out.print( " -> " );
            System.out.print( _namespaceStack.get( i - 1 ) );
            System.out.println();

            System.out.print( "Prefix Undo: " );
            System.out.print( _namespaceStack.get( i - 4 ) );
            System.out.print( " -> " );
            System.out.print( _namespaceStack.get( i - 3 ) );
            System.out.println();

            System.out.print( "Uri Rename: " );
            System.out.print( _namespaceStack.get( i - 5 ) );
            System.out.print( " -> " );
            System.out.print( _namespaceStack.get( i - 6 ) );
            System.out.println();

            System.out.print( "UriUndo: " );
            System.out.print( _namespaceStack.get( i - 7 ) );
            System.out.print( " -> " );
            System.out.print( _namespaceStack.get( i - 8 ) );
            System.out.println();

            System.out.println();

            i -= 8;
        }
    }

    private final void addMapping ( String prefix, String uri )
    {
        assert uri != null;
        assert prefix != null;

        // If the prefix being mapped here is already mapped to a uri,
        // that uri will either go out of scope or be mapped to another
        // prefix.

        String renameUri = (String) _prefixMap.get( prefix );
        String renamePrefix = null;

        if (renameUri != null)
        {
            // See if this prefix is already mapped to this uri.  If
            // so, then add to the stack, but there is nothing to rename
        
            if (renameUri.equals( uri ))
                renameUri = null;
            else
            {
                int i = _namespaceStack.size();

                while ( i > 0 )
                {
                    if (_namespaceStack.get( i - 1 ) == null)
                    {
                        i--;
                        continue;
                    }

                    if (_namespaceStack.get( i - 7 ).equals( renameUri ))
                    {
                        renamePrefix = (String) _namespaceStack.get( i - 8 );

                        if (renamePrefix == null ||
                              !renamePrefix.equals( prefix ))
                        {
                            break;
                        }
                    }

                    i -= 8;
                }

                assert i > 0;
            }
        }

        _namespaceStack.add( _uriMap.get( uri ) );
        _namespaceStack.add( uri );

        if (renameUri != null)
        {
            _namespaceStack.add( _uriMap.get( renameUri ) );
            _namespaceStack.add( renameUri );
        }
        else
        {
            _namespaceStack.add( null );
            _namespaceStack.add( null );
        }

        _namespaceStack.add( prefix );
        _namespaceStack.add( _prefixMap.get( prefix ) );

        _namespaceStack.add( prefix );
        _namespaceStack.add( uri );

        _uriMap.put( uri, prefix );
        _prefixMap.put( prefix, uri );

        if (renameUri != null)
            _uriMap.put( renameUri, renamePrefix );
    }

    private final void popMappings ( )
    {
        for ( ; ; )
        {
            int i = _namespaceStack.size();

            if (i == 0)
                break;

            if (_namespaceStack.get( i - 1 ) == null)
            {
                _namespaceStack.remove( i - 1 );
                break;
            }

            Object oldUri = _namespaceStack.get( i - 7 ); 
            Object oldPrefix = _namespaceStack.get( i - 8 ); 

            if (oldPrefix == null) 
                _uriMap.remove( oldUri ); 
            else 
                _uriMap.put( oldUri, oldPrefix ); 

            oldPrefix = _namespaceStack.get( i - 4 ); 
            oldUri = _namespaceStack.get( i - 3 ); 

            if (oldUri == null) 
                _prefixMap.remove( oldPrefix ); 
            else 
                _prefixMap.put( oldPrefix, oldUri ); 

            String uri = (String) _namespaceStack.get( i - 5 );

            if (uri != null)
                _uriMap.put( uri, _namespaceStack.get( i - 6 ) );

            // Hahahahahaha -- :-(
            _namespaceStack.remove( i - 1 );
            _namespaceStack.remove( i - 2 );
            _namespaceStack.remove( i - 3 );
            _namespaceStack.remove( i - 4 );
            _namespaceStack.remove( i - 5 );
            _namespaceStack.remove( i - 6 );
            _namespaceStack.remove( i - 7 );
            _namespaceStack.remove( i - 8 );
        }
    }

    protected final String getUriMapping ( String uri )
    {
        assert _uriMap.get( uri ) != null;
        return (String) _uriMap.get( uri );
    }

    protected final boolean tryPrefix ( String prefix )
    {
        if (prefix == null || Splay.beginsWithXml( prefix ))
            return false;

        String existingUri = (String) _prefixMap.get( prefix );

        // If the prefix is currently mapped, then try another prefix.  A
        // special case is that of trying to map the default prefix ("").
        // Here, there always exists a default mapping.  If this is the
        // mapping we found, then remap it anyways. I use != to compare
        // strings because I want to test for the specific initial default
        // uri I added when I initialized the saver.

        if (existingUri != null &&
              (prefix.length() > 0 || existingUri != _initialDefaultUri))
        {
            return false;
        }

        return true;
    }

    protected final String ensureMapping (
        String uri, String candidatePrefix,
        boolean considerCreatingDefault, boolean mustHavePrefix )
    {
        assert uri != null;
        assert candidatePrefix == null || candidatePrefix.length() > 0;

        // Can be called for no-namespaced things

        if (uri.length() == 0)
            return null;

        String prefix = (String) _uriMap.get( uri );

        if (prefix != null && (prefix.length() > 0 || !mustHavePrefix))
            return prefix;

        //
        // I try prefixes from a number of places, in order:
        //
        //  1) What was passed in
        //  2) The optional suggestions (for uri's)
        //  3) The default mapping is allowed
        //  4) ns#++
        //
        
        if (candidatePrefix == null || !tryPrefix( candidatePrefix ))
        {
            if (_suggestedPrefixes != null &&
                    _suggestedPrefixes.containsKey( uri ) &&
                        tryPrefix( (String) _suggestedPrefixes.get( uri ) ))
            {
                candidatePrefix = (String) _suggestedPrefixes.get( uri );
            }
            else if (considerCreatingDefault && _useDefaultNamespace && tryPrefix( "" ))
                candidatePrefix = "";
            else
            {
                String basePrefix = QNameHelper.suggestPrefix( uri );
                candidatePrefix = basePrefix;
                
                for ( int i = 1 ; ; i++ )
                {
                    if (tryPrefix( candidatePrefix ))
                        break;
                    
                    candidatePrefix = basePrefix + i;
                }
            }
        }

        assert candidatePrefix != null;

        syntheticNamespace( candidatePrefix, uri, considerCreatingDefault );

        addMapping( candidatePrefix, uri );

        return candidatePrefix;
    }

    public final String find_prefix_for_nsuri ( String uri, String prefix )
    {
        assert uri != null;
        assert prefix == null || prefix.length() > 0;

        boolean emptyUri = uri.length() == 0;
        
        return ensureMapping( uri, prefix, emptyUri, !emptyUri );
    }

    public final String getNamespaceForPrefix ( String prefix )
    {
        if (prefix != null && prefix.equals( "xml" ))
            return Splay._xml1998Uri;

        return (String) _prefixMap.get( prefix );
    }

    protected final String ensureFragmentNamespace ( )
    {
        if (_fragment.getNamespaceURI().length() == 0)
            return "";
        
        return ensureMapping( _fragment.getNamespaceURI(), "frag", false, false );
    }

    /**
     * A Saver which records synthetic namespaces
     */
    
    static final class SynthNamespaceSaver extends Saver
    {
        LinkedHashMap _synthNamespaces = new LinkedHashMap();
        
        SynthNamespaceSaver ( Root r, Splay s, int p, XmlOptions options )
        {
            super( r, s, p, options );
        }
        
        protected void syntheticNamespace (
            String prefix, String uri, boolean considerCreatingDefault )
        {
            _synthNamespaces.put( uri, considerCreatingDefault ? "useDefault" : null );
        }
        
        protected void emitXmlnsFragment ( Splay s ) { }
        protected void emitAttrFragment ( Splay s ) { }
        protected void emitTextFragment ( Splay s, int p, int cch ) { }
        protected void emitCommentFragment ( Splay s ) { }
        protected void emitProcinstFragment ( Splay s ) { }
        protected void emitComment ( Splay s ) { }
        protected void emitTextAfter ( Splay s, int p, int cch ) { }
        protected void emitEnd ( Splay s, QName name ) { }
        protected void emitProcinst ( Splay s ) { }
        protected void emitContainer ( Container c, QName name ) { }
        
        protected void emitDocType(
            String doctypeName, String publicID, String systemID ) { }
    }

    /**
     * A Saver which generates characters.
     */

    static final class TextSaver extends Saver
    {
        TextSaver (
            Root r, Splay s, int p, XmlOptions options, String encoding )
        {
            super( r, s, p, options );

            _wantFragTest = true;

            if (encoding != null)
            {
                String version = r._props.getVersion();

                if (version == null)
                    version = "1.0";
                
                emit( "<?xml version=\"" );
                emit( version );
                emit( "\" encoding=\"" + encoding + "\"?>" + _newLine );
            }
        }

        protected void emitContainer ( Container c, QName name )
        {
            if (c.isBegin())
            {
                emitContainerHelper( c, name, null, null, false );

                if (c.isLeaf())
                {
                    int cch = _text == null ? c.getCchValue() : _text.length();

                    if (cch > 0)
                    {
                        emit( '>' );

                        if (_text == null)
                            emit( getRoot().getCp( c ), cch );
                        else
                            emit( _text );

                        entitizeContent();

                        emit( "</" );
                        emitName( name );
                    }
                    else
                        emit( '/' );
                }
                else
                {
                    assert !c.isLeaf();

                    if (c.getCchAfter() == 0 && c.nextNonAttrSplay().isEnd())
                    {
                        emit( '/' );
                        _skipContainerFinish = true;
                    }
                }

                emit( '>' );
            }
            else
            {
                assert c.isDoc();

                if (name != null)
                    emitContainerHelper( c, name, null, null, true );

                if (_text == null)
                    emit( getRoot().getCp( c ), c.getCch() );
                else
                    emit( _text );

                entitizeContent();
            }
        }

        private void emitAttrHelper ( Splay s, String invalidValue )
        {
            assert s.isNormalAttr();
            
            emit( ' ' );
            emitName( s.getName() );
            emit( "=\"" );

            if (invalidValue != null)
                emit( invalidValue );
            else
                emit( getRoot().getCp( s ), s.getCch() );

            entitizeAttrValue();

            emit( '"' );
        }

        private void emitNamespacesHelper ( )
        {
            for ( iterateMappings() ; hasMapping() ; nextMapping() )
            {
                emit( ' ' );
                emitXmlns( mappingPrefix(), mappingUri() );
            }
        }
                                     
        private void emitContainerHelper (
            Container c, QName name,
            Splay extraAttr, StringBuffer extraAttrText,
            boolean close )
        {
            assert name != null;

            emit( '<' );
            emitName( name );

            if (_saveNamespacesFirst)
                emitNamespacesHelper();

            if (c != null)
            {
                for ( Iterator i = _attrs.keySet().iterator() ; i.hasNext() ; )
                {
                    Splay s = (Splay) i.next();
                    emitAttrHelper( s, (String) _attrs.get( s ) );
                }
            }

            if (extraAttr != null)
            {
                emitAttrHelper(
                    extraAttr,
                    extraAttrText == null ? null : extraAttrText.toString() );
            }

            if (!_saveNamespacesFirst)
                emitNamespacesHelper();

            if (close)
                emit( '>' );
        }

        protected void emitText ( Splay s, int p, int cch )
        {
            emit( s.getCpForPos( getRoot(), p ), cch );
            entitizeContent();
        }

        protected void emitTextAfter ( Splay s, int p, int cch )
        {
            if (_text == null)
                emitText( s, p, cch );
            else
            {
                emit( _text );
                entitizeContent();
            }
        }

        protected void emitTextFragment ( Splay s, int p, int cch )
        {
            emitContainerHelper( null, _fragment, null, null, false );

            if (_text != null)
            {
                if (_text.length() > 0)
                {
                    emit( ">" );
                    emit( _text );
                    emitEndHelper( _fragment );
                    return;
                }
            }
            else if (s != null)
            {
                if (cch > 0)
                {
                    emit( ">" );
                    emitText( s, p, cch );
                    emitEndHelper( _fragment );
                    return;
                }
            }
            
            emit( "/>" );
        }

        protected void emitAttrFragment ( Splay s )
        {
            emitContainerHelper( null, _fragment, s, _text, false );
            emit( "/>" );
        }

        protected void emitXmlnsFragment ( Splay s )
        {
            emitContainerHelper( null, _fragment, null, null, false );
            emit( "/>" );
        }

        protected void emitCommentFragment ( Splay s )
        {
            emitContainerHelper( null, _fragment, null, null, true );
            emitComment( s );
            emitEndHelper( _fragment );
        }

        protected void emitProcinstFragment ( Splay s )
        {
            emitContainerHelper( null, _fragment, null, null, true );
            emitProcinst( s );
            emitEndHelper( _fragment );
        }

        protected void emitXmlns ( String prefix, String uri )
        {
            assert prefix != null;
            assert uri != null;

            emit( "xmlns" );

            if (prefix.length() > 0)
            {
                emit( ":" );
                emit( prefix );
            }

            emit( "=\"" );

            // TODO - must encode uri properly

            emit( uri );
            entitizeAttrValue();

            emit( '"' );
        }

        protected void emitEndHelper ( QName name )
        {
            emit( "</" );
            emitName( name );
            emit( '>' );
        }

        protected void emitEnd ( Splay s, QName name )
        {
            if (name != null)
                emitEndHelper( name );
        }

        private void emitLiteral ( String literal )
        {
            if (literal.indexOf( "\"" ) < 0)
            {
                emit( "\"" );
                emit( literal );
                emit( "\"" );
            }
            else
            {
                emit( "'" );
                emit( literal );
                emit( "'" );
            }
        }

        protected void emitDocType(
            String doctypeName, String publicID, String systemID )
        {
            assert doctypeName != null;
            
            emit( "<!DOCTYPE " );
            emit( doctypeName );

            if (publicID == null && systemID != null)
            {
                emit( " SYSTEM " );
                emitLiteral( systemID );
            }
            else if (publicID != null)
            {
                emit( " PUBLIC " );
                emitLiteral( publicID );
                emit( " " );
                emitLiteral( systemID );
            }

            emit( ">" + _newLine );
        }
        
        protected void emitComment ( Splay s )
        {
            assert s.isComment();
            emit( "<!--" );
            emit( getRoot().getCp( s ), s.getCchValue() );
            entitizeComment();
            emit( "-->" );
        }

        protected void emitProcinst ( Splay s )
        {
            assert s.isProcinst();
            emit( "<?" );
            // TODO - encoding issues here?
            emit( s.getLocal() );

            if (s.getCchValue() > 0)
            {
                emit( " " );
                emit( getRoot().getCp( s ), s.getCchValue() );
                entitizeProcinst();
            }

            emit( "?>" );
        }

        /**
         * Ensure that there are at least cch chars available in the buffer.
         * Return the actual number of characters available.  If less than cch,
         * then that's all which will ever become available.
         */

        private int ensure ( int cch )
        {
            // Even if we're asked to ensure nothing, still try to ensure
            // atleast one character so we can determine if we're at the
            // end of the stream.

            if (cch <= 0)
                cch = 1;

            int available = getAvailable();

            for ( ; available < cch ; available = getAvailable() )
                if (!process())
                    break;

            assert available == getAvailable();

            if (available == 0)
                return 0;

            return available;
        }

        private void emitName ( QName name )
        {
            assert name != null;

            String uri = name.getNamespaceURI();

            assert uri != null;

            if (uri.length() != 0)
            {
                String prefix = getUriMapping( uri );

                if (prefix.length() > 0)
                {
                    emit( prefix );
                    emit( ":" );
                }
            }

            assert name.getLocalPart().length() > 0;

            emit( name.getLocalPart() );
        }

        private boolean preEmit ( int cch )
        {
            assert cch >= 0;
            
            _lastEmitCch = cch;

            if (cch == 0)
                return true;

            if (_free < cch)
                resize( cch, -1 );

            assert cch <= _free;

            int used = getAvailable();

            // if we are about to emit and there is noting in the buffer, reset
            // the buffer to be at the beginning so as to not grow it anymore
            // than needed.
            
            if (used == 0)
            {
                assert _in == _out;
                assert _free == _buf.length;
                _in = _out = 0;
            }

            _lastEmitIn = _in;

            _free -= cch;
            
            assert _free >= 0;

            return false;
        }

        private void emit ( String s )
        {
            int cch = s == null ? 0 : s.length();

            if (preEmit( cch ))
                return;

            int chunk;

            if (_in <= _out || cch < (chunk = _buf.length - _in))
            {
                s.getChars( 0, cch, _buf, _in );
                _in += cch;
            }
            else
            {
                s.getChars( 0, chunk, _buf, _in );
                s.getChars( chunk, cch, _buf, 0 );
                _in = (_in + cch) % _buf.length;
            }
        }

        private void emit ( StringBuffer sb )
        {
            int cch = sb == null ? 0 : sb.length();

            if (preEmit( cch ))
                return;

            int chunk;

            if (_in <= _out || cch < (chunk = _buf.length - _in))
            {
                sb.getChars( 0, cch, _buf, _in );
                _in += cch;
            }
            else
            {
                sb.getChars( 0, chunk, _buf, _in );
                sb.getChars( chunk, cch, _buf, 0 );
                _in = (_in + cch) % _buf.length;
            }
        }

        private void emit ( int cp, int cch )
        {
            emit(
                getRoot()._text._buf,
                getRoot()._text.unObscure( cp, cch ),
                cch );
        }

        private void emit ( char ch )
        {
            preEmit( 1 );

            _buf[ _in ] = ch;

            _in = (_in + 1) % _buf.length;
        }

        private void emit ( char[] buf, int off, int cch )
        {
            assert cch >= 0;

            if (preEmit( cch ))
                return;

            int chunk;

            if (_in <= _out || cch < (chunk = _buf.length - _in))
            {
                System.arraycopy( buf, off, _buf, _in, cch );
                _in += cch;
            }
            else
            {
                System.arraycopy( buf, off, _buf, _in, chunk );
                System.arraycopy( buf, off + chunk, _buf, 0, cch - chunk );
                _in = (_in + cch) % _buf.length;
            }
        }

        private void entitizeContent ( )
        {
            if (_lastEmitCch == 0)
                return;

            int i = _lastEmitIn;
            final int n = _buf.length;

            boolean hasOutOfRange = false;
            
            int count = 0;
            for ( int cch = _lastEmitCch ; cch > 0 ; cch-- )
            {
                char ch = _buf[ i ];

                if (ch == '<' || ch == '&')
                    count++;
                else if (isBadChar( ch ))
                    hasOutOfRange = true;

                if (++i == n)
                    i = 0;
            }

            if (count == 0 && !hasOutOfRange)
                return;

            i = _lastEmitIn;

            //
            // Heuristic for knowing when to save out stuff as a CDATA.
            //

            if (_lastEmitCch > 32 && count > 5 &&
                  count * 100 / _lastEmitCch > 1)
            {
                boolean lastWasBracket = _buf[ i ] == ']';

                i = replace( i, "<![CDATA[" + _buf[ i ] );

                boolean secondToLastWasBracket = lastWasBracket;

                lastWasBracket = _buf[ i ] == ']';

                if (++i == _buf.length)
                    i = 0;

                for ( int cch = _lastEmitCch ; cch > 0 ; cch-- )
                {
                    char ch = _buf[ i ];

                    if (ch == '>' && secondToLastWasBracket && lastWasBracket)
                        i = replace( i, "&gt;" );
                    else if (isBadChar( ch ))
                        i = replace( i, "?" );
                    else
                        i++;

                    secondToLastWasBracket = lastWasBracket;
                    lastWasBracket = ch == ']';

                    if (i == _buf.length)
                        i = 0;
                }

                emit( "]]>" );
            }
            else
            {
                for ( int cch = _lastEmitCch ; cch > 0 ; cch-- )
                {
                    char ch = _buf[ i ];

                    if (ch == '<')
                        i = replace( i, "&lt;" );
                    else if (ch == '&')
                        i = replace( i, "&amp;" );
                    else if (isBadChar( ch ))
                        i = replace( i, "?" );
                    else
                        i++;

                    if (i == _buf.length)
                        i = 0;
                }
            }
        }

        private void entitizeAttrValue ( )
        {
            if (_lastEmitCch == 0)
                return;

            int i = _lastEmitIn;

            for ( int cch = _lastEmitCch ; cch > 0 ; cch-- )
            {
                char ch = _buf[ i ];

                if (ch == '<')
                    i = replace( i, "&lt;" );
                else if (ch == '&')
                    i = replace( i, "&amp;" );
                else if (ch == '"')
                    i = replace( i, "&quot;" );
                else
                    i++;

                if (i == _buf.length)
                    i = 0;
            }
        }

        private void entitizeComment ( )
        {
            if (_lastEmitCch == 0)
                return;

            int i = _lastEmitIn;

            boolean lastWasDash = false;

            for ( int cch = _lastEmitCch ; cch > 0 ; cch-- )
            {
                char ch = _buf[ i ];

                if (isBadChar( ch ))
                    i = replace( i, "?" );
                else if (ch == '-')
                {
                    if (lastWasDash)
                    {
                        // Replace "--" with "- " to make well formed
                        i = replace( i, " " );
                        lastWasDash = false;
                    }
                    else
                    {
                        lastWasDash = true;
                        i++;
                    }
                }
                else
                {
                    lastWasDash = false;
                    i++;
                }

                if (i == _buf.length)
                    i = 0;
            }

            // Because I have only replaced chars with single chars,
            // _lastEmitIn will still be ok

            if (_buf[ _lastEmitIn + _lastEmitCch - 1 ] == '-')
                i = replace( _lastEmitIn + _lastEmitCch - 1, " " );
        }

        private void entitizeProcinst ( )
        {
            if (_lastEmitCch == 0)
                return;

            int i = _lastEmitIn;

            boolean lastWasQuestion = false;

            for ( int cch = _lastEmitCch ; cch > 0 ; cch-- )
            {
                char ch = _buf[ i ];

                if (isBadChar( ch ))
                    i = replace( i, "?" );

                if (ch == '>')
                {
    // TODO - Had to convert to a space here ... imples not well formed XML
                    if (lastWasQuestion)
                        i = replace( i, " " );
                    else
                        i++;

                    lastWasQuestion = false;
                }
                else
                {
                    lastWasQuestion = ch == '?';
                    i++;
                }

                if (i == _buf.length)
                    i = 0;
            }
        }


        /**
         * Test if a character is valid in xml character content. See
         * http://www.w3.org/TR/REC-xml#NT-Char
         */
        
        private boolean isBadChar ( char ch )
        {
            return ! (
                (ch >= 0x20 && ch <= 0xD7FF ) ||
                (ch >= 0xE000 && ch <= 0xFFFD) ||
                (ch >= 0x10000 && ch <= 0x10FFFF) ||
                (ch == 0x9) || (ch == 0xA) || (ch == 0xD)
                );
        }

        private int replace ( int i, String replacement )
        {
            assert replacement.length() > 0;

            int dCch = replacement.length() - 1;

            if (dCch == 0)
            {
                _buf[ i ] = replacement.charAt( 0 );
                return i + 1;
            }

            assert _free >= 0;

            if (dCch > _free)
                i = resize( dCch, i );
            
            assert _free >= 0;

            assert _free >= dCch;
            assert getAvailable() > 0;

            if (_out > _in && i >= _out)
            {
                System.arraycopy( _buf, _out, _buf, _out - dCch, i - _out );
                _out -= dCch;
                i -= dCch;
            }
            else
            {
                assert i < _in;
                System.arraycopy( _buf, i, _buf, i + dCch, _in - i );
                _in += dCch;
            }

            replacement.getChars( 0, dCch + 1, _buf, i );

            _free -= dCch;
            
            assert _free >= 0;

            return i + dCch + 1;
        }

        int getAvailable ( )
        {
            return _buf == null ? 0 : _buf.length - _free;
        }

        /**
         * Make sure there is enough room for cch chars in the buffer
         */

        private int resize ( int cch, int i )
        {
            assert _free >= 0;
            assert cch > 0;
            assert cch > _free;

            int newLen = _buf == null ? _initialBufSize : _buf.length * 2;
            int used = getAvailable();

            while ( newLen - used < cch )
                newLen *= 2;

            char[] newBuf = new char [ newLen ];

            if (used > 0)
            {
                if (_in > _out)
                {
                    assert i == -1 || (i >= _out && i < _in);
                    System.arraycopy( _buf, _out, newBuf, 0, used );
                    i -= _out;
                }
                else
                {
                    assert i == -1 || (i >= _out || i < _in);
                    System.arraycopy( _buf, _out, newBuf, 0, used - _in );
                    System.arraycopy( _buf, 0, newBuf, used - _in, _in );
                    i = i >= _out ? i - _out : i + _out;
                }
                
                _out = 0;
                _in = used;
                _free += newBuf.length - _buf.length;
            }
            else
            {
                _free += newBuf.length;
                assert _in == 0 && _out == 0;
                assert i == -1;
            }

            _buf = newBuf;

            assert _free >= 0;

            return i;
        }

        public int read ( )
        {
            if (ensure( 1 ) == 0)
                return -1;

            assert getAvailable() > 0;

            int ch = _buf[ _out ];

            _out = (_out + 1) % _buf.length;
            _free++;

            return ch;
        }

        public int read ( char[] cbuf, int off, int len )
        {
            // Check for end of stream even if there is no way to return
            // characters because the Reader doc says to return -1 at end of
            // stream.

            int n;

            if ((n = ensure( len )) == 0)
                return -1;

            if (cbuf == null || len <= 0)
                return 0;

            if (n < len)
                len = n;

            if (_out < _in)
            {
                System.arraycopy( _buf, _out, cbuf, off, len );
            }
            else
            {
                int chunk = _buf.length - _out;

                if (chunk >= len)
                    System.arraycopy( _buf, _out, cbuf, off, len );
                else
                {
                    System.arraycopy( _buf, _out, cbuf, off, chunk );
                    System.arraycopy( _buf, 0, cbuf, off + chunk, len - chunk );
                }
            }

            _out = (_out + len) % _buf.length;
            _free += len;

            assert _free >= 0;

            return len;
        }

        public int write ( Writer writer, int cchMin )
        {
            while ( getAvailable() < cchMin)
            {
                if (!process())
                    break;
            }

            int charsAvailable = getAvailable();

            if (charsAvailable > 0)
            {
                // I don't want to deal with the circular cases

                assert _out == 0;

                try
                {
                    writer.write( _buf, 0, charsAvailable );
                    writer.flush();
                }
                catch ( IOException e )
                {
                    throw new RuntimeException( e );
                }

                _free += charsAvailable;
                
                assert _free >= 0;
                
                _in = 0;
            }

            return charsAvailable;
        }

        /**
         * Ensure all text and return it as a string.
         */

        public String saveToString ( )
        {
            // We're gonna build a string.  Instead of using StringBuffer, may
            // as well use my buffer here.  Fill the whole sucker up and
            // create a String!

            while ( process() )
                ;

            assert _out == 0;

            int available = getAvailable();

            return available == 0 ? "" : new String( _buf, _out, available );
        }

        private static final int _initialBufSize = 4096;

        private int _lastEmitIn;
        private int _lastEmitCch;

        private int    _free;
        private int    _in;
        private int    _out;
        private char[] _buf;
    }

    /**
     * A Reader which exposes the text of a part of the tree.
     */

    static final class TextReader extends Reader
    {
        TextReader ( Root r, Splay s, int p, XmlOptions options )
        {
            _textSaver = new TextSaver( r, s, p, options, null );
        }

        public void close ( ) throws IOException { }

        public boolean ready ( ) throws IOException { return true; }

        public int read ( ) throws IOException
        {
            return _textSaver.read();
        }

        public int read ( char[] cbuf ) throws IOException
        {
            return _textSaver.read( cbuf, 0, cbuf == null ? 0 : cbuf.length );
        }

        public int read ( char[] cbuf, int off, int len ) throws IOException
        {
            return _textSaver.read( cbuf, off, len );
        }

        private TextSaver _textSaver;
    }

    /**
     *
     */

    static final class InputStreamSaver extends InputStream
    {
        InputStreamSaver (
            Root r, Splay s, int p, XmlOptions options )
        {
            options = XmlOptions.maskNull(options);
            _byteBuffer = new OutputStreamImpl();

            String encoding = null;
            
            if (r._props.getEncoding() != null)
            {
                encoding =
                    EncodingMap.getIANA2JavaMapping( r._props.getEncoding() );
            }

            if (options.hasOption(XmlOptions.CHARACTER_ENCODING))
            {
                encoding =
                    (String) options.get(XmlOptions.CHARACTER_ENCODING);
            }

            if (encoding != null)
            {
                String ianaEncoding =
                    EncodingMap.getJava2IANAMapping( encoding );

                if (ianaEncoding != null)
                    encoding = ianaEncoding;
            }

            if (encoding == null)
                encoding = EncodingMap.getJava2IANAMapping( "UTF8" );

            String javaEncoding = EncodingMap.getIANA2JavaMapping( encoding );

            if (javaEncoding == null)
            {
                throw
                    new IllegalStateException(
                        "Unknown encoding: " + encoding );
            }

            try
            {
                _converter = new OutputStreamWriter( _byteBuffer, javaEncoding);
            }
            catch ( UnsupportedEncodingException e )
            {
                throw new RuntimeException( e );
            }

            _textSaver = new TextSaver( r, s, p, options, encoding );
        }

        public int read ( )
        {
            return _byteBuffer.read();
        }

        public int read ( byte[] bbuf, int off, int len )
        {
            return _byteBuffer.read ( bbuf, off, len );
        }

        private int ensure ( int cbyte )
        {
            // Even if we're asked to ensure nothing, still try to ensure
            // atleast one byte so we can determine if we're at the
            // end of the stream.

            if (cbyte <= 0)
                cbyte = 1;

            int bytesAvailable = _byteBuffer.getAvailable();

            for ( ; bytesAvailable < cbyte ;
                  bytesAvailable = _byteBuffer.getAvailable() )
            {
                if (_textSaver.write( _converter, 2048 ) < 2048)
                    break;
            }

            bytesAvailable = _byteBuffer.getAvailable();

            if (bytesAvailable == 0)
                return 0;

            return bytesAvailable;
        }

        private final class OutputStreamImpl extends OutputStream
        {
            int read ( )
            {
                if (InputStreamSaver.this.ensure( 1 ) == 0)
                    return -1;

                assert getAvailable() > 0;

                int bite = _buf[ _out ];

                _out = (_out + 1) % _buf.length;
                _free++;

                return bite;
            }

            int read ( byte[] bbuf, int off, int len )
            {
                // Check for end of stream even if there is no way to return
                // characters because the Reader doc says to return -1 at end of
                // stream.

                int n;

                if ((n = ensure( len )) == 0)
                    return -1;

                if (bbuf == null || len <= 0)
                    return 0;

                if (n < len)
                    len = n;

                if (_out < _in)
                {
                    System.arraycopy( _buf, _out, bbuf, off, len );
                }
                else
                {
                    int chunk = _buf.length - _out;

                    if (chunk >= len)
                        System.arraycopy( _buf, _out, bbuf, off, len );
                    else
                    {
                        System.arraycopy( _buf, _out, bbuf, off, chunk );

                        System.arraycopy(
                            _buf, 0, bbuf, off + chunk, len - chunk );
                    }
                }

                _out = (_out + len) % _buf.length;
                _free += len;

                return len;
            }

            int getAvailable ( )
            {
                return _buf == null ? 0 : _buf.length - _free;
            }

            public void write ( int bite )
            {
                if (_free == 0)
                    resize( 1 );

                assert _free > 0;

                _buf[ _in ] = (byte) bite;

                _in = (_in + 1) % _buf.length;
                _free--;
            }

            public void write ( byte[] buf, int off, int cbyte )
            {
                assert cbyte >= 0;

                if (cbyte == 0)
                    return;

                if (_free < cbyte)
                    resize( cbyte );

                if (_in == _out)
                {
                    assert getAvailable() == 0;
                    assert _free == _buf.length - getAvailable();
                    _in = _out = 0;
                }

                int chunk;

                if (_in <= _out || cbyte < (chunk = _buf.length - _in))
                {
                    System.arraycopy( buf, off, _buf, _in, cbyte );
                    _in += cbyte;
                }
                else
                {
                    System.arraycopy( buf, off, _buf, _in, chunk );

                    System.arraycopy(
                        buf, off + chunk, _buf, 0, cbyte - chunk );

                    _in = (_in + cbyte) % _buf.length;
                }

                _free -= cbyte;
            }

            void resize ( int cbyte )
            {
                assert cbyte > _free;

                int newLen = _buf == null ? _initialBufSize : _buf.length * 2;
                int used = getAvailable();

                while ( newLen - used < cbyte )
                    newLen *= 2;

                byte[] newBuf = new byte [ newLen ];

                if (used > 0)
                {
                    if (_out == _in)
                        System.arraycopy( _buf, 0, newBuf, 0, used );
                    else if (_in > _out)
                        System.arraycopy( _buf, _out, newBuf, 0, used );
                    else
                    {
                        System.arraycopy(
                            _buf, _out, newBuf, 0, used - _in );

                        System.arraycopy(
                            _buf, 0, newBuf, used - _in, _in );
                    }

                    _out = 0;
                    _in = used;
                    _free += newBuf.length - _buf.length;
                }
                else
                {
                    _free += newBuf.length;
                    assert _in == 0 && _out == 0;
                }

                _buf = newBuf;
            }

            private static final int _initialBufSize = 4096;

            int    _free;
            int    _in;
            int    _out;
            byte[] _buf;
        }

        private OutputStreamImpl   _byteBuffer;
        private TextSaver          _textSaver;
        private OutputStreamWriter _converter;
    }

    /**
     *
     */

    static final class XmlInputStreamSaver extends Saver
    {
        XmlInputStreamSaver ( Root r, Splay s, int p, XmlOptions options )
        {
            super( r, s, p, options );
        }

        XMLEvent dequeue ( ) throws XMLStreamException
        {
            if (_out == null && !process())
                return null;

            if (_out == null)
                return null;

            XmlEventImpl e = _out;

            if ((_out = _out._next) == null)
                _in = null;

            return e;
        }

        private void enqueue ( XmlEventImpl e )
        {
            assert e._next == null;

            if (_in == null)
            {
                assert _out == null;
                _out = _in = e;
            }
            else
            {
                _in._next = e;
                _in = e;
            }
        }

        //
        //
        //

        protected void emitXmlnsFragment ( Splay s )
        {
            throw new IllegalStateException( "Can't stream an attribute" );
        }

        protected void emitText ( Splay s, int p, int cch )
        {
            assert cch > 0;
            enqueue( new CharacterDataImpl( getRoot(), s, p, null ) );
        }

        protected void emitTextFragment ( Splay s, int p, int cch )
        {
            // BUGBUG - there could be namespaces pushed for this text, not
            // sure how to communicate those, however

            if (_text != null)
            {
                if (_text.length() > 0)
                    emitText( s, p, cch );
                
                return;
            }
            
            if (s != null && cch > 0)
                emitText( s, p, cch );
        }

        protected void emitTextAfter ( Splay s, int p, int cch )
        {
            if (_text == null)
                emitText( s, p, cch );
            else
                enqueue( new CharacterDataImpl( getRoot(), s, -1, text() ) );
        }

        protected void emitEndPrefixMappings ( )
        {
            Root r = getRoot();

            for ( iterateMappings() ; hasMapping() ; nextMapping() )
            {
                String prevPrefixUri = mappingPrevPrefixUri();

                if (prevPrefixUri == null)
                    enqueue( new EndPrefixMappingImpl( r, mappingPrefix() ) );
                else
                {
                    enqueue(
                        new ChangePrefixMappingImpl(
                            r, mappingPrefix(), mappingUri(), prevPrefixUri ) );
                }
            }
        }

        protected void emitEnd ( Splay s, QName name )
        {
            Root r = getRoot();

            if (s.isRoot())
                enqueue( new EndDocumentImpl( r, s ) );
            else if (s.isEnd())
                enqueue( new EndElementImpl( r, s, name, getUriMap() ) );
            else
            {
                assert s.isLeaf();
                enqueue( new EndElementImpl( r, s, name, getUriMap() ) );
            }

            emitEndPrefixMappings();
        }

        protected void emitCommentFragment ( Splay s )
        {
            emitComment( s );
        }

        protected void emitProcinstFragment ( Splay s )
        {
            emitProcinst( s );
        }

        protected void emitDocType(
            String doctypeName, String publicID, String systemID )
        {
        }
        
        protected void emitComment ( Splay s )
        {
            enqueue( new CommentImpl( getRoot(), s ) );
        }

        protected void emitProcinst ( Splay s )
        {
            enqueue( new ProcessingInstructionImpl( getRoot(), s ) );
        }

        protected void emitAttrFragment ( Splay s )
        {
            throw new IllegalStateException( "Can't stream an attribute" );
        }

        protected void emitContainer ( Container c, QName name )
        {
            Root r = getRoot();

            for ( iterateMappings() ; hasMapping() ; nextMapping() )
            {
                enqueue(
                    new StartPrefixMappingImpl(
                        r, mappingPrefix(), mappingUri() ) );
            }

            if (c.isDoc())
                enqueue( new StartDocumentImpl( r, c ) );

            if (name != null)
            {
                enqueue(
                    new StartElementImpl( r, c, name, this ) );
            }

            if (c.isDoc())
            {
                assert c.isDoc();

                if (_text != null)
                {
                    if (_text.length() > 0)
                        enqueue( new CharacterDataImpl( r, c, -1, text() ) );
                }
                else if (c.getCch() > 0)
                    enqueue( new CharacterDataImpl( r, c, 1, null ) );
            }
            else if (c.isLeaf())
            {
                if (_text != null)
                {
                    if (_text.length() > 0)
                        enqueue( new CharacterDataImpl( r, c, -1, text() ));
                }
                else if (c.getCchValue() > 0)
                    enqueue( new CharacterDataImpl( r, c, 1, null ) );

                enqueue( new EndElementImpl( r, c, name, getUriMap() ) );

                emitEndPrefixMappings();
            }
        }


        //
        //
        //

        private static XMLName computeName ( QName n, Map uriMap )
        {
            String uri = n.getNamespaceURI();

            if (uri.length() == 0)
                uri = null;

            assert n.getLocalPart().length() > 0;

            // The following assert may fire if someone computes a name
            // of an element/attr too late (after other events have been
            // enqueued and the uri map has been updated.  I check later
            // to make sure we don't crash, however.

            assert uri == null || uriMap.containsKey( uri ) : "Problem uri " + uri;

            String prefix = null;

            if (uri != null)
            {
                prefix = (String) uriMap.get( uri );

                if (prefix != null && prefix.length() == 0)
                    prefix = null;
            }

            return new XmlNameImpl( uri, n.getLocalPart(), prefix );
        }

        private static abstract class XmlEventImpl extends XmlEventBase
        {
            public Object monitor()
            {
                return _root;
            }
            
            XmlEventImpl ( int type, Root r, Splay s )
            {
                super( type );
                _root = r;
                _splay = s;
                _version = _root.getVersion();
            }

            public XMLName getName ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
                    return null;
                }
            }

            public XMLName getSchemaType ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
                    throw new RuntimeException( "NYI" );
                }
            }

            public boolean hasName ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
                    return false;
                }
            }

            public final Location getLocation ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
                    // TODO - perhaps I can save a location goober sometimes?
                    return null;
                }
            }

            protected final void checkVersion ( )
            {
                if (_version != _root.getVersion())
                    throw new ConcurrentModificationException( "Document changed" );
            }

            protected final Root getRoot ( )
            {
                return _root;
            }

            protected final Splay getSplay ( )
            {
                return _splay;
            }

            private Root  _root;
            private Splay _splay;
            private long  _version;

            XmlEventImpl _next;
        }

        private static class StartDocumentImpl
            extends XmlEventImpl implements StartDocument
        {
            StartDocumentImpl ( Root r, Splay s )
            {
                super( XMLEvent.START_DOCUMENT, r, s );
            }

            public String getSystemId ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
                    return getRoot()._props.getDoctypeSystemId();
                }
            }

            public String getCharacterEncodingScheme ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
                    return getRoot()._props.getEncoding();
                }
            }

            public boolean isStandalone ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
                    return getRoot()._standAlone;
                }
            }

            public String getVersion ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
                    return getRoot()._props.getVersion();
                }
            }
        }

        private static class StartElementImpl
            extends XmlEventImpl implements StartElement
        {
            StartElementImpl ( Root r, Splay s, QName name, Saver saver )
            {
                super( XMLEvent.START_ELEMENT, r, s );

                _name = computeName( name, saver.getUriMap() );

                _prefixMap = saver.getPrefixMap();

                AttributeImpl lastAttr = null;

                for ( Iterator i = saver._attrs.keySet().iterator() ;
                      i.hasNext() ; )
                {
                    Splay a = (Splay) i.next();
                    
                    AttributeImpl attr =
                        new NormalAttributeImpl(
                            r, a, (String) saver._attrs.get( a ),
                            saver.getUriMap() );
                    
                    if (_attributes == null)
                        _attributes = attr;
                    else
                        lastAttr._next = attr;

                    lastAttr = attr;
                }

                lastAttr = null;

                for ( saver.iterateMappings() ;
                      saver.hasMapping() ; saver.nextMapping() )
                {
                    AttributeImpl attr =
                        new XmlnsAttributeImpl(
                            r, saver.mappingPrefix(), saver.mappingUri(),
                            saver.getUriMap() );

                    if (_namespaces == null)
                        _namespaces = attr;
                    else
                        lastAttr._next = attr;

                    lastAttr = attr;
                }
            }

            public boolean hasName()
            {
                synchronized (monitor())
                {
                    checkVersion();
                    return true;
                }
            }

            public XMLName getName ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
                    return _name;
                }
            }

            public AttributeIterator getAttributes ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
    
                    return
                        new AttributeIteratorImpl( getRoot(), _attributes, null );
                }
            }

            public AttributeIterator getNamespaces ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
    
                    return
                        new AttributeIteratorImpl( getRoot(), null, _namespaces );
                }
            }

            public AttributeIterator getAttributesAndNamespaces ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
    
                    return
                        new AttributeIteratorImpl(
                            getRoot(), _attributes, _namespaces );
                }
            }

            public Attribute getAttributeByName ( XMLName xmlName )
            {
                synchronized (monitor())
                {
                    checkVersion();
    
                    for ( AttributeImpl a = _attributes ; a != null ; a = a._next )
                    {
                        if (xmlName.equals( a.getName() ))
                            return a;
                    }
    
                    return null;
                }
            }

            public String getNamespaceUri ( String prefix )
            {
                synchronized (monitor())
                {
                    checkVersion();
    
                    return
                        (String) _prefixMap.get( prefix == null ? "" : prefix );
                }
            }

            public Map getNamespaceMap ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
    
                    return _prefixMap;
                }
            }

            private static class AttributeIteratorImpl
                implements AttributeIterator
            {
                public Object monitor()
                {
                    return _root;
                }
                
                AttributeIteratorImpl (
                    Root r, AttributeImpl attributes, AttributeImpl namespaces )
                {
                    _root = r;
                    _version = r.getVersion();
                    _attributes = attributes;
                    _namespaces = namespaces;
                }

                public Attribute next ( )
                {
                    synchronized (monitor())
                    {
                        checkVersion();
    
                        AttributeImpl attr = null;
    
                        if (_attributes != null)
                        {
                            attr = _attributes;
                            _attributes = attr._next;
                        }
                        else if (_namespaces != null)
                        {
                            attr = _namespaces;
                            _namespaces = attr._next;
                        }
    
                        return attr;
                    }
                }

                public boolean hasNext ( )
                {
                    synchronized (monitor())
                    {
                        checkVersion();
    
                        return _attributes != null || _namespaces != null;
                    }
                }

                public Attribute peek ( )
                {
                    synchronized (monitor())
                    {
                        checkVersion();
    
                        if (_attributes != null)
                            return _attributes;
                        else if (_namespaces != null)
                            return _namespaces;
    
                        return null;
                    }
                }

                public void skip ( )
                {
                    synchronized (monitor())
                    {
                        checkVersion();
    
                        if (_attributes != null)
                            _attributes = _attributes._next;
                        else if (_namespaces != null)
                            _namespaces = _namespaces._next;
                    }
                }

                private final void checkVersion ( )
                {
                    if (_version != _root.getVersion())
                        throw new IllegalStateException( "Document changed" );
                }

                private Root          _root;
                private long          _version;
                private AttributeImpl _attributes;
                private AttributeImpl _namespaces;
            }

            private static abstract class AttributeImpl implements Attribute
            {
                public Object monitor()
                {
                    return _root;
                }
                
                AttributeImpl ( Root r )
                {
                    _root = r;
                    _version = r.getVersion();
                }

                public XMLName getName ( )
                {
                    synchronized (monitor())
                    {
                        checkVersion();
                        return _name;
                    }
                }

                public String getType ( )
                {
                    synchronized (monitor())
                    {
                        checkVersion();
    // TODO - Make sure throwing away this DTD info is ok.
    // Is there schema info which can return more useful info?
                        return "CDATA";
                    }
                }

                public XMLName getSchemaType ( )
                {
                    synchronized (monitor())
                    {
                        checkVersion();
    // TODO - Can I return something reasonable here?
                        return null;
                    }
                }

                protected final void checkVersion ( )
                {
                    if (_version != _root.getVersion())
                        throw new IllegalStateException( "Document changed" );
                }

                AttributeImpl _next;

                protected XMLName _name;

                protected Root  _root;
                private long  _version;
            }

            private static class XmlnsAttributeImpl extends AttributeImpl
            {
                XmlnsAttributeImpl (
                    Root r, String prefix, String uri, Map uriMap )
                {
                    super( r );

                    _uri = uri;

                    String local;

                    if (prefix.length() == 0)
                    {
                        prefix = null;
                        local = "xmlns";
                    }
                    else
                    {
                        local = prefix;
                        prefix = "xmlns";
                    }

                    _name = new XmlNameImpl( null, local, prefix );
                }

                public String getValue ( )
                {
                    synchronized (monitor())
                    {
                        checkVersion();
                        return _uri;
                    }
                }

                private String _uri;
            }

            private static class NormalAttributeImpl extends AttributeImpl
            {
                NormalAttributeImpl (
                    Root r, Splay s, String value, Map uriMap )
                {
                    super( r );
                    assert s.isNormalAttr();
                    _splay = s;
                    _value = value;
                    _name = computeName( s.getName(), uriMap );
                }

                public String getValue ( )
                {
                    synchronized (monitor())
                    {
                        checkVersion();
    
                        return _value != null ? _value : _splay.getText( _root );
                    }
                }

                private String _value; // If invalid in the store
                private Splay _splay;
            }

            private XMLName _name;
            private Map     _prefixMap;

            private AttributeImpl _attributes;
            private AttributeImpl _namespaces;
        }

        private static class StartPrefixMappingImpl
            extends XmlEventImpl implements StartPrefixMapping
        {
            StartPrefixMappingImpl ( Root r, String prefix, String uri )
            {
                super( XMLEvent.START_PREFIX_MAPPING, r, null );

                _prefix = prefix;
                _uri = uri;
            }

            public String getNamespaceUri ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
                    return _uri;
                }
            }

            public String getPrefix ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
                    return _prefix;
                }
            }

            private String _prefix, _uri;
        }

        private static class ChangePrefixMappingImpl
            extends XmlEventImpl implements ChangePrefixMapping
        {
            ChangePrefixMappingImpl (
                Root r, String prefix, String oldUri, String newUri )
            {
                super( XMLEvent.CHANGE_PREFIX_MAPPING, r, null );

                _oldUri = oldUri;
                _newUri = newUri;
                _prefix = prefix;
            }

            public String getOldNamespaceUri ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
                    return _oldUri;
                }
            }

            public String getNewNamespaceUri ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
                    return _newUri;
                }
            }

            public String getPrefix ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
                    return _prefix;
                }
            }

            private String _oldUri, _newUri, _prefix;
        }

        private static class EndPrefixMappingImpl
            extends XmlEventImpl implements EndPrefixMapping
        {
            EndPrefixMappingImpl ( Root r, String prefix )
            {
                super( XMLEvent.END_PREFIX_MAPPING, r, null );
                _prefix = prefix;
            }

            public String getPrefix ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
                    return _prefix;
                }
            }

            private String _prefix;
        }

        private static class CharacterDataImpl
            extends XmlEventImpl implements CharacterData
        {
            CharacterDataImpl ( Root r, Splay s, int p, String charData )
            {
                super( XMLEvent.CHARACTER_DATA, r, s );

                assert p > 0 || (charData != null && charData.length() > 0);

                _pos = p;
                _charData = charData;
            }

            public String getContent ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
    
                    Splay s = getSplay();
    
                    if (_pos == -1)
                        return _charData;
    
                    Root r = getRoot();
    
                    return
                        r._text.fetch(
                            s.getCpForPos( r, _pos ), s.getPostCch( _pos ) );
                }
            }

            public boolean hasContent ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
                    return true;
                }
            }

            private int    _pos;
            private String _charData;
        }

        private static class EndElementImpl
            extends XmlEventImpl implements EndElement
        {
            EndElementImpl ( Root r, Splay s, QName name, Map uriMap )
            {
                super( XMLEvent.END_ELEMENT, r, s );

                assert s.isLeaf() || s.isEnd();

                if (name == null)
                {
                    if (s.isEnd())
                        s = s.getContainer();
                    
                    name = s.getName();
                }

                _name = computeName( name, uriMap );
            }

            public boolean hasName ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
                    return true;
                }
            }

            public XMLName getName ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
                    return _name;
                }
            }

            private XMLName _name;
        }

        private static class EndDocumentImpl
            extends XmlEventImpl implements EndDocument
        {
            EndDocumentImpl ( Root r, Splay s )
            {
                super( XMLEvent.END_DOCUMENT, r, s );
            }
        }

        private static class CommentImpl
            extends XmlEventImpl implements Comment
        {
            CommentImpl ( Root r, Splay s )
            {
                super( XMLEvent.COMMENT, r, s );
                assert s.isComment();
            }

            public String getContent ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
    
                    Splay s = getSplay();
                    return s.getCch() == 0 ? null : s.getText( getRoot() );
                }
            }

            public boolean hasContent ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
    
                    return getSplay().getCch() > 0;
                }
            }
        }

        private static class ProcessingInstructionImpl
            extends XmlEventImpl implements ProcessingInstruction
        {
            ProcessingInstructionImpl ( Root r, Splay s )
            {
                super( XMLEvent.PROCESSING_INSTRUCTION, r, s );
                assert s.isProcinst();
            }

            public String getTarget ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
                    return getSplay().getLocal();
                }
            }

            public String getData ( )
            {
                synchronized (monitor())
                {
                    checkVersion();
                    Splay s = getSplay();
                    return s.getCch() == 0 ? null : s.getText( getRoot() );
                }
            }
        }

        private XmlEventImpl _in, _out;
    }

    static final class XmlInputStreamImpl extends GenericXmlInputStream
    {
        XmlInputStreamImpl ( Root r, Splay s, int p, XmlOptions options )
        {
            _xmlInputStreamSaver =
                new XmlInputStreamSaver( r, s, p, options );

            // Make the saver grind away just a bit to throw any exceptions
            // related to the inability to create a stream on this xml
            
            _xmlInputStreamSaver.process();
        }

        protected XMLEvent nextEvent ( ) throws XMLStreamException
        {
            return _xmlInputStreamSaver.dequeue();
        }

        private XmlInputStreamSaver _xmlInputStreamSaver;
    }

    //
    //
    //

    static final class ValidatorSaver
        extends Saver implements ValidatorListener.Event
    {
        ValidatorSaver (
            Root r, Splay s, int p,
            XmlOptions options, ValidatorListener vEventSink )
        {
            super( r, s, p, options );

            _wantDupAttrs = true;

            assert p == 0;

            _startSplay = s;
            _vEventSink = vEventSink;

            while ( process() )
                ;  // Empty
        }

        protected void emitXmlnsFragment ( Splay s )
        {
            throw new IllegalStateException();
        }

        protected void emitTextFragment ( Splay s, int p, int cch )
        {
            throw new IllegalStateException();
        }

        protected void emitCommentFragment ( Splay s )
        {
            throw new IllegalStateException();
        }

        protected void emitProcinstFragment ( Splay s )
        {
            throw new IllegalStateException();
        }

        protected void emitTextAfter ( Splay s, int p, int cch )
        {
            if (_text == null)
            {
                assert cch > 0;
                emitEvent( ValidatorListener.TEXT, s, p, null, s, p );
            }
            else
                emitEvent( ValidatorListener.TEXT, s, p, null, text() );
        }

        protected void emitEnd ( Splay s, QName name )
        {
            emitEvent( ValidatorListener.END, s, 0 );
        }

        protected void emitDocType(
            String doctypeName, String publicID, String systemID )
        {
        }
        
        protected void emitComment ( Splay s )
        {
            if (s.getCchAfter() > 0)
                emitEvent( ValidatorListener.TEXT, s, 0, null, s, 1 );
        }

        protected void emitProcinst ( Splay s )
        {
            if (s.getCchAfter() > 0)
                emitEvent( ValidatorListener.TEXT, s, 0, null, s, 1 );
        }

        protected void emitAttrFragment ( Splay s )
        {
            emitEvent( ValidatorListener.BEGIN, s, 0, null );

            if (_text != null)
            {
                if (_text.length() > 0)
                    emitEvent( ValidatorListener.TEXT, s, 0, null, text() );
            }
            else if (s.getCch() > 0)
                emitEvent( ValidatorListener.TEXT, s, 0, null, s, 0 );

            emitEvent( ValidatorListener.END, s, 0 );
        }

        protected void emitContainer ( Container c, QName name )
        {
            assert _xsiNoLoc == null;
            assert _xsiLoc == null;
            assert _xsiType == null;
            assert _xsiNil == null;

            for ( Splay s = c.nextSplay() ; s.isAttr() ; s = s.nextSplay() )
            {
                if (s.isXsiAttr())
                {
                    String local = s.getLocal();

                    if (local.equals( "type" ))
                        _xsiType = s;
                    else if (local.equals( "nil" ))
                        _xsiNil = s;
                    else if (local.equals( "schemaLocation" ))
                        _xsiLoc = s;
                    else if (local.equals( "noNamespaceSchemaLocation" ))
                        _xsiNoLoc = s;
                }
            }

            emitEvent(
                ValidatorListener.BEGIN, c, 0,
                c == _startSplay ? null : name );

            _xsiNoLoc = _xsiLoc = _xsiType = _xsiNil = null;

            for ( Iterator i = _attrs.keySet().iterator() ; i.hasNext() ; )
            {
                Splay s = (Splay) i.next();
                
                if (s.isXsiAttr())
                {
                    String local = s.getLocal();

                    if (local.equals( "type" ) ||
                        local.equals( "nil" ) ||
                        local.equals( "schemaLocation" ) ||
                        local.equals( "noNamespaceSchemaLocation" ))
                    {
                        continue;
                    }
                }

                String invalidAttrValue = (String) _attrs.get( s );

                if (invalidAttrValue == null)
                {
                    emitEvent(
                        ValidatorListener.ATTR, s, 0, s.getName(), s, 0 );
                }
                else
                {
                    emitEvent(
                        ValidatorListener.ATTR, s, 0, s.getName(),
                        invalidAttrValue );
                }
            }

            emitEvent( ValidatorListener.ENDATTRS, c, 0 );

            if (c.isDoc())
            {
                assert c.isDoc();

                if (_text != null)
                {
                    if (_text.length() > 0)
                        emitEvent( ValidatorListener.TEXT, c, 1, null, text() );
                }
                else if (c.getCch() > 0)
                    emitEvent( ValidatorListener.TEXT, c, 1, null, c, 1 );
            }
            else if (c.isLeaf())
            {
                int cch = _text != null ? _text.length() : c.getCchValue();

                if (cch > 0)
                {
                    if (_text != null)
                    {
                        emitEvent(
                            ValidatorListener.TEXT, c, 1, null, text() );
                    }
                    else
                    {
                        emitEvent(
                            ValidatorListener.TEXT, c, 1, null, c, 1 );
                    }
                }

                emitEvent( ValidatorListener.END, c, c.getPosLeafEnd() );
            }
        }

        protected void emitEvent ( int kind, Splay sLoc, int pLoc )
            { emitEvent( kind, sLoc, pLoc, null, null, 0, null ); }

        protected void emitEvent ( int kind, Splay sLoc, int pLoc, QName name )
            { emitEvent( kind, sLoc, pLoc, name, null, 0, null ); }

        protected void emitEvent (
            int kind, Splay sLoc, int pLoc, QName name, String text )
                { emitEvent ( kind, sLoc, pLoc, name, null, 0, text ); }

        protected void emitEvent (
            int kind, Splay sLoc, int pLoc, QName name, Splay sText, int pText )
                { emitEvent ( kind, sLoc, pLoc, name, sText, pText, null ); }

        protected void emitEvent (
            int kind, Splay sLoc, int pLoc, QName name,
            Splay sText, int pText, String text )
        {
            if (kind == ValidatorListener.TEXT && _emittedText)
                return;

            boolean hasText = text != null || sText != null;

            assert
                !hasText ||
                    (kind == ValidatorListener.ATTR ||
                        kind == ValidatorListener.TEXT);

            assert kind != ValidatorListener.ATTR || hasText;
            assert kind != ValidatorListener.TEXT || hasText;

            assert kind != ValidatorListener.ATTR || name != null;

            _name = name;
            _sText = sText;
            _pText = pText;
            _eventText = text;
            _hasText = hasText;
            _sLoc = sLoc;
            _pLoc = pLoc;

            _vEventSink.nextEvent( kind, this );

            _emittedText = kind == ValidatorListener.TEXT;
        }

        //
        //
        //

        public XmlCursor getLocationAsCursor ( )
        {
            checkVersion();
            return new Cursor( getRoot(), _sLoc, _pLoc );
        }

        public boolean getXsiType ( Chars chars )
        {
            if (_xsiType == null)
                return false;

            setChars( chars, PRESERVE, null, _xsiType, 0 );

            return true;
        }

        public boolean getXsiNil ( Chars chars )
        {
            if (_xsiNil == null)
                return false;


            setChars( chars, PRESERVE, null, _xsiNil, 0 );

            return true;
        }

        public boolean getXsiLoc ( Chars chars )
        {
            if (_xsiLoc == null)
                return false;

            setChars( chars, PRESERVE, null, _xsiLoc, 0 );

            return true;
        }

        public boolean getXsiNoLoc ( Chars chars )
        {
            if (_xsiNoLoc == null)
                return false;

            setChars( chars, PRESERVE, null, _xsiNoLoc, 0 );

            return true;
        }

        public QName getName ( )
        {
            return _name;
        }

        private void setChars (
            Chars chars, int wsr, String string, Splay sText, int pText )
        {
            assert string != null || sText != null;

            checkVersion();

            Root r = getRoot();

            chars.buffer = null;
            chars.string = null;

            if (string != null)
            {
                chars.string = string;
            }
            else if (pText == 0)
            {
                chars.buffer = r._text._buf;
                chars.length = sText.getCch();

                chars.offset =
                    r._text.unObscure(
                        r.getCp( sText ), chars.length );
            }
            else if (pText == 1 && sText.isLeaf())
            {
                chars.buffer = r._text._buf;
                chars.length = sText.getCchValue();

                chars.offset =
                    r._text.unObscure(
                        r.getCp( sText ), chars.length );
            }
            else
            {
                assert pText == sText.getPosAfter();

                boolean moreText = false;

                for ( Splay t = sText.nextNonAttrSplay() ; ;
                      t = t.nextSplay() )
                {
                    if (!t.isComment() && !t.isProcinst())
                        break;

                    if (t.getCchAfter() > 0)
                    {
                        moreText = true;
                        break;
                    }
                }

                if (!moreText)
                {
                    chars.buffer = r._text._buf;
                    chars.length = sText.getCchAfter();

                    chars.offset =
                        r._text.unObscure(
                            sText.getCpForPos( r, pText ),
                            chars.length );
                }
                else
                {
                    StringBuffer sb = new StringBuffer();

                    int cch = sText.getCchAfter();

                    int off =
                        r._text.unObscure(
                            sText.getCpForPos( r, pText ), cch );

                    sb.append( r._text._buf, off, cch );

                    for ( Splay t = sText.nextNonAttrSplay() ; ;
                          t = t.nextSplay() )
                    {
                        if (!t.isComment() && !t.isProcinst())
                            break;

                        if (t.getCchAfter() > 0)
                        {
                            cch = t.getCchAfter();

                            off =
                                r._text.unObscure(
                                    t.getCpForPos( r, 1 ), cch );

                            sb.append( r._text._buf, off, cch );
                        }
                    }

                    chars.length = sb.length();
                    chars.buffer = new char [ chars.length ];
                    chars.offset = 0;

                    sb.getChars( 0, chars.length, chars.buffer, 0 );
                }
            }

            if (wsr != PRESERVE)
            {
                // TODO - this is quick, dirty and very inefficient
                //        make it faster!

                String str = chars.asString();
                StringBuffer sb = new StringBuffer();
                int state = -1, nSpaces = 0, cch = str.length();

                for ( int i = 0 ; i < cch ; i++ )
                {
                    char ch = str.charAt( i );

                    if (ch == '\n' || ch == '\r' || ch == '\t')
                        ch = ' ';

                    if (wsr == COLLAPSE)
                    {
                        if (ch == ' ')
                        {
                            if (state == -1)
                                continue;

                            nSpaces++;

                            continue;
                        }

                        if (nSpaces > 1)
                            nSpaces = 1;

                        for ( ; nSpaces > 0 ; nSpaces-- )
                            sb.append( ' ' );

                        state = 0;
                    }

                    sb.append( ch );
                }

                chars.string = sb.toString();
                chars.buffer = null;
            }
        }

        public void getText ( Chars chars )
        {
            getText( chars, PRESERVE );
        }

        public void getText ( Chars chars, int wsr )
        {
            if (!_hasText)
                throw new RuntimeException( "No text for this event");

            setChars( chars, wsr, _eventText, _sText, _pText );
        }

        // TODO - rather expensive to make Chars and getText and get
        // String
        public boolean textIsWhitespace ( )
        {
            Chars chars = new Chars();
            getText( chars );
            String s = chars.asString();

            for ( int i = 0 ; i < s.length() ; i++ )
            {
                switch ( s.charAt( i ) )
                {
                    case ' ':
                    case '\n':
                    case '\r':
                    case '\t':
                        break;

                    default :
                        return false;
                }
            }

            return true;
        }

        private ValidatorListener _vEventSink;
        private Splay             _startSplay;
        private boolean           _emittedText;

        private QName   _name;
        private Splay   _xsiType;
        private Splay   _xsiNil;
        private Splay   _xsiLoc;
        private Splay   _xsiNoLoc;
        private String  _eventText;
        private Splay   _sText;
        private int     _pText;
        private Splay   _sLoc;
        private int     _pLoc;
        private boolean _hasText;
    }

    //
    //
    //

    static final class SaxSaver extends Saver
    {
        SaxSaver (
            Root r, Splay s, int p, XmlOptions options,
            ContentHandler contentHandler, LexicalHandler lexicalhandler )
                throws SAXException
        {
            super( r, s, p, options );

            _attributes = new AttributesImpl();
            
            _wantFragTest = true;
            
            _contentHandler = contentHandler;
            _lexicalhandler = lexicalhandler;

//            _contentHandler.setDocumentLocator( hhmmmm  );
            
            _contentHandler.startDocument();

            try
            {
                while ( process() )
                    ;
            }
            catch ( SaverSAXException e )
            {
                throw e._saxException;
            }
            
            _contentHandler.endDocument();
        }

        private class SaverSAXException extends RuntimeException
        {
            SaverSAXException ( SAXException e )
            {
                _saxException = e;
            }
            
            SAXException _saxException;
        }

        private void throwSaxException ( SAXException e )
        {
            throw new SaverSAXException( e );
        }

        protected void emitContainer ( Container c, QName name )
        {
            if (c.isBegin())
            {
                emitContainerHelper( c, name, null, null );

                if (c.isLeaf())
                {
                    int cch = _text == null ? c.getCchValue() : _text.length();

                    if (cch > 0)
                    {
                        if (_text == null)
                            emitCharacters( c, 0, cch );
                        else
                            emitCharacters( _text );
                    }
                    
                    emitEndElement( c.getName() );
                }
            }
            else
            {
                assert c.isDoc();

                if (name != null)
                    emitContainerHelper( c, name, null, null );

                if (_text == null)
                    emitCharacters( c, 0, c.getCch() );
                else
                    emitCharacters( _text );
            }
        }

        private void emitAttrHelper ( Splay s, String value )
        {
            assert s.isNormalAttr();
            
            String local = s.getLocal();
            String uri = s.getUri();

            _attributes.addAttribute(
                s.getUri(), s.getLocal(),
                getPrefixedName( s.getName() ),
                "CDATA",
                value == null ? s.getText( getRoot() ) : value );
        }

        private void addNamespaceAttr ( String prefix, String uri )
        {
            try
            {
                _contentHandler.startPrefixMapping( prefix, uri );
            }
            catch ( SAXException e )
            {
                throwSaxException( e );
            }

            if (prefix.length() == 0)
                _attributes.addAttribute( "", "", "xmlns", "CDATA", uri );
            else
            {
                _attributes.addAttribute(
                    "", "", "xmlns:" + prefix, "CDATA", uri );
            }
        }
        
        private void emitNamespacesHelper ( )
        {
            for ( iterateMappings() ; hasMapping() ; nextMapping() )
                addNamespaceAttr( mappingPrefix(), mappingUri() );
        }

        private void emitContainerHelper (
            Container c, QName name,
            Splay extraAttr, StringBuffer extraAttrText )
        {
            assert name != null;
            
            _attributes.clear();

            if (_saveNamespacesFirst)
                emitNamespacesHelper();

            for ( Iterator i = _attrs.keySet().iterator() ; i.hasNext() ; )
            {
                Splay s = (Splay) i.next();
                emitAttrHelper( s, (String) _attrs.get( s ) );
            }

            if (extraAttr != null)
            {
                emitAttrHelper(
                    extraAttr,
                    extraAttrText == null ? null : extraAttrText.toString() );
            }

            if (!_saveNamespacesFirst)
                emitNamespacesHelper();
            
            emitElement( name, getPrefixedName( name ) );
        }

        private void emitCharacters ( char[] buf, int off, int cch )
        {
            try
            {
                _contentHandler.characters( buf, off, cch );
            }
            catch ( SAXException e )
            {
                throwSaxException( e );
            }
        }
        
        private void emitCharacters ( Splay s, int p, int cch )
        {
            emitCharacters( s.getCpForPos( getRoot(), p ), cch );
        }
        
        private void emitCharacters ( int cp, int cch )
        {
            if (cch == 0)
                return;

            emitCharacters(
                getRoot()._text._buf, getRoot()._text.unObscure( cp, cch ),
                cch );
        }
        
        private void emitCharacters ( StringBuffer sb )
        {
            if (sb.length() == 0)
                return;
            
            String text = sb.toString(); // Inefficient, use a shared char[]
            
            emitCharacters( text.toCharArray(), 0, text.length() );
        }
        
        private void emitEndElement ( QName name )
        {
            try
            {
                _contentHandler.endElement(
                    name.getNamespaceURI(), name.getLocalPart(),
                    getPrefixedName( name ) );
            }
            catch ( SAXException e )
            {
                throwSaxException( e );
            }
            
            for ( iterateMappings() ; hasMapping() ; nextMapping() )
            {
                try
                {
                    _contentHandler.endPrefixMapping( mappingPrefix() );
                }
                catch ( SAXException e )
                {
                    throwSaxException( e );
                }
            }
        }
        
        private void endNamespaces ( )
        {
            try
            {
                for ( int i = 0 ; i < _attributes.getLength() ; i ++ )
                {
                    String qn = _attributes.getQName( i );
                    
                    if (!qn.startsWith( "xmlns" ))
                        continue;

                    int j = qn.indexOf( ':' );

                    if (j >= 0)
                    {
                        _contentHandler.endPrefixMapping(
                            qn.substring( j + 1 ) );
                    }
                    else
                        _contentHandler.endPrefixMapping( "" );
                }
            }
            catch ( SAXException e )
            {
                throwSaxException( e );
            }
        }
                
        private String getPrefixedName ( QName name )
        {
            String ns = name.getNamespaceURI();
            String lp = name.getLocalPart();

            if (ns.length() == 0)
                return lp;

            String prefix = getUriMapping( ns );

            if (prefix.length() == 0)
                return lp;

            return prefix + ":" + lp;
        }

        private void emitElement ( QName name, String prefixedName )
        {
            try
            {
                _contentHandler.startElement(
                    name.getNamespaceURI(), name.getLocalPart(),
                    getPrefixedName( name ), _attributes );
            }
            catch ( SAXException e )
            {
                throwSaxException( e );
            }
        }

        protected void emitEnd ( Splay s, QName name )
        {
            if (name != null)
                emitEndElement( name );
        }

        protected void emitTextAfter ( Splay s, int p, int cch )
        {
            if (_text == null)
                emitCharacters( s, p, cch );
            else
                emitCharacters( _text );
        }

        protected void emitDocType(
            String doctypeName, String publicID, String systemID )
        {
            if (_lexicalhandler != null)
            {
                try
                {
                    _lexicalhandler.startDTD( doctypeName, publicID, systemID );
                    _lexicalhandler.endDTD();
                }
                catch ( SAXException e )
                {
                    throwSaxException( e );
                }
            }
        }
        
        protected void emitComment ( Splay s )
        {
            if (_lexicalhandler != null)
            {
                int cp = getRoot().getCp( s );
                int cch = s.getCchValue();

                try
                {
                    _lexicalhandler.comment(
                        getRoot()._text._buf,
                        getRoot()._text.unObscure( cp, cch ),
                        cch );
                }
                catch ( SAXException e )
                {
                    throwSaxException( e );
                }
            }
        }

        protected void emitProcinst ( Splay s )
        {
            try
            {
                _contentHandler.processingInstruction(
                    s.getLocal(), s.getText( getRoot() ) );
            }
            catch ( SAXException e )
            {
                throwSaxException( e );
            }
        }

        protected void emitTextFragment ( Splay s, int p, int cch )
        {
            emitContainerHelper( null, _fragment, null, null );
            
            if (_text != null)
            {
                if (_text.length() > 0)
                    emitCharacters( _text );
            }
            else if (s != null && cch > 0)
                emitCharacters( s, p, cch );

            emitEndElement( _fragment );
        }

        protected void emitXmlnsFragment ( Splay s )
        {
            emitContainerHelper( null, _fragment, null, null );
            emitEndElement( _fragment );
        }

        protected void emitAttrFragment ( Splay s )
        {
            emitContainerHelper( null, _fragment, s, _text );
            emitEndElement( _fragment );
        }

        protected void emitCommentFragment ( Splay s )
        {
            emitContainerHelper( null, _fragment, null, null );
            emitComment( s );
            emitEndElement( _fragment );
        }

        protected void emitProcinstFragment ( Splay s )
        {
            emitContainerHelper( null, _fragment, null, null );
            emitProcinst( s );
            emitEndElement( _fragment );
        }
        
        private AttributesImpl _attributes;

        private SAXException _saxException;
        
        private ContentHandler _contentHandler;
        private LexicalHandler _lexicalhandler;
    }

            
    //
    //
    //

    private static ThreadLocal _threadDocumentBuilderFactory =
        new ThreadLocal()
        {
            protected Object initialValue()
            {
                try
                {
                    return DocumentBuilderFactory.newInstance().newDocumentBuilder();
                }
                catch ( ParserConfigurationException e )
                {
                    throw new RuntimeException( e.getMessage(), e );
                }
            }
        };
    
    static final class DomSaver extends Saver
    {
        DomSaver ( Root r, Splay s, int p, boolean createDoc, XmlOptions options )
        {
            super( r, s, p, options );
            _createDoc = createDoc;
        }

        Node exportDom ( )
            throws Exception
        {
            // TODO - add an options which specifies a Document with which
            // to create the fragment

            _doc = ((DocumentBuilder) _threadDocumentBuilderFactory.get()).newDocument();

            Node result;

            if (_createDoc)
            {
                result = _currentNode = _doc;
            }
            else
            {
                DocumentFragment frag = _doc.createDocumentFragment();
                result = _currentNode = frag;
            }

            while ( process() )
                ;

            return result;
        }

        protected void emitContainer ( Container c, QName name )
        {
            Root r = getRoot();

            if (c.isDoc())
            {
                if (hasMappings())
                {
                    throw new IllegalStateException(
                        "Namespace attribute not associated with an element" );
                }

                for ( Splay s = c.nextSplay() ; s.isAttr() ; s = s.nextSplay() )
                {
                    if (s.isNormalAttr())
                    {
                        throw new IllegalStateException(
                            "Attribute not associated with an element" );
                    }
                }

                String text = null;

                if (_text != null)
                {
                    if (_text.length() > 0)
                        text = text();
                }
                else if (c.getCch() > 0)
                    text = r._text.fetch( 0, c.getCch() );

                if (text != null && _currentNode != _doc)
                {
                    _currentNode.insertBefore(
                        _doc.createTextNode( text ), null );
                }
            }
            else
            {
                assert c.isBegin();

                String qname = c.getLocal();

                if (c.getUri().length() > 0)
                {
                    String prefix = getUriMapping( c.getUri() );

                    if (prefix.length() > 0)
                        qname = prefix + ":" + qname;
                }

                Element e = _doc.createElementNS( c.getUri(), qname );

                _currentNode.insertBefore( e, null );

                for ( iterateMappings() ; hasMapping() ; nextMapping() )
                {
                    String prefix = mappingPrefix();

                    if (prefix.length() == 0)
                        qname = "xmlns";
                    else
                        qname = "xmlns:" + prefix;

                    e.setAttributeNS( Splay._xmlnsUri, qname, mappingUri() );
                }

                for ( Iterator i = _attrs.keySet().iterator() ; i.hasNext() ; )
                {
                    Splay s = (Splay) i.next();
                    
                    qname = s.getLocal();

                    if (s.getUri().length() > 0)
                    {
                        String prefix = getUriMapping( s.getUri() );

                        if (prefix.length() > 0)
                            qname = prefix + ":" + qname;
                    }

                    String invalidAttrValue = (String) _attrs.get( s );

                    e.setAttributeNS(
                        s.getUri(), qname,
                        invalidAttrValue == null
                            ? s.getText( r )
                            : invalidAttrValue );
                }

                if (c.isLeaf())
                {
                    String text = null;

                    if (_text != null)
                    {
                        if (_text.length() > 0)
                            text = text();
                    }
                    else if (c.getCchValue() > 0)
                    {
                        text =
                            r._text.fetch(
                                c.getCpForPos( r, 1 ), c.getCchValue() );
                    }

                    if (text != null)
                        e.insertBefore( _doc.createTextNode( text ), null );
                }
                else
                {
                    _currentNode = e;
                }
            }
        }

        protected void emitEnd ( Splay s, QName name )
        {
            _currentNode = _currentNode.getParentNode();
        }

        protected void emitTextAfter ( Splay s, int p, int cch )
        {
            assert cch > 0;

            Root r = getRoot();

            String text = null;

            if (_text != null)
            {
                if (_text.length() > 0)
                    text = text();
            }
            else if (cch > 0)
                text = r._text.fetch( s.getCpForPos( r, p ), cch );

            if (_currentNode != _doc)
                _currentNode.insertBefore( _doc.createTextNode( text ), null );
        }

        protected void emitDocType(
            String doctypeName, String publicID, String systemID )
        {
        }
                
        protected void emitComment ( Splay s )
        {
            Root r = getRoot();

            _currentNode.insertBefore(
                _doc.createComment(
                    r._text.fetch(
                        s.getCpForPos( r, 0 ), s.getCchValue() ) ), null );
        }

        protected void emitProcinst ( Splay s )
        {
            Root r = getRoot();

            _currentNode.insertBefore(
                _doc.createProcessingInstruction(
                    s.getLocal(),
                    r._text.fetch(
                        s.getCpForPos( r, 0 ), s.getCchValue() ) ), null );
        }

        protected void emitXmlnsFragment ( Splay s )
        {
            throw new IllegalStateException(
                "Cannot create a node for a namespace attribute" );
        }

        protected void emitAttrFragment ( Splay s )
        {
            throw new IllegalStateException(
                "Cannot create a node for a attribute" );
        }

        protected void emitTextFragment ( Splay s, int p, int cch )
        {
            // BUGBUG - there could be namespaces pushed for this text, but not
            // sure how to represent them here....  I should really put the
            // fragment logic in the base saver and have the base saver
            // synthesize well formed output...
            
            if (s != null)
            {
                Root r = getRoot();

                _currentNode.insertBefore(
                    _doc.createTextNode(
                    _text != null
                        ? _text.toString()
                        : s != null
                            ? r._text.fetch( s.getCpForPos( r, p ), cch )
                                : "" ),
                    null );
            }
        }

        protected void emitCommentFragment ( Splay s )
        {
            Root r = getRoot();

            _currentNode.insertBefore(
                _doc.createComment(
                    r._text.fetch(
                        s.getCpForPos( r, 0 ), s.getCchValue() ) ), null );
        }

        protected void emitProcinstFragment ( Splay s )
        {
            Root r = getRoot();

            _currentNode.insertBefore(
                _doc.createProcessingInstruction(
                    s.getLocal(),
                    r._text.fetch(
                        s.getCpForPos( r, 0 ), s.getCchValue() ) ), null );
        }

        Document _doc;
        Node     _currentNode;
        boolean  _createDoc;
    }

    //
    //
    //

    protected StringBuffer  _text;
    protected StringBuffer  _sb;
    protected boolean       _skipContainerFinish;
    protected LinkedHashMap _attrs;
    
    private HashSet _attrNames;

    private final boolean _inner;
    private final Root    _root;
    private final Splay   _top;
    private final long    _version;

    protected boolean _wantDupAttrs;
    protected boolean _wantFragTest;
    protected boolean _needsFrag;
    protected QName   _fragment;
    protected QName   _docElem;
    
    protected QName _synthElem;

    protected boolean _saveNamespacesFirst;
    protected boolean _useDefaultNamespace;

    private boolean _prettyPrint;
    private int     _prettyIndent;
    private int     _prettyOffset;

    private Splay   _splay;
    private int     _pos;
    private boolean _preProcess;
    private boolean _postProcess;
    private boolean _postPop;
    private Splay   _endSplay;

    private ArrayList _namespaceStack;
    private int       _currentMapping;
    private HashMap   _uriMap;
    private HashMap   _prefixMap;
    private boolean   _firstPush;
    private String    _initialDefaultUri;
    
    private HashMap _preComputedNamespaces;
    private String  _filterProcinst;
    private Map     _suggestedPrefixes;

    protected static String  _newLine;

    static
    {
        _newLine = System.getProperty( "line.separator" );
        if (_newLine == null)
            _newLine = "\n";
    }
}
