Java tutorial
/* * ***** BEGIN LICENSE BLOCK ***** * Zimbra Collaboration Suite Server * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software Foundation, * version 2 of the License. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * You should have received a copy of the GNU General Public License along with this program. * If not, see <https://www.gnu.org/licenses/>. * ***** END LICENSE BLOCK ***** */ package com.zimbra.common.soap; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; import org.dom4j.QName; import com.google.common.base.Charsets; import com.google.common.collect.Lists; import com.google.common.io.Files; import com.zimbra.common.service.ServiceException; import com.zimbra.common.util.StringUtil; import com.zimbra.common.util.ZimbraLog; /** * @since Mar 16, 2005 */ public abstract class Element implements Cloneable { protected String mName; protected String mPrefix = ""; protected Element mParent; protected Map<String, Object> mAttributes; protected Map<String, String> mNamespaces; /** These values are used to control how the <tt>Element</tt> serializes * an attribute. In our model, {@link Element#getAttribute(String)} will * find the attribute <tt>b</tt> with value <tt>"something"</tt> when * presented with either of the following two pieces of XML:<ul> * <li><a><b>something</b></a> * <li><a b="something"/></ul><p> * By setting the dispositon of an attribute to {@link #CONTENT}, you can * force the attribute to be rendered in the former form.<p> * * <i>Note that JSON serialization ignores all such hints and serializes * all attributes as <tt>"key": "value"</tt>.</i> */ public static enum Disposition { ATTRIBUTE, CONTENT } /** Creates a new <tt>Element</tt> with the given name. This method should * be used when generating a standalone <tt>Element</tt>. If you want to * add a child to an existing <tt>Element</tt>, please use * {@link #addElement(String)} instead. */ public static Element create(SoapProtocol proto, String name) throws ServiceException { if (proto == SoapProtocol.SoapJS) { return new JSONElement(name); } else if (proto == SoapProtocol.Soap11 || proto == SoapProtocol.Soap12) { return new XMLElement(name); } throw ServiceException.INVALID_REQUEST("Unknown SoapProtocol: " + proto, null); } /** Creates a new <tt>Element</tt> with the given {@link QName}. This * method should be used when generating a standalone <tt>Element</tt>. * If you want to add a child to an existing <tt>Element</tt>, please * use {@link #addElement(QName) instead. */ public static Element create(SoapProtocol proto, QName qname) throws ServiceException { if (proto == SoapProtocol.SoapJS) { return new JSONElement(qname); } else if (proto == SoapProtocol.Soap11 || proto == SoapProtocol.Soap12) { return new XMLElement(qname); } throw ServiceException.INVALID_REQUEST("Unknown SoapProtocol: " + proto, null); } /** Returns the appropriate {@link ElementFactory} for generating * <tt>Element</tt>s of this <tt>Element</tt>'s type. */ public abstract ElementFactory getFactory(); /** * Cleanup any resources supporting this element. For instance, * {@link FileBackedElement} removes its backing file. */ public abstract void destroy(); /** Creates a new child {@link Element} with the given name and adds it to this {@link Element}.<br /> * This is a legacy equivalent to {@link addNonUniqueElement}. It has been deprecated to discourage * use when {@link addUniqueElement} would be more appropriate. * @Deprecated - replaced by either {@link addNonUniqueElement} or {@link addUniqueElement} */ @Deprecated public Element addElement(String name) throws ContainerException { return addNonUniqueElement(name); } /** Creates a new child {@link Element} with the given QName and adds it to this {@link Element}.<br /> * This is a legacy equivalent to {@link addNonUniqueElement}. It has been deprecated to discourage * use when {@link addUniqueElement} would be more appropriate. * @Deprecated - replaced by either {@link addNonUniqueElement} or {@link addUniqueElement} */ @Deprecated public Element addElement(QName qname) throws ContainerException { return addNonUniqueElement(qname); } /** Adds an existing {@code Element} to this {@code Element}.<br /> * This is a legacy equivalent to {@link addNonUniqueElement}. It has been deprecated to discourage * use when {@link addUniqueElement} would be more appropriate. * @Deprecated - replaced by either {@link addNonUniqueElement} or {@link addUniqueElement} */ @Deprecated public Element addElement(Element elt) throws ContainerException { return addNonUniqueElement(elt); } /** Creates a new child {@link Element} with the given name and adds it to this {@link Element}.<br /> */ public abstract Element addNonUniqueElement(String name) throws ContainerException; /** Creates a new child {@link Element} with the given QName and adds it to this {@link Element}.<br /> */ public abstract Element addNonUniqueElement(QName qname) throws ContainerException; /** Adds an existing {@code Element} to this {@code Element}.<br /> */ public abstract Element addNonUniqueElement(Element elt) throws ContainerException; public Element addUniqueElement(String name) throws ContainerException { return addNonUniqueElement(name); } public Element addUniqueElement(QName qname) throws ContainerException { return addNonUniqueElement(qname); } public Element addUniqueElement(Element elt) throws ContainerException { return addNonUniqueElement(elt); } /** * The approach to namespaces is to ALWAYS store them on elements that use them (for either the element's name * or in one of its attributes names) but ignore them where they are not used. This means that unused namespace * definitions may be dropped - but that shouldn't matter. * It also means that namespaces won't be dropped by mistake from detached elements because the namespace is only * stored in a parent element where it was defined. */ protected Element setNamespace(String prefix, String uri) { if (prefix != null && uri != null && !uri.equals("")) { if (mNamespaces == null) { mNamespaces = new HashMap<String, String>(); } mNamespaces.put(prefix, uri); } return this; } public abstract Element setText(String content) throws ContainerException; public Element addText(String content) throws ContainerException { return setText(getText() + content); } public Element addAttribute(String key, String value) throws ContainerException { return addAttribute(key, value, Disposition.ATTRIBUTE); } public Element addAttribute(String key, long value) throws ContainerException { return addAttribute(key, value, Disposition.ATTRIBUTE); } public Element addAttribute(String key, double value) throws ContainerException { return addAttribute(key, value, Disposition.ATTRIBUTE); } public Element addAttribute(String key, boolean value) throws ContainerException { return addAttribute(key, value, Disposition.ATTRIBUTE); } public Element addAttribute(String key, Number value) throws ContainerException { return addAttribute(key, value, Disposition.ATTRIBUTE); } public Element addAttribute(String key, Boolean value) throws ContainerException { return addAttribute(key, value, Disposition.ATTRIBUTE); } public abstract Element addAttribute(String key, String value, Disposition disp) throws ContainerException; public Element addAttribute(String key, long value, Disposition disp) throws ContainerException { return addAttribute(key, Long.toString(value), disp); } public Element addAttribute(String key, double value, Disposition disp) throws ContainerException { return addAttribute(key, Double.toString(value), disp); } public Element addAttribute(String key, boolean value, Disposition disp) throws ContainerException { return addAttribute(key, value ? "1" : "0", disp); } public Element addAttribute(String key, Number value, Disposition disp) throws ContainerException { return addAttribute(key, value != null ? value.toString() : null, disp); } public Element addAttribute(String key, Boolean value, Disposition disp) throws ContainerException { if (value != null) { return addAttribute(key, value.booleanValue(), disp); } return addAttribute(key, (String) null, disp); } public KeyValuePair addKeyValuePair(String key, String value) throws ContainerException { return addKeyValuePair(key, value, null, null); } public abstract KeyValuePair addKeyValuePair(String key, String value, String eltname, String attrname) throws ContainerException; protected void detach(Element child) throws ContainerException { if (child == null) return; if (child.mParent != this) { throw new ContainerException("wrong parent"); } child.mParent = null; } public Element detach() throws ContainerException { setNamespace(mPrefix, getNamespaceURI(mPrefix)); if (mParent != null) { mParent.detach(this); } return this; } @Override public abstract Element clone(); // reading from the element hierarchy public String getName() { return mName; } public String getQualifiedName() { return mPrefix != null && !mPrefix.equals("") ? mPrefix + ':' + mName : mName; } public QName getQName() { String uri = getNamespaceURI(mPrefix); return uri == null ? QName.get(mName) : QName.get(getQualifiedName(), uri); } public static QName getQName(String qualifiedName) { String[] parts = qualifiedName.split("\\."); return new QName(parts[parts.length - 1]); } public Element getParent() { return mParent; } /** Returns the first child <tt>Element</tt> with the given name. * @throws ServiceException if no matching <tt>Element</tt> is found */ public Element getElement(String name) throws ServiceException { return checkNull(name, getOptionalElement(name)); } /** Returns the first child <tt>Element</tt> with the given QName. * @throws ServiceException if no matching <tt>Element</tt> is found */ public Element getElement(QName qname) throws ServiceException { return checkNull(qname.getName(), getOptionalElement(qname)); } /** Returns the first child <tt>Element</tt> with the given name, or * <tt>null</tt> if no matching <tt>Element</tt> is found. */ public abstract Element getOptionalElement(String name); /** Returns the first child <tt>Element</tt> with the given QName, or * <tt>null</tt> if no matching <tt>Element</tt> is found. */ public Element getOptionalElement(QName qname) { return getOptionalElement(qname.getName()); } /** Returns all an <tt>Element</tt>'s attributes. */ public abstract Set<Attribute> listAttributes(); /** Returns all an <tt>Element</tt>'s sub-elements, or an empty <tt>List</tt>. */ public List<Element> listElements() { return listElements(null); } /** Returns all the sub-elements with the given name. If <tt>name></tt> * is <tt>null</tt>, returns <u>all</u> sub-elements. If no elements * with the given name exist, returns an empty <tt>List</tt> */ public abstract List<Element> listElements(String name); /** Returns whether the element has any sub-elements. */ public abstract boolean hasChildren(); public List<KeyValuePair> listKeyValuePairs() { return listKeyValuePairs(null, null); } public abstract List<KeyValuePair> listKeyValuePairs(String eltname, String attrname); /** Returns all attributes as an <tt>Iterator</tt>. */ public Iterator<Attribute> attributeIterator() { return listAttributes().iterator(); } /** Returns all sub-elements as an <tt>Iterator</tt>. */ public Iterator<Element> elementIterator() { return listElements().iterator(); } /** Returns all sub-elements with the given name as an <tt>Iterator</tt>. * If <tt>name></tt> is <tt>null</tt>, returns <u>all</u> sub-elements. */ public Iterator<Element> elementIterator(String name) { return listElements(name).iterator(); } public abstract String getText(); abstract String getRawText(); public String getTextTrim() { return getText().trim().replaceAll("\\s+", " "); } public String getAttribute(String key) throws ServiceException { return checkNull(key, getAttribute(key, null)); } public int getAttributeInt(String key) throws ServiceException { return parseInt(key, checkNull(key, getAttribute(key, null))); } public long getAttributeLong(String key) throws ServiceException { return parseLong(key, checkNull(key, getAttribute(key, null))); } public double getAttributeDouble(String key) throws ServiceException { return parseDouble(key, checkNull(key, getAttribute(key, null))); } public boolean getAttributeBool(String key) throws ServiceException { return parseBool(key, checkNull(key, getAttribute(key, null))); } public abstract String getAttribute(String key, String defaultValue); public long getAttributeLong(String key, long defaultValue) throws ServiceException { String raw = getAttribute(key, null); return raw == null ? defaultValue : parseLong(key, raw); } public int getAttributeInt(String key, int defaultValue) throws ServiceException { String raw = getAttribute(key, null); return raw == null ? defaultValue : parseInt(key, raw); } public double getAttributeDouble(String key, double defaultValue) throws ServiceException { String raw = getAttribute(key, null); return raw == null ? defaultValue : parseDouble(key, raw); } public boolean getAttributeBool(String key, boolean defaultValue) throws ServiceException { String raw = getAttribute(key, null); return raw == null ? defaultValue : parseBool(key, raw); } protected String getNamespaceURI(String prefix) { if (mNamespaces != null) { Object uri = mNamespaces.get(prefix); if (uri != null && !((String) uri).trim().equals("")) { return (String) uri; } } return mParent == null ? null : mParent.getNamespaceURI(prefix); } protected Element collapseNamespace() { if (mNamespaces != null && mParent != null && mParent.mPrefix.equals(mPrefix)) { String localURI = mNamespaces.get(mPrefix); if (localURI != null && localURI.equals(mParent.getNamespaceURI(mPrefix))) { mNamespaces.remove(mPrefix); if (mNamespaces.isEmpty()) { mNamespaces = null; } } } return this; } public static String checkNull(String key, String value) throws ServiceException { if (value == null) { throw ServiceException.INVALID_REQUEST("missing required attribute: " + key, null); } return value; } private Element checkNull(String key, Element value) throws ServiceException { if (value == null) { throw ServiceException.INVALID_REQUEST("missing required element: " + key, null); } return value; } public static long parseLong(String key, String value) throws ServiceException { try { return Long.parseLong(value); } catch (NumberFormatException nfe) { throw ServiceException.INVALID_REQUEST("invalid long value '" + value + "' for attribute: " + key, nfe); } } public static int parseInt(String key, String value) throws ServiceException { try { return Integer.parseInt(value); } catch (NumberFormatException nfe) { throw ServiceException.INVALID_REQUEST("invalid int value '" + value + "' for attribute: " + key, nfe); } } public static short parseShort(String key, String value) throws ServiceException { try { return Short.parseShort(value); } catch (NumberFormatException nfe) { throw ServiceException.INVALID_REQUEST("invalid short value '" + value + "' for attribute: " + key, nfe); } } public static double parseDouble(String key, String value) throws ServiceException { try { return Double.parseDouble(value); } catch (NumberFormatException nfe) { throw ServiceException.INVALID_REQUEST("invalid double value '" + value + "' for attribute: " + key, nfe); } } public static boolean parseBool(String key, String value) throws ServiceException { if (value.equals("1") || value.equalsIgnoreCase("true")) { return true; } else if (value.equals("0") || value.equalsIgnoreCase("false")) { return false; } throw ServiceException.INVALID_REQUEST("invalid boolean value '" + value + "' for attribute: " + key, null); } protected boolean namespaceDeclarationNeeded(String prefix, String uri) { if (mParent == null || getClass() != mParent.getClass()) { return true; } String thatURI = mParent.getNamespaceURI(prefix); return thatURI == null || !thatURI.equals(uri); } // dumping the element hierarchy public byte[] toUTF8() { try { return toString().getBytes("utf-8"); } catch (UnsupportedEncodingException e) { // should *never* happen since UTF-8 and UTF-16 cover the exact same character space return null; } } public void output(Appendable out) throws IOException { marshal(out); } public abstract String prettyPrint(); public abstract String prettyPrint(boolean safe); /** Serialize this <tt>Element</tt> to an <code>Appendable</code>. */ public abstract void marshal(Appendable out) throws IOException; private static final String FORTY_SPACES = " "; protected void indent(Appendable sb, int indent, boolean newline) throws IOException { if (indent < 0) return; if (newline) { sb.append('\n'); } while (indent > 40) { sb.append(FORTY_SPACES); indent -= 40; } if (indent > 0) { sb.append(FORTY_SPACES.substring(40 - indent)); } } public org.dom4j.Element toXML() { org.dom4j.Document doc = new org.dom4j.tree.DefaultDocument(); doc.setRootElement(toXML(null)); return doc.getRootElement(); } private org.dom4j.Element toXML(org.dom4j.Element d4parent) { org.dom4j.Element d4elt = (d4parent == null ? org.dom4j.DocumentHelper.createElement(getQName()) : d4parent.addElement(getQName())); for (Attribute attr : listAttributes()) { d4elt.addAttribute(attr.getKey(), attr.getValue()); } for (Element elt : listElements()) { elt.toXML(d4elt); } d4elt.setText(getText()); return d4elt; } /** * Intended to produce XML suitable for use with JAXB objects. * @return */ public org.w3c.dom.Document toW3cDom() { javax.xml.parsers.DocumentBuilder w3DomBuilder = W3cDomUtil.getBuilder(); w3DomBuilder.reset(); org.w3c.dom.Document doc = w3DomBuilder.newDocument(); doc.appendChild(toW3cDom(doc, null)); return doc; } private org.w3c.dom.Node toW3cDom(org.w3c.dom.Document doc, org.w3c.dom.Element parent) { String uri = getNamespaceURI(mPrefix); if ((uri != null) && uri.equals("urn:zimbraSoap")) { uri = null; } org.w3c.dom.Element elem; elem = doc.createElementNS(uri, getQualifiedName()); elem.setTextContent(getText()); if (parent != null) { parent.appendChild(elem); } for (Attribute attr : listAttributes()) { elem.setAttribute(attr.getKey(), attr.getValue()); } for (Element elt : listElements()) { elt.toW3cDom(doc, elem); } return elem; } /** Return the attribute value that is at the specified path, or null if * one could not be found. * @param xpath an array of names to traverse in the element tree */ public String getPathAttribute(String[] xpath) { int depth = 0; Element cur = this; while (depth < xpath.length - 1 && cur != null) { cur = cur.getOptionalElement(xpath[depth++]); } return cur == null ? null : cur.getAttribute(xpath[depth], null); } /** Return the first Element matching the specified path, or null if none was found. * @param xpath an array of names to traverse in the element tree */ public Element getPathElement(String[] xpath) { int depth = 0; Element cur = this; while (depth < xpath.length && cur != null) { cur = cur.getOptionalElement(xpath[depth++]); } return cur; } /** Return the list of {@code Element}s matching the specified path, or an empty {@code List} * if none were found. * @param xpath an array of names to traverse in the element tree */ public List<Element> getPathElementList(String[] xpath) { int depth = 0; Element cur = this; while (depth < xpath.length - 1 && cur != null) { cur = cur.getOptionalElement(xpath[depth++]); } if (cur == null) { return Collections.emptyList(); } return cur.listElements(xpath[xpath.length - 1]); } /** Set the attribute at the specified path, throwing an exception if the * parent Element does not exist. * @param xpath an array of names to traverse in the element tree */ public void setPathAttribute(String[] xpath, String value) throws ServiceException { if (xpath == null || xpath.length == 0) return; int depth = 0; Element cur = this; while (depth < xpath.length - 1 && cur != null) { cur = cur.getOptionalElement(xpath[depth++]); } if (cur == null) { throw ServiceException.INVALID_REQUEST("could not find path", null); } cur.addAttribute(xpath[depth], value); } /** * Parses a JSON element structure and closes the stream. */ public static Element parseJSON(InputStream is) throws SoapParseException { return parseJSON(is, JSONElement.mFactory); } /** * Parses a JSON element structure and closes the stream. */ public static Element parseJSON(InputStream is, ElementFactory factory) throws SoapParseException { try { return parseJSON(new String(com.zimbra.common.util.ByteUtil.getContent(is, -1), "utf-8"), factory); } catch (SoapParseException e) { throw e; } catch (Exception e) { throw new SoapParseException("could not transcode request from utf-8", null); } } public static Element parseJSON(String js) throws SoapParseException { return parseJSON(js, JSONElement.mFactory); } public static Element parseJSON(String js, ElementFactory factory) throws SoapParseException { return parseJSON(js, SoapProtocol.SoapJS.getEnvelopeQName(), factory); } public static Element parseJSON(String js, QName qn, ElementFactory factory) throws SoapParseException { try { return JSONElement.parseElement(new JSONElement.JSRequest(js), qn, factory); } catch (ContainerException ce) { SoapParseException spe = new SoapParseException(ce.getMessage(), js); spe.initCause(ce); throw spe; } } public static final String XHTML_NS_URI = "http://www.w3.org/1999/xhtml"; public static Element parseXML(InputStream is) throws XmlParseException { return W3cDomUtil.parseXML(is, XMLElement.mFactory); } public static Element parseXML(String xml) throws XmlParseException { return W3cDomUtil.parseXML(xml, XMLElement.mFactory); } public static Element convertDOM(org.dom4j.Element d4root) { return convertDOM(d4root, XMLElement.mFactory); } public static Element convertDOM(org.dom4j.Element d4root, ElementFactory factory) { Element elt = factory.createElement(d4root.getQName()); for (Iterator<?> it = d4root.attributeIterator(); it.hasNext();) { org.dom4j.Attribute d4attr = (org.dom4j.Attribute) it.next(); elt.addAttribute(d4attr.getQualifiedName(), d4attr.getValue()); } for (Iterator<?> it = d4root.elementIterator(); it.hasNext();) { org.dom4j.Element d4elt = (org.dom4j.Element) it.next(); if (XHTML_NS_URI.equalsIgnoreCase(d4elt.getNamespaceURI()) && !d4elt.elements().isEmpty()) { // need to treat XHTML as text return flattenDOM(d4root, factory); } else { elt.addNonUniqueElement(convertDOM(d4elt, factory)); } } String content = d4root.getText(); if (content != null && !content.trim().equals("")) { try { elt.setText(content); } catch (ContainerException ce) { // can't hold both children and text on a single node, so flatten contents to text return flattenDOM(d4root, factory); } } return elt; } private static Element flattenDOM(org.dom4j.Element d4root, ElementFactory factory) { Element elt = factory.createElement(d4root.getQName()); for (Iterator<?> it = d4root.attributeIterator(); it.hasNext();) { org.dom4j.Attribute d4attr = (org.dom4j.Attribute) it.next(); elt.addAttribute(d4attr.getQualifiedName(), d4attr.getValue()); } StringBuilder content = new StringBuilder(); for (int i = 0, size = d4root.nodeCount(); i < size; i++) { org.dom4j.Node node = d4root.node(i); switch (node.getNodeType()) { case org.dom4j.Node.TEXT_NODE: content.append(node.getText()); break; case org.dom4j.Node.ELEMENT_NODE: content.append(((org.dom4j.Element) node).asXML()); break; } } return elt.setText(content.toString()); } /** * Re-order the child elements under {@code top} into the order specified in {@code order}. * Any child elements which have names not found in {@code order} will be moved after those that * are in {@code order}. * <p>Useful for ensuring XML conforms to WSDL where it is derived from JAXB that enforces propOrder. Certain * constructs in the legacy Zimbra SOAP API result in JAXB that won't create valid WSDL unless propOrder is * enforced in the JAXB. * <p>This typically happens when we have a list of possible elements with different names that are NOT wrapped * under a parent element.</p> * <p>Using JAXB to construct the XML would avoid this issue but in some instances would be too disruptive</p> * @return {@code top} with the child elements re-ordered */ public static Element reorderChildElements(Element top, List<List<QName>> order) { if (top == null) { return top; } List<Element> ordered = Lists.newArrayList(); for (List<QName> childNames : order) { List<Element> orphans = null; // detach child elements from top AFTER processing the list of them for (Element child : top.listElements()) { if (childNames.contains(child.getQName())) { if (orphans == null) { orphans = Lists.newArrayList(); } orphans.add(child); ordered.add(child); } } if (orphans != null) { for (Element orphan : orphans) { orphan.detach(); } } } /* add any elements with names which are not in order */ for (Element child : top.listElements()) { child.detach(); ordered.add(child); } for (Element child : ordered) { top.addNonUniqueElement(child); } return top; } public static class ContainerException extends RuntimeException { private static final long serialVersionUID = -5884422477180821199L; public ContainerException(String message) { super(message); } } public static interface ElementFactory { public Element createElement(String name); public Element createElement(QName qname); } public static interface KeyValuePair { public KeyValuePair setValue(String value) throws ContainerException; public KeyValuePair addAttribute(String key, String value) throws ContainerException; public KeyValuePair addAttribute(String key, long value) throws ContainerException; public KeyValuePair addAttribute(String key, double value) throws ContainerException; public KeyValuePair addAttribute(String key, boolean value) throws ContainerException; public String getKey() throws ContainerException; public String getValue() throws ContainerException; } public static class Attribute { private final String mKey; private Object mValue; private final Element mParent; Attribute(Map.Entry<String, Object> entry, Element parent) { mKey = entry.getKey(); mValue = entry.getValue(); mParent = parent; } public String getKey() { return mKey; } public String getValue() { return mValue.toString(); } public void setValue(String value) { mParent.addAttribute(mKey, value); mValue = value; } } public static class JSONElement extends Element { public static final ElementFactory mFactory = new JSONFactory(); public static final String E_ATTRS = "_attrs"; public static final String A_CONTENT = "_content"; public static final String A_NAMESPACE = "_jsns"; public JSONElement(String name) { mName = name; mAttributes = new LinkedHashMap<String, Object>(); } @Override public void destroy() { // Assumption - Only FileBackedElement children of XMLElement // need special action from destroy(), so, we're done here. } public JSONElement(QName qname) { this(qname.getName()); setNamespace("", qname.getNamespaceURI()); } private static final class JSONFactory implements ElementFactory { @Override public Element createElement(String name) { return new JSONElement(name); } @Override public Element createElement(QName qname) { return new JSONElement(qname); } } private static final class JSONKeyValuePair implements KeyValuePair, Cloneable { private final JSONElement mTarget; JSONKeyValuePair(String key, String value) { (mTarget = new JSONElement(key)).setText(value); } @Override public KeyValuePair setValue(String value) throws ContainerException { mTarget.setText(value); return this; } @Override public KeyValuePair addAttribute(String key, String value) throws ContainerException { mTarget.addAttribute(key, value); return this; } @Override public KeyValuePair addAttribute(String key, long value) throws ContainerException { mTarget.addAttribute(key, value); return this; } @Override public KeyValuePair addAttribute(String key, double value) throws ContainerException { mTarget.addAttribute(key, value); return this; } @Override public KeyValuePair addAttribute(String key, boolean value) throws ContainerException { mTarget.addAttribute(key, value); return this; } @Override public String getKey() throws ContainerException { return mTarget.getName(); } @Override public String getValue() throws ContainerException { return mTarget.getRawText(); } @Override public JSONKeyValuePair clone() { JSONKeyValuePair clone = new JSONKeyValuePair(getKey(), getValue()); clone.mTarget.mAttributes.putAll(mTarget.mAttributes); return clone; } @Override public String toString() { if (mTarget.mAttributes.isEmpty()) return "null"; else if (mTarget.mAttributes.size() == 1 && mTarget.mAttributes.containsKey(A_CONTENT)) return '"' + StringUtil.jsEncode(mTarget.mAttributes.get(A_CONTENT)) + '"'; else return mTarget.toString(); } /** * Converts to an equivalent Element. Supplying parent information ensures namespaces are handled * correctly - with a null parent, toW3cDom would render the pair as * {@code <a n="key1" xmlns="">value</a>} instead of {@code <a n="key1">value1</a>} */ Element asElement(Element parent) { Element elt = new JSONElement(XMLElement.E_ATTRIBUTE).addAttribute(XMLElement.A_ATTR_NAME, mTarget.mName); elt.mAttributes.putAll(mTarget.mAttributes); elt.mParent = parent; return elt; } } @Override public ElementFactory getFactory() { return mFactory; } @Override public Element addNonUniqueElement(String name) throws ContainerException { return addNonUniqueElement(new JSONElement(name)); } @Override public Element addNonUniqueElement(QName qname) throws ContainerException { return addNonUniqueElement(new JSONElement(qname)); } @Override public Element addNonUniqueElement(Element elt) throws ContainerException { if (elt == null || elt.mParent == this) { return elt; } else if (elt.mParent != null) { throw new ContainerException("element already has a parent"); } assert (elt instanceof JSONElement); String name = elt.getName(); Object obj = mAttributes.get(name); if (obj instanceof Element) { throw new ContainerException("already stored element as unique: " + name); } else if (obj != null && !(obj instanceof List)) { throw new ContainerException("already stored attribute with name: " + name); } @SuppressWarnings("unchecked") List<Element> content = (List<Element>) obj; if (content == null) { mAttributes.put(name, content = new ArrayList<Element>()); } content.add(elt); elt.mParent = this; return elt.collapseNamespace(); } @Override public Element addUniqueElement(String name) throws ContainerException { return addUniqueElement(new JSONElement(name)); } @Override public Element addUniqueElement(QName qname) throws ContainerException { return addUniqueElement(new JSONElement(qname)); } @Override public Element addUniqueElement(Element elt) throws ContainerException { if (elt == null) return null; String name = elt.getName(); Object obj = mAttributes.get(name); if (obj instanceof List<?>) { throw new ContainerException("already stored non-unique element(s) with name: " + name); } else if (obj instanceof String || obj instanceof Number || obj instanceof Boolean) { throw new ContainerException("already stored attribute with name: " + name); } else if (obj instanceof Element) { if (elt.mAttributes.isEmpty()) return (Element) obj; else if (!((Element) obj).mAttributes.isEmpty()) throw new ContainerException("already stored unique element with name: " + name); } mAttributes.put(name, elt); elt.mParent = this; return elt; } @Override public Element setText(String content) throws ContainerException { return addAttribute(A_CONTENT, content); } @Override public Element addAttribute(String key, String value, Disposition disp) throws ContainerException { checkNamingConflict(key); if (value == null) mAttributes.remove(key); else mAttributes.put(key, value); return this; } @Override public Element addAttribute(String key, long value, Disposition disp) throws ContainerException { checkNamingConflict(key); mAttributes.put(key, Long.valueOf(value)); return this; } @Override public Element addAttribute(String key, double value, Disposition disp) throws ContainerException { checkNamingConflict(key); mAttributes.put(key, new Double(value)); return this; } @Override public Element addAttribute(String key, boolean value, Disposition disp) throws ContainerException { checkNamingConflict(key); mAttributes.put(key, new Boolean(value)); return this; } private void checkNamingConflict(String key) throws ContainerException { Object obj = mAttributes.get(key); if (obj instanceof Element || obj instanceof List<?>) throw new ContainerException("already stored element with name: " + key); } @SuppressWarnings("unchecked") @Override public KeyValuePair addKeyValuePair(String key, String value, String eltname, String attrname) { JSONElement attrs = (JSONElement) addUniqueElement(E_ATTRS); Object existing = attrs.mAttributes.get(key); KeyValuePair kvp = new JSONKeyValuePair(key, value); if (existing == null) { attrs.mAttributes.put(key, kvp); } else if (existing instanceof KeyValuePair) { List<KeyValuePair> pairs = new ArrayList<KeyValuePair>(3); pairs.add((KeyValuePair) existing); pairs.add(kvp); attrs.mAttributes.put(key, pairs); } else { ((List<KeyValuePair>) existing).add(kvp); } return kvp; } @Override protected void detach(Element elt) throws ContainerException { if (elt == null) return; super.detach(elt); Object obj = mAttributes.get(elt.getName()); if (obj == elt) { mAttributes.remove(elt.getName()); } else if (obj instanceof List<?>) { ((List<?>) obj).remove(elt); if (((List<?>) obj).isEmpty()) mAttributes.remove(elt.getName()); } } @Override public Element getOptionalElement(String name) { Object obj = mAttributes.get(name); if (obj instanceof Element) return (Element) obj; else if (obj instanceof List<?>) return (Element) ((List<?>) obj).get(0); // could return a "pseudo-element" for attribute values... return null; } @Override public Set<Attribute> listAttributes() { if (mAttributes.isEmpty()) return Collections.emptySet(); HashSet<Attribute> set = new HashSet<Attribute>(); for (Map.Entry<String, Object> attr : mAttributes.entrySet()) { Object obj = attr.getValue(); if (obj != null && !attr.getKey().equals(A_CONTENT) && !(obj instanceof Element || obj instanceof List<?>)) set.add(new Attribute(attr, this)); } return set; } @SuppressWarnings("unchecked") @Override public List<Element> listElements(String name) { if (mAttributes.isEmpty()) return Collections.emptyList(); List<Element> list = new ArrayList<Element>(); for (Map.Entry<String, Object> entry : mAttributes.entrySet()) { String key = entry.getKey(); Object obj = entry.getValue(); if ((name == null || name.equals(XMLElement.E_ATTRIBUTE)) && key.equals(E_ATTRS) && obj instanceof JSONElement) { for (Object attr : ((Element) obj).mAttributes.values()) { if (attr instanceof JSONKeyValuePair) list.add(((JSONKeyValuePair) attr).asElement(this)); else for (JSONKeyValuePair kvp : (List<JSONKeyValuePair>) attr) { list.add(kvp.asElement(this)); } } } else if (name == null || name.equals(key)) { if (obj instanceof Element) list.add((Element) obj); else if (obj instanceof List) list.addAll((List<Element>) obj); } } return list; } @Override public boolean hasChildren() { if (!mAttributes.isEmpty()) { for (Object obj : mAttributes.values()) if (obj instanceof Element || obj instanceof List<?> || obj instanceof KeyValuePair) return true; } return false; } @Override public List<KeyValuePair> listKeyValuePairs(String eltname, String attrname) { Element attrs = getOptionalElement(E_ATTRS); if (attrs == null || !(attrs instanceof JSONElement)) return Collections.emptyList(); List<KeyValuePair> pairs = new ArrayList<KeyValuePair>(); for (Map.Entry<String, Object> entry : attrs.mAttributes.entrySet()) { List<?> values = (entry.getValue() instanceof List<?> ? (List<?>) entry.getValue() : Arrays.asList(entry.getValue())); for (Object multi : values) { if (multi instanceof KeyValuePair) pairs.add((KeyValuePair) multi); else if (multi instanceof String) pairs.add(new JSONKeyValuePair(entry.getKey(), (String) multi)); } } return pairs; } @Override public String getText() { return getAttribute(A_CONTENT, ""); } @Override String getRawText() { return getAttribute(A_CONTENT, null); } @Override public String getAttribute(String key, String defaultValue) { Object obj = mAttributes.get(key); if (obj != null) { if (obj instanceof List<?>) obj = ((List<?>) obj).isEmpty() ? null : ((List<?>) obj).get(0); if (obj instanceof Element) obj = ((Element) obj).getRawText(); else if (obj instanceof KeyValuePair) obj = ((KeyValuePair) obj).getValue(); } else { Element attrs = getOptionalElement(E_ATTRS); obj = (attrs == null ? null : attrs.mAttributes.get(key)); } if (obj == null) { return defaultValue; } else if (obj instanceof JSONKeyValuePair) { return ((JSONKeyValuePair) obj).getValue(); } else { return obj.toString(); } } @SuppressWarnings("unchecked") @Override public JSONElement clone() { JSONElement clone = new JSONElement(getQName()); if (mNamespaces != null) { if (clone.mNamespaces == null) clone.mNamespaces = new HashMap<String, String>(mNamespaces); else clone.mNamespaces.putAll(mNamespaces); } for (Map.Entry<String, Object> entry : mAttributes.entrySet()) { String key = entry.getKey(); Object value = entry.getValue(); if (value instanceof Element) { clone.addUniqueElement(((Element) value).clone()); } else if (value instanceof JSONKeyValuePair) { clone.mAttributes.put(key, ((JSONKeyValuePair) value).clone()); } else if (value instanceof List<?>) { for (Object child : (List<?>) value) { if (child instanceof Element) { clone.addNonUniqueElement(((Element) child).clone()); } else { Object childclone = child instanceof JSONKeyValuePair ? ((JSONKeyValuePair) child).clone() : child; List<Object> children = (List<Object>) clone.mAttributes.get(key); if (children == null) { (children = new ArrayList<Object>(((List<?>) value).size())).add(childclone); clone.mAttributes.put(key, children); } else { children.add(childclone); } } } } else { clone.mAttributes.put(key, value); } } return clone; } private static final class JSRequest { String js; private int offset; private final int max; JSRequest(String content) { js = content; max = js.length(); } private char readEscaped() throws SoapParseException { skipChar('\\'); char c, length; switch (c = js.charAt(offset)) { case 'b': return '\b'; case 't': return '\t'; case 'n': return '\n'; case 'f': return '\f'; case 'r': return '\r'; case 'u': length = 4; break; case 'x': length = 2; break; default: return c; } try { c = (char) Integer.parseInt(js.substring(offset + 1, offset + length + 1), 16); offset += length; } catch (NumberFormatException nfe) { error("malformed escape sequence: " + js.substring(offset - 1, offset + length + 1)); } return c; } private String readQuoted(char quote) throws SoapParseException { StringBuilder sb = new StringBuilder(); for (char c = js.charAt(offset); c != quote; c = js.charAt(++offset)) { if (c == '\n' || c == '\t' || offset >= max - 1) error("unterminated string"); else sb.append(c == '\\' ? readEscaped() : c); } skipChar(); return sb.toString(); } private String readLiteral() throws SoapParseException { StringBuilder sb = new StringBuilder(); for (char c = peekChar(); offset < max - 1; c = js.charAt(++offset)) { if (c <= ' ' || ",:]}/\"[{;=#".indexOf(c) >= 0) break; else if (c != '\\' || max - offset < 6 || js.charAt(offset + 1) != 'u') sb.append(c); else sb.append(readEscaped()); } if (sb.length() == 0) error("zero-length identifier"); return sb.toString(); } String readString() throws SoapParseException { char c = peekChar(); return (c == '"' || c == '\'' ? readQuoted(readChar()) : readLiteral()); } Object readValue() throws SoapParseException { char c = peekChar(); if (c == '"' || c == '\'') return readQuoted(readChar()); String literal = readLiteral(); if (literal.equals("null")) return null; if (literal.equals("true")) return Boolean.TRUE; if (literal.equals("false")) return Boolean.FALSE; if ((c >= '0' && c <= '9') || c == '.' || c == '-' || c == '+') { try { return Long.decode(literal); } catch (NumberFormatException nfe) { } try { return new Double(literal); } catch (NumberFormatException nfe) { } } return literal; } char peekChar() throws SoapParseException { skipWhitespace(); return js.charAt(offset); } char readChar() throws SoapParseException { skipWhitespace(); return js.charAt(offset++); } void skipChar() throws SoapParseException { readChar(); } void skipChar(char c) throws SoapParseException { char nxtChar = readChar(); if (nxtChar != c) { error("expected character: " + c + " found:" + nxtChar); } } private void skipWhitespace() throws SoapParseException { if (offset >= max) error("unexpected end of JSON input"); for (char c = js.charAt(offset); offset < max; c = js.charAt(++offset)) if (c != 0x09 && (c < 0x0A || c > 0x0D) && (c < 0x1C || c > 0x20)) break; } private void error(String cause) throws SoapParseException { throw new SoapParseException(cause, js); } } private static void parseKeyValuePair(JSRequest jsr, String key, Element parent) throws SoapParseException { Object value; switch (jsr.peekChar()) { case '{': jsr.skipChar(); KeyValuePair kvp = parent.addKeyValuePair(key, null); do { String attr = jsr.readString(); switch (jsr.readChar()) { case ':': break; case '=': if (jsr.peekChar() == '>') jsr.skipChar(); break; default: throw new SoapParseException("missing expected ':'", jsr.js); } if ((value = jsr.readValue()) == null) /* do nothing */; else if (key.equals(A_CONTENT)) kvp.setValue(value.toString()); else if (value instanceof Boolean) kvp.addAttribute(attr, ((Boolean) value).booleanValue()); else if (value instanceof Long) kvp.addAttribute(attr, ((Long) value).longValue()); else if (value instanceof Double) kvp.addAttribute(attr, ((Double) value).doubleValue()); else kvp.addAttribute(attr, value.toString()); switch (jsr.peekChar()) { case '}': break; case ',': case ';': jsr.skipChar(); break; default: throw new SoapParseException("missing expected ',' or ']'", jsr.js); } } while (jsr.peekChar() != '}'); jsr.skipChar(); break; case '[': jsr.skipChar(); while (jsr.peekChar() != ']') { parseKeyValuePair(jsr, key, parent); switch (jsr.peekChar()) { case ']': break; case ',': case ';': jsr.skipChar(); break; default: throw new SoapParseException("missing expected ',' or ']'", jsr.js); } } ; jsr.skipChar(); break; default: if ((value = jsr.readValue()) != null) parent.addKeyValuePair(key, value.toString()); break; } } static Element parseElement(JSRequest jsr, QName qname, ElementFactory factory) throws SoapParseException { Element elt = parseElement(jsr, qname.getName(), factory, null); if (elt.getNamespaceURI("") == null) { elt.setNamespace("", qname.getNamespaceURI()); } return elt; } private static Element parseElement(JSRequest jsr, String name, ElementFactory factory, Element parent) throws SoapParseException { boolean isAttrs = parent != null && name.equals(E_ATTRS); Element elt = isAttrs ? null : factory.createElement(name); jsr.skipChar('{'); while (jsr.peekChar() != '}') { String key = jsr.readString(); switch (jsr.readChar()) { case ':': break; case '=': if (jsr.peekChar() == '>') jsr.skipChar(); break; default: throw new SoapParseException("missing expected ':'", jsr.js); } if (isAttrs) { parseKeyValuePair(jsr, key, parent); } else { Object value; switch (jsr.peekChar()) { case '{': elt.addUniqueElement(parseElement(jsr, key, factory, elt)); break; case '[': jsr.skipChar(); while (jsr.peekChar() != ']') { elt.addNonUniqueElement(parseElement(jsr, key, factory, elt)); switch (jsr.peekChar()) { case ']': break; case ',': case ';': jsr.skipChar(); break; default: throw new SoapParseException("missing expected ',' or ']'", jsr.js); } } ; jsr.skipChar(); break; default: if ((value = jsr.readValue()) == null) break; if (key.equals(A_NAMESPACE)) elt.setNamespace("", value.toString()); else if (value instanceof Boolean) elt.addAttribute(key, ((Boolean) value).booleanValue()); else if (value instanceof Long) elt.addAttribute(key, ((Long) value).longValue()); else if (value instanceof Double) elt.addAttribute(key, ((Double) value).doubleValue()); else elt.addAttribute(key, value.toString()); break; } } switch (jsr.peekChar()) { case '}': break; case ',': case ';': jsr.skipChar(); break; default: throw new SoapParseException("missing expected ',' or '}'", jsr.js); } } jsr.skipChar('}'); return elt; } @Override public String toString() { StringBuilder sb = new StringBuilder(); try { marshal(sb, -1, false); } catch (IOException e) { // should really not happen with the StringBuilder impl of Appendable, just log it ZimbraLog.soap.error("Caught IOException: ", e); } return sb.toString(); } @Override public void marshal(Appendable out) throws IOException { marshal(out, -1, false); } @Override public String prettyPrint() { return prettyPrint(false); } @Override public String prettyPrint(boolean safe) { StringBuilder sb = new StringBuilder(); try { marshal(sb, 0, safe); } catch (IOException e) { // should really not happen with the StringBuilder impl of Appendable, just log it ZimbraLog.soap.error("Caught IOException: ", e); } return sb.toString(); } private static final int INDENT_SIZE = 2; private void marshal(Appendable out, int indent, boolean safe) throws IOException { indent = indent < 0 ? -1 : indent + INDENT_SIZE; out.append('{'); boolean needNamespace = mNamespaces == null ? false : namespaceDeclarationNeeded("", mNamespaces.get("").toString()); int size = mAttributes.size() + (needNamespace ? 1 : 0), lsize; if (size != 0) { int index = 0; for (Map.Entry<String, Object> attr : mAttributes.entrySet()) { indent(out, indent, true); out.append('"').append(StringUtil.jsEncode(attr.getKey())).append(indent >= 0 ? "\": " : "\":"); Object value = attr.getValue(); if (value instanceof String) { out.append('"').append(StringUtil.jsEncode(getAttrStringValue(attr, safe))).append('"'); } else if (value instanceof JSONKeyValuePair) { out.append(value.toString()); } else if (value instanceof JSONElement) { ((JSONElement) value).marshal(out, indent, safe); } else if (value instanceof FileBackedElement) { ((FileBackedElement) value).marshal(out); } else if (value instanceof Element) { out.append('"').append(StringUtil.jsEncode(value)).append('"'); } else if (!(value instanceof List<?>)) { out.append(String.valueOf(value)); } else { out.append('['); if ((lsize = ((List<?>) value).size()) > 0) { for (ListIterator<?> lit = ((List<?>) value).listIterator(); lit.hasNext();) { int lindent = indent < 0 ? -1 : indent + INDENT_SIZE; if (lsize > 1) { indent(out, lindent, true); } Object child = lit.next(); if (child instanceof JSONElement) { ((JSONElement) child).marshal(out, lindent, safe); } else if (child instanceof JSONKeyValuePair) { out.append(child.toString()); } else { out.append('"').append(StringUtil.jsEncode(child)).append('"'); } if (lit.nextIndex() != lsize) { out.append(','); } } } out.append(']'); } if (index++ < size - 1) { out.append(','); } } if (needNamespace) { indent(out, indent, true); out.append('"').append(A_NAMESPACE).append(indent >= 0 ? "\": \"" : "\":\""); out.append(StringUtil.jsEncode(mNamespaces.get(""))).append('"'); } indent(out, indent - 2, true); } out.append('}'); } private String getAttrStringValue(Map.Entry<String, Object> attr, boolean safe) { if (safe && ((A_CONTENT.equals(attr.getKey()) && isSensitiveElement(this)) || isSensitiveAttr(attr))) return SENSITIVE_STRING_REPLACEMENT; else return (String) attr.getValue(); } } public static class XMLElement extends Element { private String mText; private List<Element> mChildren; public static final ElementFactory mFactory = new XMLFactory(); public static final String E_ATTRIBUTE = "a"; public static final String A_ATTR_NAME = "n"; public static final String A_NAMESPACE = "xmlns"; public XMLElement(String name) throws ContainerException { mName = validateName(name); } public XMLElement(QName qname) throws ContainerException { mName = validateName(qname.getName()); String uri = qname.getNamespaceURI(); if (uri == null || uri.equals("")) return; mPrefix = qname.getNamespacePrefix(); setNamespace(mPrefix, uri); } private static final class XMLFactory implements ElementFactory { @Override public Element createElement(String name) { return new XMLElement(name); } @Override public Element createElement(QName qname) { return new XMLElement(qname); } } @Override public void destroy() { if (mChildren != null) { for (Element elt : mChildren) { elt.destroy(); } } } private final class XMLKeyValuePair implements KeyValuePair { private final XMLElement mTarget; private final String mAttrName; XMLKeyValuePair(String key, String value, String eltname, String attrname) { this(key, value, eltname, attrname, true); } XMLKeyValuePair(String key, String value, String eltname, String attrname, boolean register) { (mTarget = new XMLElement(eltname)).addAttribute(mAttrName = attrname, key).setText(value); if (register) addNonUniqueElement(mTarget); } @Override public KeyValuePair setValue(String value) throws ContainerException { mTarget.setText(value); return this; } @Override public KeyValuePair addAttribute(String key, String value) throws ContainerException { mTarget.addAttribute(key, value); return this; } @Override public KeyValuePair addAttribute(String key, long value) throws ContainerException { mTarget.addAttribute(key, value); return this; } @Override public KeyValuePair addAttribute(String key, double value) throws ContainerException { mTarget.addAttribute(key, value); return this; } @Override public KeyValuePair addAttribute(String key, boolean value) throws ContainerException { mTarget.addAttribute(key, value); return this; } @Override public String getKey() throws ContainerException { return mTarget.getAttribute(mAttrName, null); } @Override public String getValue() throws ContainerException { return mTarget.getRawText(); } @Override public String toString() { return mTarget.toString(); } } @Override public ElementFactory getFactory() { return mFactory; } @Override public Element addNonUniqueElement(String name) throws ContainerException { return addNonUniqueElement(new XMLElement(name)); } @Override public Element addNonUniqueElement(QName qname) throws ContainerException { return addNonUniqueElement(new XMLElement(qname)); } @Override public Element addNonUniqueElement(Element elt) throws ContainerException { if (elt == null || elt.mParent == this) { return elt; } else if (elt.mParent != null) { throw new ContainerException("element already has a parent - <" + elt.getName() + ">"); } else if (mText != null) { throw new ContainerException("cannot add children to element containing text - <" + this.getName() + ">, trying to add <" + elt.getName() + ">"); } assert (elt instanceof XMLElement || elt instanceof FileBackedElement); if (mChildren == null) { mChildren = new ArrayList<Element>(); } mChildren.add(elt); elt.mParent = this; return elt.collapseNamespace(); } @Override public Element setText(String content) throws ContainerException { if (content != null && !content.trim().equals("") && mChildren != null) { throw new ContainerException("cannot set text on element with children - <" + this.getName() + ">"); } mText = content; return this; } @Override public Element addAttribute(String key, String value, Disposition disp) throws ContainerException { validateName(key); // if we're setting an attribute, we need to clear all other things that could be considered the same... if (mAttributes != null) { mAttributes.remove(key); } if (mChildren != null) { for (Element child : listElements(key)) { if (!child.hasChildren()) { child.detach(); } } } // a null value leaves it unset; a non-null value places the attribute appropriately if (value != null) { if (disp == Disposition.CONTENT) { addNonUniqueElement(key).setText(value); } else { if (mAttributes == null) { mAttributes = new HashMap<String, Object>(); } mAttributes.put(key, value); } } return this; } @Override public KeyValuePair addKeyValuePair(String key, String value, String eltname, String attrname) throws ContainerException { return new XMLKeyValuePair(key, value, eltname == null ? E_ATTRIBUTE : validateName(eltname), attrname == null ? A_ATTR_NAME : validateName(attrname)); } private String validateName(String name) throws ContainerException { if (name == null || name.equals("")) { throw new ContainerException("blank/missing XML attribute name"); } for (int i = 0; i < name.length(); i++) { char c = name.charAt(i); if (c == ':' || c == '_' || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) continue; if (i > 0 && (c == '-' || c == '.' || (c >= '0' && c <= '9') || c == 0xB7 || c == 0x203F || c == 0x2040)) continue; if (c >= 0xC0 && c <= 0x1FFF && c != 0xD7 && c != 0xF7 && c != 0x37E) { if (i > 0 || c < 0x300 || c > 0x36F) continue; } if ((c >= 0x2070 && c <= 0x218F) || (c >= 0x2C00 && c <= 0x2FEF) || (c >= 0x3001 && c <= 0xD7FF)) continue; if ((c >= 0xF900 && c <= 0xFDCF) || (c >= 0xFDF0 && c <= 0xFFFD) || (c >= 0x10000 && c <= 0xEFFFF)) continue; throw new ContainerException("invalid XML attribute name: " + name); } return name; } @Override protected void detach(Element elt) throws ContainerException { super.detach(elt); if (mChildren != null) { mChildren.remove(elt); if (mChildren.size() == 0) { mChildren = null; } } } @Override public Element getOptionalElement(String name) { if (mChildren != null && name != null) { for (Element elt : mChildren) { if (elt.getName().equals(name)) { return elt; } } } return null; } @Override public Element getOptionalElement(QName qname) { if (mChildren != null && qname != null) { for (Element elt : mChildren) { if (elt.getQName().equals(qname)) { return elt; } } } return null; } @Override public Set<Attribute> listAttributes() { if (mAttributes == null || mAttributes.isEmpty()) { return Collections.emptySet(); } HashSet<Attribute> set = new HashSet<Attribute>(); for (Map.Entry<String, Object> attr : mAttributes.entrySet()) { set.add(new Attribute(attr, this)); } return set; } @Override public List<Element> listElements(String name) { if (mChildren == null) { return Collections.emptyList(); } ArrayList<Element> list = new ArrayList<Element>(); if (name == null || name.trim().equals("")) { list.addAll(mChildren); } else { for (Element elt : mChildren) { if (elt.getName().equals(name)) { list.add(elt); } } } return list; } @Override public boolean hasChildren() { return mChildren != null && !mChildren.isEmpty(); } @Override public List<KeyValuePair> listKeyValuePairs(String eltname, String attrname) { eltname = eltname == null ? E_ATTRIBUTE : validateName(eltname); attrname = attrname == null ? A_ATTR_NAME : validateName(attrname); List<KeyValuePair> pairs = new ArrayList<KeyValuePair>(); for (Element elt : listElements(eltname)) { String key = elt.getAttribute(attrname, null); if (key != null) { pairs.add(new XMLKeyValuePair(key, elt.getText(), eltname, attrname, false)); } } return pairs; } @Override public String getText() { return (mText == null ? "" : mText); } @Override String getRawText() { return mText; } @Override public String getAttribute(String key, String defaultValue) { if (key == null) { return defaultValue; } if (mAttributes != null) { String result; if ((result = (String) mAttributes.get(key)) != null) { return result; } } if (mChildren != null) { for (Element elt : mChildren) { if (elt.getName().equals(key)) { return elt.getText(); } } } return defaultValue; } private String xmlEncode(String str, boolean escapeQuotes) { if (str == null) return ""; StringBuilder sb = null; String replacement; int i, last; for (i = 0, last = -1; i < str.length(); i++) { char c = str.charAt(i); switch (c) { case '&': replacement = "&"; break; case '<': replacement = "<"; break; case '>': if (i < 2 || str.charAt(i - 1) != ']' || str.charAt(i - 2) != ']') continue; replacement = ">"; break; case '"': if (!escapeQuotes) continue; replacement = """; break; default: //Unicode supplementary characters (UTF-16) - Japnese/Chinese characters if (i + 1 < str.length() && isSupplementaryCharacter(c, str.charAt(i + 1))) { i++; continue; } if (isValidXmlCharacter(c)) continue; replacement = "?"; break; } if (sb == null) sb = new StringBuilder(str.substring(0, i)); else sb.append(str.substring(last, i)); sb.append(replacement); last = i + 1; } return (sb == null ? str : sb.append(str.substring(last, i)).toString()); } /** * Supplementary characters : Always appear in pairs, as a high surrogate followed by a low surrogate * @param first * @param second * @return true if passed set of characters are pair of supplementary character. */ static boolean isSupplementaryCharacter(char first, char second) { return Character.isHighSurrogate(first) && Character.isLowSurrogate(second); } static boolean isValidXmlCharacter(char ch) { int codepoint = ch; //Surrogate characters are NOT allowed if (ch == 0xFFFE || ch == 0xFFFF) { return false; } //Supported Character range in XML : #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] if ((codepoint == 0x09) || (codepoint == 0x0A) || (codepoint == 0x0D) || ((codepoint >= 0x20) && (codepoint <= 0xD7FF)) || ((codepoint >= 0xE000) && (codepoint <= 0xFFFD)) || ((codepoint >= 0x10000) && (codepoint <= 0x10FFFF))) { return true; } return false; } @Override public XMLElement clone() { XMLElement clone = new XMLElement(getQName()); clone.mText = mText; if (mAttributes != null) { clone.mAttributes = new HashMap<String, Object>(mAttributes); } if (mNamespaces != null) { if (clone.mNamespaces == null) clone.mNamespaces = new HashMap<String, String>(mNamespaces); else clone.mNamespaces.putAll(mNamespaces); } if (mChildren != null) { for (Element child : mChildren) clone.addNonUniqueElement(child.clone()); } return clone; } @Override public String toString() { StringBuilder sb = new StringBuilder(); try { marshal(sb, -1, false); } catch (IOException e) { // should really not happen with the StringBuilder impl of Appendable, just log it ZimbraLog.soap.error("Caught IOException: ", e); } return sb.toString(); } @Override public void marshal(Appendable out) throws IOException { marshal(out, -1, false); } @Override public String prettyPrint() { return prettyPrint(false); } @Override public String prettyPrint(boolean safe) { StringBuilder sb = new StringBuilder(); try { marshal(sb, 0, safe); } catch (IOException e) { // should really not happen with the StringBuilder impl of Appendable, just log it ZimbraLog.soap.error("Caught IOException: ", e); } return sb.toString(); } private static final int INDENT_SIZE = 2; private void marshal(Appendable out, int indent, boolean safe) throws IOException { indent(out, indent, indent > 0); // element's qualified name String qn = getQualifiedName(); out.append("<").append(qn); // element's attributes if (mAttributes != null) { for (Map.Entry<String, Object> attr : mAttributes.entrySet()) { out.append(' ').append(attr.getKey()).append("=\""); out.append(xmlEncode(getAttrValue(attr, safe), true)).append('"'); } } // new namespaces defined on this element if (mNamespaces != null) { for (Map.Entry<String, String> ns : mNamespaces.entrySet()) { String prefix = ns.getKey(); String uri = ns.getValue(); if (namespaceDeclarationNeeded(prefix, uri)) { out.append(' ').append(A_NAMESPACE).append(prefix.equals("") ? "" : ":").append(prefix); out.append("=\"").append(xmlEncode(uri, true)).append('"'); } } } // element content (children/text) and closing if (mChildren != null || !StringUtil.isNullOrEmpty(mText)) { out.append('>'); if (mChildren != null) { for (Element child : mChildren) { if (child instanceof XMLElement) { ((XMLElement) child).marshal(out, indent < 0 ? -1 : indent + INDENT_SIZE, safe); } else if (child instanceof FileBackedElement) { child.marshal(out); } else { out.append(xmlEncode(child.toString(), false)); } } indent(out, indent, true); } else { out.append(xmlEncode(getText(safe), false)); } out.append("</").append(qn).append('>'); } else { out.append("/>"); } } private static String getAttrValue(Map.Entry<String, Object> attr, boolean safe) { return safe && isSensitiveAttr(attr) ? SENSITIVE_STRING_REPLACEMENT : (String) attr.getValue(); } private String getText(boolean safe) { if (safe && isSensitiveElement(this)) return SENSITIVE_STRING_REPLACEMENT; else return getText(); } } private static final List<String> SENSITIVE_ATTRS = Arrays.asList("password", "pass", "pwd"); private static final String SENSITIVE_STRING_REPLACEMENT = "****"; private static boolean isSensitiveAttr(Map.Entry<String, Object> attr) { return SENSITIVE_ATTRS.contains(attr.getKey()); } private static boolean isSensitiveElement(Element element) { // - elements having name that ends with "password" or "Password" // - elements like: <a n='zimbraGalLdapBindPassword'>...</a> // - elements like: <a n='hostPwd'>...</a> // - elements like (zimlet specific case): <a n='webexZimlet_pwd1'>...</a> // - elements like: <prop name='passwd'>...</prop> String name = element.getName(); if (name.endsWith("assword")) { return true; } else if (name.equals("a")) { String propName = element.getAttribute("n", null); if (propName != null && (propName.endsWith("assword") || propName.endsWith("Pwd") || propName.contains("webexZimlet_pwd"))) { return true; } } else if (name.equals("prop")) { String propName = element.getAttribute("name", null); if (propName != null && (propName.endsWith("assword") || propName.endsWith("asswd") || propName.endsWith("Pwd"))) { return true; } } return false; } /** * Read-only {@link Element} backed by a file. Use this for encoding a big XML document which can't fit in memory. * The backed file must consist of a raw element already encoded. * Note that {@link destroy} will delete the backedFile */ public static final class FileBackedElement extends Element { private final File backedFile; public FileBackedElement(File file) { backedFile = file; } @Override public void destroy() { if (ZimbraLog.misc.isDebugEnabled()) { ZimbraLog.misc.debug("FileBackedElement destroy - rm %s", backedFile); } backedFile.delete(); } @Override public ElementFactory getFactory() { return null; } @Override public Element addNonUniqueElement(String name) { throw new UnsupportedOperationException(); } @Override public Element addNonUniqueElement(QName qname) { throw new UnsupportedOperationException(); } @Override public Element addNonUniqueElement(Element elt) { throw new UnsupportedOperationException(); } @Override public Element setText(String content) { throw new UnsupportedOperationException(); } @Override public Element addAttribute(String key, String value, Disposition disp) { throw new UnsupportedOperationException(); } @Override public KeyValuePair addKeyValuePair(String key, String value, String eltname, String attrname) { throw new UnsupportedOperationException(); } @Override public Element clone() { throw new UnsupportedOperationException(); } @Override public Element getOptionalElement(String name) { throw new UnsupportedOperationException(); } @Override public Set<Attribute> listAttributes() { throw new UnsupportedOperationException(); } @Override public List<Element> listElements(String name) { throw new UnsupportedOperationException(); } @Override public boolean hasChildren() { throw new UnsupportedOperationException(); } @Override public List<KeyValuePair> listKeyValuePairs(String eltname, String attrname) { throw new UnsupportedOperationException(); } @Override public String getText() { throw new UnsupportedOperationException(); } @Override String getRawText() { throw new UnsupportedOperationException(); } @Override public String getAttribute(String key, String defaultValue) { throw new UnsupportedOperationException(); } @Override public String prettyPrint() { throw new UnsupportedOperationException(); } @Override public String prettyPrint(boolean safe) { throw new UnsupportedOperationException(); } @Override public void marshal(Appendable out) throws IOException { if (!backedFile.exists()) { throw new IOException("marshal for FileBackedElement <" + this.getName() + "> failed - backing file " + backedFile + " does not exist"); } Files.copy(backedFile, Charsets.UTF_8, out); } } public static void main(String[] args) throws ContainerException, SoapParseException { System.out.println(Element.parseJSON("{ 'a':'b'}").getAttribute("a", null)); System.out.println(Element.parseJSON("{ '_attrs' : {'a':'b'}}").getAttribute("a", null)); System.out.println(Element.parseJSON("{foo:' bar'}").getAttribute("foo", null)); System.out.println(Element.parseJSON("{foo:'bar'}").getAttribute("foo", null)); System.out.println(Element.parseJSON("{foo:''}").getAttribute("foo", null)); System.out.println(Element.parseJSON("{ \"items\" : [ ] }")); System.out.println(Element.parseJSON("{ '_attrs' : {'a':[]}}").getAttribute("a", null)); org.dom4j.Namespace bogusNS = org.dom4j.Namespace.get("bogus", ""); QName qm = new QName("m", bogusNS); SoapProtocol proto = SoapProtocol.SoapJS; Element ctxt = new JSONElement(proto.getHeaderQName()).addUniqueElement(HeaderConstants.E_CONTEXT); ctxt.addNonUniqueElement(HeaderConstants.E_SESSION).setText("3").addAttribute(HeaderConstants.A_ID, 3); System.out.println(ctxt.getAttribute(HeaderConstants.E_SESSION, null)); Element env = testMessage(new JSONElement(proto.getEnvelopeQName()), proto, qm); System.out.println(env); System.out.println(Element.parseJSON(env.toString())); proto = SoapProtocol.Soap12; env = testMessage(new XMLElement(proto.getEnvelopeQName()), proto, qm); System.out.println(env.prettyPrint()); System.out.println(" name: " + env.getName()); System.out.println(" qualified name: " + env.getQualifiedName()); System.out.println(" qname: " + env.getQName()); Element e = testContacts(new JSONElement(MailConstants.GET_CONTACTS_RESPONSE)); System.out.println(e); System.out.println(e.prettyPrint()); testKeyValuePairs(e); System.out.println(Element.parseJSON(e.toString())); testKeyValuePairs(Element.parseJSON(e.toString())); System.out.println(Element.parseJSON(e.toString(), XMLElement.mFactory).prettyPrint()); e = testContacts(new XMLElement(MailConstants.GET_CONTACTS_RESPONSE)); System.out.println(e.prettyPrint()); for (Element elt : e.listElements()) System.out.println(" found: id=" + elt.getAttribute("ID", null)); testKeyValuePairs(e); // System.out.println(com.zimbra.common.soap.SoapProtocol.toString(e.toXML(), true)); System.out.println(new XMLElement("test").setText(" this\t is\nthe\rway ").getTextTrim() + "|"); System.out.println(Element.parseJSON( "{part:\"TEXT\",t:null,h:true,i:\"false\",\"ct\":\"\\x25multipart\\u0025\\/mixed\",\\u0073:3718}") .toString()); try { Element.parseJSON("{\"wkday\":{\"day\":\"TU\"},\"wkday\":{\"day\":\"WE\"},\"wkday\":{\"day\":\"FR\"}}"); } catch (SoapParseException spe) { System.out.println("caught exception (expected): " + spe.getMessage()); } System.out.println( new XMLElement("test").addAttribute("x", (String) null).addAttribute("x", "", Disposition.CONTENT) .addAttribute("x", "bar").addAttribute("x", (String) null)); System.out.println(new JSONElement("test").addAttribute("x", (String) null) .addAttribute("x", "foo", Disposition.CONTENT).addAttribute("x", "bar") .addAttribute("x", (String) null)); try { System.out.println("foo: |" + Element.parseXML("<test><foo/></test>").getAttribute("foo") + "|"); } catch (Exception x) { System.out.println("error parsing XML element: " + x); } } private static Element testMessage(Element env, SoapProtocol proto, QName qm) { env.addUniqueElement(proto.getBodyQName()).addUniqueElement(MailConstants.GET_MSG_RESPONSE) .addUniqueElement(qm).addAttribute("id", 1115).addAttribute("f", "aw").addAttribute("t", "64,67") .addAttribute("s", "Subject of the message has a \"\\\" in it", Disposition.CONTENT) .addAttribute("mid", "<kashdfgiai67r3wtuwfg@goo.com>", Disposition.CONTENT) .addNonUniqueElement("mp").addAttribute("part", "TEXT").addAttribute("ct", "multipart/mixed") .addAttribute("s", 3718); String orig = env.toString(), clone = env.clone().toString(); System.out.println("< " + orig); System.out.println("> " + clone); return env; } private static Element testContacts(Element parent) { parent.addNonUniqueElement("cn"); Element cn = parent.addNonUniqueElement("cn").addAttribute("id", 256).addAttribute("md", 1111196674000L) .addAttribute("l", 7).addAttribute("x", false); cn.addKeyValuePair("workPhone", "(408) 973-0500 x112", "pm", "name"); cn.addKeyValuePair("notes", "These are &\nrandom notes", "pm", "name"); cn.addKeyValuePair("firstName", "Ross \"Killa\"", "pm", "name"); cn.addKeyValuePair("lastName", "Dargahi", "pm", "name"); cn.addKeyValuePair("lastName", "Dangerous", "pm", "name"); cn.addKeyValuePair("image", null, "pm", "name").addAttribute("size", 34102).addAttribute("ct", "image/png") .addAttribute("part", "1"); cn = parent.addNonUniqueElement("cn").addAttribute("id", 257).addAttribute("md", 1111196674000L) .addAttribute("l", 7); cn.addKeyValuePair("workPhone", "(408) 973-0500 x111"); cn.addKeyValuePair("jobTitle", "CEO"); cn.addKeyValuePair("firstName", "Satish"); cn.addKeyValuePair("lastName", "Dharmaraj"); cn.addKeyValuePair("foo=bar", "baz=whop"); if (!parent.toString().equals(parent.clone().toString())) System.out.println("error: clone diverges from parent"); return parent; } private static void testKeyValuePairs(Element parent) { for (Element cn : parent.listElements()) for (KeyValuePair kvp : cn.listKeyValuePairs("pm", "name")) System.out.print(" " + kvp.getKey() + ": " + kvp.getValue()); System.out.println(); } }