public XStreamDOM expandMacro(VariableResolver<String> vars) {
        String[] newAttributes = new String[attributes.length];
        for (int i=0; i<attributes.length; i+=2) {
            //noinspection PointlessArithmeticExpression
            newAttributes[i+0] = attributes[i]; // name
            newAttributes[i+1] = Util.replaceMacro(attributes[i+1],vars);
        }

        List<XStreamDOM> newChildren = null;
        if (children!=null) {
            newChildren = new ArrayList<>(children.size());
            for (XStreamDOM d : children)
                newChildren.add(d.expandMacro(vars));
        }

        return new XStreamDOM(tagName,newAttributes,newChildren,Util.replaceMacro(value,vars));
    }

    public String getAttribute(String name) {
        for (int i=0; i<attributes.length; i+=2)
            if (attributes[i].equals(name))
                return attributes[i+1];
        return null;
    }

    public int getAttributeCount() {
        return attributes.length / 2;
    }

    String getAttributeName(int index) {
        return attributes[index*2];
    }

    public String getAttribute(int index) {
        return attributes[index*2+1];
    }

    public String getValue() {
        return value;
    }

    public List<XStreamDOM> getChildren() {
        return children;
    }

    /**
     * Returns a new {@link HierarchicalStreamReader} that reads a sub-tree rooted at this node.
     */
    public HierarchicalStreamReader newReader() {
        return new ReaderImpl(this);
    }

    /**
     * Returns a new {@link HierarchicalStreamWriter} for marshalling objects into {@link XStreamDOM}.
     * After the writer receives the calls, call {@link WriterImpl#getOutput()} to obtain the populated tree.
     */
    public static WriterImpl newWriter() {
        return new WriterImpl();
    }

    /**
     * Writes this {@link XStreamDOM} into {@link OutputStream}.
     */
    public void writeTo(OutputStream os) {
        writeTo(XStream2.getDefaultDriver().createWriter(os));
    }

    public void writeTo(Writer w) {
        writeTo(XStream2.getDefaultDriver().createWriter(w));
    }

    public void writeTo(HierarchicalStreamWriter w) {
        new ConverterImpl().marshal(this,w,null);
    }

    /**
     * Marshals the given object with the given XStream into {@link XStreamDOM} and return it.
     */
    public static XStreamDOM from(XStream xs, Object obj) {
        WriterImpl w = newWriter();
        xs.marshal(obj, w);
        return w.getOutput();
    }

    public static XStreamDOM from(InputStream in) {
        return from(XStream2.getDefaultDriver().createReader(in));
    }

    public static XStreamDOM from(Reader in) {
        return from(XStream2.getDefaultDriver().createReader(in));
    }

    public static XStreamDOM from(HierarchicalStreamReader in) {
        return new ConverterImpl().unmarshalElement(in, null);
    }

    public Map<String, String> getAttributeMap() {
        Map<String,String> r = new HashMap<>();
        for (int i=0; i<attributes.length; i+=2)
            r.put(attributes[i],attributes[i+1]);
        return r;
    }

    private static class ReaderImpl extends AbstractXmlReader implements DocumentReader {
        private static class Pointer {
            final XStreamDOM node;
            int pos;

            private Pointer(XStreamDOM node) {
                this.node = node;
            }

            public String peekNextChild() {
                if (hasMoreChildren())
                    return node.children.get(pos).tagName;
                return null;
            }

            public boolean hasMoreChildren() {
                return node.children!=null && pos<node.children.size();
            }

            public String xpath() {
                XStreamDOM child = node.children.get(pos - 1);
                int count =0;
                for (int i=0; i<pos-1; i++)
                    if (node.children.get(i).tagName.equals(child.tagName))
                        count++;
                boolean more = false;
                for (int i=pos; !more && i<node.children.size(); i++)
                    if (node.children.get(i).tagName.equals(child.tagName))
                        more = true;

                if (count==0 && !more)  return child.tagName;   // sole child
                return child.tagName+'['+count+']';
            }
        }

        private final Stack<Pointer> pointers = new Stack<>();


        public ReaderImpl(XStreamDOM current) {
            super(new XmlFriendlyReplacer());
            pointers.push(new Pointer(current));
        }

        private Pointer current() {
            return pointers.peek();
        }

        public Object getCurrent() {
            return current().node;
        }

        public boolean hasMoreChildren() {
            return current().hasMoreChildren();
        }

        public HierarchicalStreamReader underlyingReader() {
            return this;
        }

        public void moveDown() {
            Pointer p = current();
            pointers.push(new Pointer(p.node.children.get(p.pos++)));
        }

        public void moveUp() {
            pointers.pop();
        }

        public Iterator getAttributeNames() {
            return new AttributeNameIterator(this);
        }

        public void appendErrors(ErrorWriter errorWriter) {
            StringBuilder buf = new StringBuilder();
            Pointer parent = null;
            for (Pointer cur : pointers) {
                if (parent!=null) {
                    buf.append('/').append(parent.xpath());
                } else {
                    buf.append(cur.node.tagName);
                }
                parent = cur;
            }
            errorWriter.add("xpath", buf.toString());
        }

        public void close() {
        }

        public String peekNextChild() {
            return current().peekNextChild();
        }

        public String getNodeName() {
            return unescapeXmlName(current().node.tagName);
        }

        public String getValue() {
            return Util.fixNull(current().node.value);
        }

        public String getAttribute(String name) {
            return current().node.getAttribute(name);
        }

        public String getAttribute(int index) {
            return current().node.getAttribute(index);
        }

        public int getAttributeCount() {
            return current().node.getAttributeCount();
        }

        public String getAttributeName(int index) {
            return unescapeXmlName(current().node.getAttributeName(index));
        }
    }

    public static class WriterImpl extends AbstractXmlWriter {
        private static class Pending {
            final String tagName;
            List<XStreamDOM> children;
            List<String> attributes = new ArrayList<String>();
            String value;

            private Pending(String tagName) {
                this.tagName = tagName;
            }

            void addChild(XStreamDOM dom) {
                if (children==null)
                    children = new ArrayList<XStreamDOM>();
                children.add(dom);
            }

            XStreamDOM toDOM() {
                return new XStreamDOM(tagName,attributes.toArray(new String[attributes.size()]),children,value);
            }
        }

        private final Stack<Pending> pendings = new Stack<Pending>();

        public WriterImpl() {
            pendings.push(new Pending(null));   // to get the final result
        }

        public void startNode(String name) {
            pendings.push(new Pending(escapeXmlName(name)));
        }



        public void endNode() {
            XStreamDOM dom = pendings.pop().toDOM();
            pendings.peek().addChild(dom);
        }

        public void addAttribute(String name, String value) {
            List<String> atts = pendings.peek().attributes;
            atts.add(escapeXmlName(name));
            atts.add(value);
        }

        public void setValue(String text) {
            pendings.peek().value = text;
        }

        public void flush() {
        }

        public void close() {
        }

        public HierarchicalStreamWriter underlyingWriter() {
            return this;
        }

        public XStreamDOM getOutput() {
            if (pendings.size()!=1)     throw new IllegalStateException();
            return pendings.peek().children.get(0);
        }
    }

    public static class ConverterImpl implements Converter {
        public boolean canConvert(Class type) {
            return type==XStreamDOM.class;
        }

        /**
         * {@link XStreamDOM} holds infoset (which is 'escaped' from XStream's PoV),
         * whereas {@link HierarchicalStreamWriter} expects unescaped names,
         * so we need to unescape it first before calling into {@link HierarchicalStreamWriter}.
         */
        // TODO: ideally we'd like to use the contextual HierarchicalStreamWriter to unescape,
        // but this object isn't exposed to us
        private String unescape(String s) {
            return REPLACER.unescapeName(s);
        }

        private String escape(String s) {
            return REPLACER.escapeName(s);
        }

        public void marshal(Object source, HierarchicalStreamWriter w, MarshallingContext context) {
            XStreamDOM dom = (XStreamDOM)source;
            w.startNode(unescape(dom.tagName));
            for (int i=0; i<dom.attributes.length; i+=2)
                w.addAttribute(unescape(dom.attributes[i]),dom.attributes[i+1]);
            if (dom.value!=null)
                w.setValue(dom.value);
            else {
                for (XStreamDOM c : Util.fixNull(dom.children)) {
                    marshal(c, w, context);
                }
            }
            w.endNode();
        }

        /**
         * Unmarshals a single child element.
         */
        public XStreamDOM unmarshal(HierarchicalStreamReader r, UnmarshallingContext context) {
            r.moveDown();
            XStreamDOM dom = unmarshalElement(r,context);
            r.moveUp();
            return dom;
        }

        public XStreamDOM unmarshalElement(HierarchicalStreamReader r, UnmarshallingContext context) {
            String name = escape(r.getNodeName());

            int c = r.getAttributeCount();
            String[] attributes = new String[c*2];
            for (int i=0; i<c; i++) {
                attributes[i*2]   = escape(r.getAttributeName(i));
                attributes[i*2+1] = r.getAttribute(i);
            }

            List<XStreamDOM> children = null;
            String value = null;
            if (r.hasMoreChildren()) {
                children = new ArrayList<XStreamDOM>();
                while (r.hasMoreChildren()) {
                    children.add(unmarshal(r, context));
                }
            } else {
                value = r.getValue();
            }

            return new XStreamDOM(name,attributes,children,value);
        }
    }

    public static XmlFriendlyReplacer REPLACER = new XmlFriendlyReplacer();
}
public XStreamDOM expandMacro ( VariableResolver < String > vars ) { String [ ] newAttributes = new String [ attributes . length ] ; for ( int i = 0 ; i < attributes . length ; i += 2 ) { newAttributes [ i + 0 ] = attributes [ i ] ; newAttributes [ i + 1 ] = Util . replaceMacro ( attributes [ i + 1 ] , vars ) ; } List < XStreamDOM > newChildren = null ; if ( children != null ) { newChildren = new ArrayList < > ( children . size ( ) ) ; for ( XStreamDOM d : children ) newChildren . add ( d . expandMacro ( vars ) ) ; } return new XStreamDOM ( tagName , newAttributes , newChildren , Util . replaceMacro ( value , vars ) ) ; } public String getAttribute ( String name ) { for ( int i = 0 ; i < attributes . length ; i += 2 ) if ( attributes [ i ] . equals ( name ) ) return attributes [ i + 1 ] ; return null ; } public int getAttributeCount ( ) { return attributes . length / 2 ; } String getAttributeName ( int index ) { return attributes [ index * 2 ] ; } public String getAttribute ( int index ) { return attributes [ index * 2 + 1 ] ; } public String getValue ( ) { return value ; } public List < XStreamDOM > getChildren ( ) { return children ; } public HierarchicalStreamReader newReader ( ) { return new ReaderImpl ( this ) ; } public static WriterImpl newWriter ( ) { return new WriterImpl ( ) ; } public void writeTo ( OutputStream os ) { writeTo ( XStream2 . getDefaultDriver ( ) . createWriter ( os ) ) ; } public void writeTo ( Writer w ) { writeTo ( XStream2 . getDefaultDriver ( ) . createWriter ( w ) ) ; } public void writeTo ( HierarchicalStreamWriter w ) { new ConverterImpl ( ) . marshal ( this , w , null ) ; } public static XStreamDOM from ( XStream xs , Object obj ) { WriterImpl w = newWriter ( ) ; xs . marshal ( obj , w ) ; return w . getOutput ( ) ; } public static XStreamDOM from ( InputStream in ) { return from ( XStream2 . getDefaultDriver ( ) . createReader ( in ) ) ; } public static XStreamDOM from ( Reader in ) { return from ( XStream2 . getDefaultDriver ( ) . createReader ( in ) ) ; } public static XStreamDOM from ( HierarchicalStreamReader in ) { return new ConverterImpl ( ) . unmarshalElement ( in , null ) ; } public Map < String , String > getAttributeMap ( ) { Map < String , String > r = new HashMap < > ( ) ; for ( int i = 0 ; i < attributes . length ; i += 2 ) r . put ( attributes [ i ] , attributes [ i + 1 ] ) ; return r ; } private static class ReaderImpl extends AbstractXmlReader implements DocumentReader { private static class Pointer { final XStreamDOM node ; int pos ; private Pointer ( XStreamDOM node ) { this . node = node ; } public String peekNextChild ( ) { if ( hasMoreChildren ( ) ) return node . children . get ( pos ) . tagName ; return null ; } public boolean hasMoreChildren ( ) { return node . children != null && pos < node . children . size ( ) ; } public String xpath ( ) { XStreamDOM child = node . children . get ( pos - 1 ) ; int count = 0 ; for ( int i = 0 ; i < pos - 1 ; i ++ ) if ( node . children . get ( i ) . tagName . equals ( child . tagName ) ) count ++ ; boolean more = false ; for ( int i = pos ; ! more && i < node . children . size ( ) ; i ++ ) if ( node . children . get ( i ) . tagName . equals ( child . tagName ) ) more = true ; if ( count == 0 && ! more ) return child . tagName ; return child . tagName + '[' + count + ']' ; } } private final Stack < Pointer > pointers = new Stack < > ( ) ; public ReaderImpl ( XStreamDOM current ) { super ( new XmlFriendlyReplacer ( ) ) ; pointers . push ( new Pointer ( current ) ) ; } private Pointer current ( ) { return pointers . peek ( ) ; } public Object getCurrent ( ) { return current ( ) . node ; } public boolean hasMoreChildren ( ) { return current ( ) . hasMoreChildren ( ) ; } public HierarchicalStreamReader underlyingReader ( ) { return this ; } public void moveDown ( ) { Pointer p = current ( ) ; pointers . push ( new Pointer ( p . node . children . get ( p . pos ++ ) ) ) ; } public void moveUp ( ) { pointers . pop ( ) ; } public Iterator getAttributeNames ( ) { return new AttributeNameIterator ( this ) ; } public void appendErrors ( ErrorWriter errorWriter ) { StringBuilder buf = new StringBuilder ( ) ; Pointer parent = null ; for ( Pointer cur : pointers ) { if ( parent != null ) { buf . append ( '/' ) . append ( parent . xpath ( ) ) ; } else { buf . append ( cur . node . tagName ) ; } parent = cur ; } errorWriter . add ( "xpath" , buf . toString ( ) ) ; } public void close ( ) { } public String peekNextChild ( ) { return current ( ) . peekNextChild ( ) ; } public String getNodeName ( ) { return unescapeXmlName ( current ( ) . node . tagName ) ; } public String getValue ( ) { return Util . fixNull ( current ( ) . node . value ) ; } public String getAttribute ( String name ) { return current ( ) . node . getAttribute ( name ) ; } public String getAttribute ( int index ) { return current ( ) . node . getAttribute ( index ) ; } public int getAttributeCount ( ) { return current ( ) . node . getAttributeCount ( ) ; } public String getAttributeName ( int index ) { return unescapeXmlName ( current ( ) . node . getAttributeName ( index ) ) ; } } public static class WriterImpl extends AbstractXmlWriter { private static class Pending { final String tagName ; List < XStreamDOM > children ; List < String > attributes = new ArrayList < String > ( ) ; String value ; private Pending ( String tagName ) { this . tagName = tagName ; } void addChild ( XStreamDOM dom ) { if ( children == null ) children = new ArrayList < XStreamDOM > ( ) ; children . add ( dom ) ; } XStreamDOM toDOM ( ) { return new XStreamDOM ( tagName , attributes . toArray ( new String [ attributes . size ( ) ] ) , children , value ) ; } } private final Stack < Pending > pendings = new Stack < Pending > ( ) ; public WriterImpl ( ) { pendings . push ( new Pending ( null ) ) ; } public void startNode ( String name ) { pendings . push ( new Pending ( escapeXmlName ( name ) ) ) ; } public void endNode ( ) { XStreamDOM dom = pendings . pop ( ) . toDOM ( ) ; pendings . peek ( ) . addChild ( dom ) ; } public void addAttribute ( String name , String value ) { List < String > atts = pendings . peek ( ) . attributes ; atts . add ( escapeXmlName ( name ) ) ; atts . add ( value ) ; } public void setValue ( String text ) { pendings . peek ( ) . value = text ; } public void flush ( ) { } public void close ( ) { } public HierarchicalStreamWriter underlyingWriter ( ) { return this ; } public XStreamDOM getOutput ( ) { if ( pendings . size ( ) != 1 ) throw new IllegalStateException ( ) ; return pendings . peek ( ) . children . get ( 0 ) ; } } public static class ConverterImpl implements Converter { public boolean canConvert ( Class type ) { return type == XStreamDOM . class ; } private String unescape ( String s ) { return REPLACER . unescapeName ( s ) ; } private String escape ( String s ) { return REPLACER . escapeName ( s ) ; } public void marshal ( Object source , HierarchicalStreamWriter w , MarshallingContext context ) { XStreamDOM dom = ( XStreamDOM ) source ; w . startNode ( unescape ( dom . tagName ) ) ; for ( int i = 0 ; i < dom . attributes . length ; i += 2 ) w . addAttribute ( unescape ( dom . attributes [ i ] ) , dom . attributes [ i + 1 ] ) ; if ( dom . value != null ) w . setValue ( dom . value ) ; else { for ( XStreamDOM c : Util . fixNull ( dom . children ) ) { marshal ( c , w , context ) ; } } w . endNode ( ) ; } public XStreamDOM unmarshal ( HierarchicalStreamReader r , UnmarshallingContext context ) { r . moveDown ( ) ; XStreamDOM dom = unmarshalElement ( r , context ) ; r . moveUp ( ) ; return dom ; } public XStreamDOM unmarshalElement ( HierarchicalStreamReader r , UnmarshallingContext context ) { String name = escape ( r . getNodeName ( ) ) ; int c = r . getAttributeCount ( ) ; String [ ] attributes = new String [ c * 2 ] ; for ( int i = 0 ; i < c ; i ++ ) { attributes [ i * 2 ] = escape ( r . getAttributeName ( i ) ) ; attributes [ i * 2 + 1 ] = r . getAttribute ( i ) ; } List < XStreamDOM > children = null ; String value = null ; if ( r . hasMoreChildren ( ) ) { children = new ArrayList < XStreamDOM > ( ) ; while ( r . hasMoreChildren ( ) ) { children . add ( unmarshal ( r , context ) ) ; } } else { value = r . getValue ( ) ; } return new XStreamDOM ( name , attributes , children , value ) ; } } public static XmlFriendlyReplacer REPLACER = new XmlFriendlyReplacer ( ) ; }
Recursively expands the variables in text and attribute values and return the new DOM.

The expansion uses {@link Util#replaceMacro(String, VariableResolver)}, so any unresolved
references will be left as-is.