/* * The MIT License * * Copyright (c) 2012, CloudBees, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package jenkins.util.xstream; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.converters.Converter; import com.thoughtworks.xstream.converters.ErrorWriter; import com.thoughtworks.xstream.converters.MarshallingContext; import com.thoughtworks.xstream.converters.UnmarshallingContext; import com.thoughtworks.xstream.io.AttributeNameIterator; import com.thoughtworks.xstream.io.HierarchicalStreamReader; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import com.thoughtworks.xstream.io.xml.AbstractXmlReader; import com.thoughtworks.xstream.io.xml.AbstractXmlWriter; import com.thoughtworks.xstream.io.xml.DocumentReader; import com.thoughtworks.xstream.io.xml.XmlFriendlyReplacer; import com.thoughtworks.xstream.io.xml.XppDriver; import hudson.Util; import hudson.util.VariableResolver; import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import java.io.Writer; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Stack; /** * XML DOM like structure to preserve a portion of XStream data as-is, so that you can * process it later in a separate XStream call. * * <p> * This object captures a subset of XML infoset that XStream understands. Namely, no XML namespace, * no mixed content. * * <p> * You use it as a field in your class (that itself participates in an XStream persistence), * and have it receive the portion of that XML. Then later you can use {@link #unmarshal(XStream)} * to convert this sub-tree to an object with a possibly separate XStream instance. * <p> * The reverse operation is {@link #from(XStream, Object)} method, which marshals an object * into {@link XStreamDOM}. * * <p> * You can also use this class to parse an entire XML document into a DOM like tree with * {@link #from(HierarchicalStreamReader)} and {@link #writeTo(HierarchicalStreamWriter)}. * These two methods support variants that accept other forms. * <p> * Whereas the above methods read from and write to {@link HierarchicalStreamReader} and, * {@link HierarchicalStreamWriter}, we can also create {@link HierarchicalStreamReader} * that read from DOM and {@link HierarchicalStreamWriter} that writes to DOM. See * {@link #newReader()} and {@link #newWriter()} for those operations. * * <h3>XStreamDOM as a field of another XStream-enabled class</h3> * <p> * {@link XStreamDOM} can be used as a type of a field of another class that's itself XStream-enabled, * such as this: * * <pre> * class Foo { * XStreamDOM bar; * } * </pre> * * With the following XML: * * <pre> * <foo> * <bar> * <payload> * ... * </payload> * </bar> * </foo> * </pre> * * <p> * The {@link XStreamDOM} object in the bar field will have the "payload" element in its tag name * (which means the bar element cannot have multiple children.) * * <h3>XStream and name escaping</h3> * <p> * Because XStream wants to use letters like '$' that's not legal as a name char in XML, * the XML data model that it thinks of (unescaped) is actually translated into the actual * XML-compliant infoset via {@link XmlFriendlyReplacer}. This translation is done by * {@link HierarchicalStreamReader} and {@link HierarchicalStreamWriter}, transparently * from {@link Converter}s. In {@link XStreamDOM}, we'd like to hold the XML infoset * (escaped form, in XStream speak), so in our {@link ConverterImpl} we go out of the way * to cancel out this effect. * * @author Kohsuke Kawaguchi * @since 1.473 */ public class XStreamDOM { private final String tagName; private final String[] attributes; // one of them is non-null, the other is null private final String value; private final List<XStreamDOM> children; public XStreamDOM(String tagName, Map<String, String> attributes, String value) { this.tagName = tagName; this.attributes = toAttributeList(attributes); this.value = value; this.children = null; } public XStreamDOM(String tagName, Map<String, String> attributes, List<XStreamDOM> children) { this.tagName = tagName; this.attributes = toAttributeList(attributes); this.value = null; this.children = children; } private XStreamDOM(String tagName, String[] attributes, List<XStreamDOM> children, String value) { this.tagName = tagName; this.attributes = attributes; this.children = children; this.value = value; } private String[] toAttributeList(Map<String, String> attributes) { String[] r = new String[attributes.size()*2]; int i=0; for (Entry<String, String> e : attributes.entrySet()) { r[i++] = e.getKey(); r[i++] = e.getValue(); } return r; } public String getTagName() { return tagName; } /** * Unmarshals this DOM into an object via the given XStream. */ public <T> T unmarshal(XStream xs) { return (T)xs.unmarshal(newReader()); } public <T> T unmarshal(XStream xs, T root) { return (T)xs.unmarshal(newReader(),root); } /** * 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. */ 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]; // name newAttributes[i+1] = Util.replaceMacro(attributes[i+1],vars); } List<XStreamDOM> newChildren = null; if (children!=null) { newChildren = new ArrayList<XStreamDOM>(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(new XppDriver().createWriter(os)); } public void writeTo(Writer w) { writeTo(new XppDriver().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(new XppDriver().createReader(in)); } public static XStreamDOM from(Reader in) { return from(new XppDriver().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<String, String>(); 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<Pointer>(); 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(); }