
com.dell.doradus.common.UNode Maven / Gradle / Ivy
/*
* Copyright (C) 2014 Dell, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.dell.doradus.common;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.GZIPOutputStream;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* Represent a "Universal Node", which can be parsed from or generated to JSON or XML.
* Note that DOM trees parsed from JSON or XML are intended to be consumed by POJOs that
* expected a UNode tree, and UNode trees generated by POJOs can be used to directly
* generate JSON or XML. However, going from JSON to a UNode tree to XML is not intended
* since there are semantics about XML that are not known when parsing JSON.
*
* This class uses manufacturing methods to create objects. Except for adding child nodes,
* objects are immutable once created.
*/
final public class UNode {
// The supported UNode types.
private enum NodeType {
ARRAY,
MAP,
VALUE
} // enum NodeType
// Members:
private final String m_name;
private final NodeType m_type;
private final String m_value;
private List m_children;
private UNode m_parent;
// Hints for special XML/JSON handling
private final boolean m_bAttribute;
private final String m_tagName;
private boolean m_bAltFormat;
// Child node names also live in this map only when this UNode is a MAP.
private Map m_childNodeMap;
// Private constructor: All objects are created via manufacturers
private UNode(String name, NodeType type, String value, boolean bAttribute, String tagName) {
// Name and type are required. Tag name can be empty but not null.
assert name != null && name.length() > 0;
assert type != null;
assert tagName != null;
m_name = name;
m_type = type;
m_value = value;
m_bAttribute = bAttribute;
m_tagName = tagName;
} // constructor
// Listener passed to JSONAnnie.parse() methods. Builds the UNode tree we want from a
// JSON document.
private static class SajListener implements JSONAnnie.SajListener {
private static final int MAX_STACK_SIZE = 32;
private UNode[] m_stack = new UNode[MAX_STACK_SIZE];
private int m_stackInx = 0;
private UNode m_rootNode;
private void push(UNode node) {
assert m_stackInx < MAX_STACK_SIZE;
if (m_stackInx == 0) {
m_rootNode = node;
} else {
m_stack[m_stackInx - 1].addChildNode(node);
}
m_stack[m_stackInx++] = node;
} // push
private UNode pop() {
assert m_stackInx > 0;
UNode node = m_stack[m_stackInx--];
m_stack[m_stackInx + 1] = null;
return node;
} // pop
private UNode getRootNode() {
if (m_rootNode == null) {
// JSON text was empty, etc.
m_rootNode = UNode.createMapNode("_unnamed");
}
return m_rootNode;
} // getRootNode
private UNode tos() {
assert m_stackInx > 0;
return m_stack[m_stackInx - 1];
} // tos
@Override
public void onStartObject(String name) {
push(UNode.createMapNode(name));
} // onStartObject
@Override
public void onEndObject() {
pop();
} // onEndObject
@Override
public void onStartArray(String name) {
push(UNode.createArrayNode(name));
} // onStartArray
@Override
public void onEndArray() {
pop();
} // onEndArray
@Override
public void onValue(String name, String value) {
if (m_stackInx == 0) {
// Outer object is a simple value
m_rootNode = UNode.createValueNode(name, value);
} else {
tos().addValueNode(name, value);
}
} // onValue
} // static class SajListener
////////// static Manufacturers
/**
* Create a MAP UNode with the given node name.
*
* @param name Name for new MAP node.
*/
public static UNode createMapNode(String name) {
return new UNode(name, NodeType.MAP, null, false, "");
} // createMapNode
/**
* Create a MAP UNode with the given node name and tag name.
*
* @param name Name for new MAP node.
* @param tagName Tag name.
*/
public static UNode createMapNode(String name, String tagName) {
return new UNode(name, NodeType.MAP, null, false, tagName);
} // createMapNode
/**
* Create an ARRAY UNode with the given node name.
*
* @param name Name for new ARRAY node.
*/
public static UNode createArrayNode(String name) {
return new UNode(name, NodeType.ARRAY, null, false, "");
} // createArrayNode
/**
* Create an ARRAY UNode with the given node name and tag name.
*
* @param name Name for new ARRAY node.
* @param tagName Tag name.
*/
public static UNode createArrayNode(String name, String tagName) {
return new UNode(name, NodeType.ARRAY, null, false, tagName);
} // createArrayNode
/**
* Create a VALUE UNode with the given node name and value.
*
* @param name Node name.
* @param value Node value.
*/
public static UNode createValueNode(String name, String value) {
String nodeValue = value == null ? "" : value;
return new UNode(name, NodeType.VALUE, nodeValue, false, "");
} // createValueNode
/**
* Create a VALUE UNode with the given node name and value, and optionally mark the
* node as an "attribute".
*
* @param name Node name.
* @param value Node value.
* @param bAttribute True to mark the node as an attribute.
*/
public static UNode createValueNode(String name, String value, boolean bAttribute) {
String nodeValue = value == null ? "" : value;
return new UNode(name, NodeType.VALUE, nodeValue, bAttribute, "");
} // createValueNode
/**
* Create a VALUE UNode with the given node name, value, and tag name.
*
* @param name Node name.
* @param value Node value.
* @param tagName Tag name.
*/
public static UNode createValueNode(String name, String value, String tagName) {
String nodeValue = value == null ? "" : value;
return new UNode(name, NodeType.VALUE, nodeValue, false, tagName);
} // createValueNode
////////// public static methods
/**
* Parse the given text, formatted with the given content-type, into a UNode tree and
* return the root node.
*
* @param text Text to be parsed.
* @param contentType {@link ContentType} of text. Only JSON and XML are supported.
* @return Root node of parsed node tree.
* @throws IllegalArgumentException If a parsing error occurs.
*/
public static UNode parse(String text, ContentType contentType) throws IllegalArgumentException {
UNode result = null;
if (contentType.isJSON()) {
result = parseJSON(text);
} else if (contentType.isXML()) {
result = parseXML(text);
} else {
Utils.require(false, "Unsupported content-type: " + contentType);
}
return result;
} // parse
/**
* Parse the text from the given character reader, formatted with the given content-type,
* into a UNode tree and return the root node. The reader is closed when finished.
*
* @param reader Reader providing source characters.
* @param contentType {@link ContentType} of text. Only JSON and XML are supported.
* @return Root node of parsed node tree.
* @throws IllegalArgumentException If a parsing error occurs.
*/
public static UNode parse(Reader reader, ContentType contentType) throws IllegalArgumentException {
UNode result = null;
if (contentType.isJSON()) {
result = parseJSON(reader);
} else if (contentType.isXML()) {
result = parseXML(reader);
} else {
Utils.require(false, "Unsupported content-type: " + contentType);
}
return result;
} // parse
/**
* Parse the text from the given file, formatted with the given content-type, into a
* UNode tree and return the root node. The file is read with a BufferedReader over
* a FileReader using the default encoding for the current system. Any I/O exception
* or parsing error is passed to the caller.
*
* @param file File to read source from.
* @param contentType {@link ContentType} of text. Only JSON and XML are supported.
* @return Root node of parsed node tree.
* @throws Exception If the file is not found, an I/O error occurs, or a parsing
* error occurs.
*/
public static UNode parse(File file, ContentType contentType) throws Exception {
try (Reader reader = new BufferedReader(new FileReader(file))) {
UNode result = null;
if (contentType.isJSON()) {
result = parseJSON(reader);
} else if (contentType.isXML()) {
result = parseXML(reader);
} else {
Utils.require(false, "Unsupported content-type: " + contentType);
}
return result;
}
} // parse
/**
* Parse the given JSON text and return the appropriate UNode object. The only JSON
* documents we allow are in the form:
*
* {"something": [value]}
*
* This means that when we parse the JSON, we should see an object with a single
* member. The UNode returned is a MAP object whose name is the member name and whose
* elements are parsed from the [value].
*
* @param text JSON text to parse
* @return UNode with type == {@link UNode.NodeType#MAP}.
* @throws IllegalArgumentException If the JSON text is malformed.
*/
public static UNode parseJSON(String text) throws IllegalArgumentException {
assert text != null && text.length() > 0;
SajListener listener = new SajListener();
new JSONAnnie(text).parse(listener);
return listener.getRootNode();
} // parseJSON
/**
* Parse the JSON text from the given character Reader and return the appropriate
* UNode object. If an error occurs reading from the reader, it is passed to the
* caller. The reader is closed when parsing is done. The only JSON documents we
* allow are in the form:
*
* {"something": [value]}
*
* This means that when we parse the JSON, we should see an object with a single
* member. The UNode returned is a MAP object whose name is the member name and whose
* elements are parsed from the [value].
*
* @param reader Character reader contain JSON text to parse. The reader is
* closed when reading is complete.
* @return UNode with type == {@link UNode.NodeType#MAP}.
* @throws IllegalArgumentException If the JSON text is malformed or an error occurs
* while reading from the reader.
*/
public static UNode parseJSON(Reader reader) throws IllegalArgumentException {
assert reader != null;
SajListener listener = new SajListener();
try {
new JSONAnnie(reader).parse(listener);
} finally {
Utils.close(reader);
}
return listener.getRootNode();
} // parseJSON
/**
* Parse the given XML text and return the appropriate UNode object. The UNode
* returned is a MAP whose child nodes are built from the attributes and child
* elements of the document's root element.
*
* @param text XML text to be parsed.
* @return UNode with type == {@link UNode.NodeType#MAP}.
* @throws IllegalArgumentException If the XML is malformed.
*/
public static UNode parseXML(String text) throws IllegalArgumentException {
assert text != null && text.length() > 0;
// This throws if the XML is malformed.
Element rootElem = Utils.parseXMLDocument(text);
return parseXMLElement(rootElem);
} // parseXML
/**
* Parse XML from the given Reader and return the appropriate UNode object. The UNode
* returned is a MAP whose child nodes are built from the attributes and child
* elements of the document's root element.
*
* @param reader Reader contain XML text to parse.
* @return UNode with type == {@link UNode.NodeType#MAP}.
* @throws IllegalArgumentException If the XML is malformed.
*/
public static UNode parseXML(Reader reader) throws IllegalArgumentException {
assert reader != null;
// This throws if the XML is malformed.
Element rootElem = Utils.parseXMLDocument(reader);
// Parse the root element and ensure it elligible as a map.
UNode rootNode = parseXMLElement(rootElem);
return rootNode;
} // parseXML
////////// public member methods
///// Getters
/**
* Get this node's name. All nodes have a name.
*
* @return This node's name.
*/
public String getName() {
return m_name;
} // getName
/**
* Get this node's value. Only {@link UNode.NodeType#VALUE} nodes have a value.
*
* @return This node's value.
*/
public String getValue() {
return m_value;
} // getValue
/**
* Return true if this UNode's type is {@link UNode.NodeType#ARRAY}.
*
* @return True if this UNode's type is {@link UNode.NodeType#ARRAY}.
*/
public boolean isArray() {
return m_type == NodeType.ARRAY;
} // isArray
/**
* Return true if this UNode's type is {@link UNode.NodeType#ARRAY} or {@link UNode.NodeType#MAP}.
*
* @return True if this UNode's type is {@link UNode.NodeType#ARRAY} or {@link UNode.NodeType#MAP}.
*/
public boolean isCollection() {
return m_type == NodeType.ARRAY || m_type == NodeType.MAP;
} // isCollection
/**
* Return true if this UNode's type is {@link UNode.NodeType#MAP}.
*
* @return True if this UNode's type is {@link UNode.NodeType#MAP}.
*/
public boolean isMap() {
return m_type == NodeType.MAP;
} // isMap
/**
* Return true if this UNode's type is {@link UNode.NodeType#VALUE}.
*
* @return True if this UNode's type is {@link UNode.NodeType#VALUE}.
*/
public boolean isValue() {
return m_type == NodeType.VALUE;
} // isValue
///// Member access
/**
* Get the child member with the given index. The node must be a MAP or ARRAY. Child
* members are retained in the order they are added. If the given index is out of
* bounds, null is returned.
*
* @param index Zero-relative child node index.
* @return The child member at the given index or null if there is no child
* node with the given index.
*/
public UNode getMember(int index) {
assert isCollection();
if (m_children == null || index >= m_children.size()) {
return null;
}
return m_children.get(index);
} // getMember
/**
* Get the number of members (child nodes) owned by this node.
*
* @return The number of members (child nodes) owned by this node.
*/
public int getMemberCount() {
return m_children == null ? 0 : m_children.size();
} // getMemberCount
/**
* Get the member (child) names of this UNode, if any, as an Iterable object.
* If this UNode is not a MAP or has no children, there will be no child names.
*
* @return An Iterable object that returns this node's member names if it is
* a map. The result won't be null but there may not be any child members.
*/
public Iterable getMemberNames() {
if (m_childNodeMap == null) {
m_childNodeMap = new LinkedHashMap();
}
return m_childNodeMap.keySet();
} // getMemberNames
/**
* Get the value of the child VALUE node (member) of this UNode with the given name.
* If this is not a MAP, has no children, there is no child node with the given name,
* or the child node isn't a VALUE node, then null is returned.
*
* @param name Candidate name of a child member node.
* @return Value of child VALUE UNode with the given name, if any, otherwise null.
*/
public String getMemberValue(String name) {
if (m_childNodeMap == null) {
return null;
}
UNode childNode = m_childNodeMap.get(name);
return childNode != null && childNode.isValue() ? childNode.getValue() : null;
} // getMemberValue
/**
* Get the child node (member) of this UNode with the given name. If this UNode isn't
* a MAP or there is no child node with the given name, null is returned. Note: the
* UNode returned is not copied.
*
* @param name Candidate name of a child member node.
* @return Child UNode with the given name, if any, otherwise null.
*/
public UNode getMember(String name) {
if (m_childNodeMap == null) {
return null;
}
return m_childNodeMap.get(name);
} // getMember
/**
* Get the list of child nodes of this collection UNode as an Iterable object.
* The UNode must be a MAP or an ARRAY.
*
* @return An Iterable object that iterates through this node's children. The
* result will never be null, but there might not be any child nodes.
*/
public Iterable getMemberList() {
assert m_type == NodeType.MAP || m_type == NodeType.ARRAY;
if (m_children == null) {
m_children = new ArrayList();
}
return m_children;
} // getMemberList
/**
* Get this UNode's tag name, if set.
*
* @return This node's tag name, if set, otherwise null.
*/
public String getTagName() {
return m_tagName;
} // getTagName
/**
* Return true if this node has child nodes and all of them are simple value nodes.
* This means that each child must be a {@link NodeType#VALUE} and its name must be
* "value". If this node has no children or if at least one child is not a simple
* value, false is returned.
*
* @return True if this node has children and they are all value nodes.
*/
public boolean childrenAreValues() {
if (m_children == null || m_children.size() == 0) {
return false;
}
for (UNode child : m_children) {
if (!child.isValue() || !child.getName().equals("value")) {
return false;
}
}
return true;
} // childrenAreValues
/**
* Return true if this is an ARRAY or MAP node with at least one child node.
*
* @return True if this is an ARRAY or MAP node with at least one child node.
*/
public boolean hasMembers() {
return m_children != null && m_children.size() > 0;
} // hasMembers
/**
* Format the DOM tree rooted at this UNode into text in the requested content-type.
*
* @param contentType Desired text format. Only JSON and XML are supported.
* @return Text in the requested format.
*/
public String toString(ContentType contentType) {
String result = null;
if (contentType.isJSON()) {
result = toJSON();
} else if (contentType.isXML()) {
result = toXML();
} else {
Utils.require(false, "Unsupported content-type: " + contentType);
}
return result;
} // toString(ContentType)
/**
* Convert the DOM tree rooted at this UNode into a JSON document.
*
* @return JSON document for the DOM tree rooted at this UNode.
*/
public String toJSON() {
JSONEmitter json = new JSONEmitter();
json.startDocument();
toJSON(json);
json.endDocument();
return json.toString();
} // toJSON
/**
* Convert the DOM tree rooted at this UNode into a JSON document. Optionally format
* the text with indenting to make it look pretty.
*
* @param bPretty True to indent JSON output.
* @return JSON document for the DOM tree rooted at this UNode.
*/
public String toJSON(boolean bPretty) {
int indent = bPretty ? 3 : 0;
JSONEmitter json = new JSONEmitter(indent);
json.startDocument();
toJSON(json);
json.endDocument();
return json.toString();
} // toJSON
/**
* Convert the DOM tree rooted at this UNode into a JSON document compressed with GZIP.
*
* @return JSON document for the DOM tree rooted at this UNode compressed with GZIP.
* @throws IOException If an error occurs writing to the GZIP stream.
*/
public byte[] toCompressedJSON() throws IOException {
// Wrap a GZIPOuputStream around a ByteArrayOuputStream.
ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
GZIPOutputStream gzipOut = new GZIPOutputStream(bytesOut);
// Wrap the GZIPOutputStream with an OutputStreamWriter that convers JSON Unicode
// text to bytes using UTF-8.
OutputStreamWriter writer = new OutputStreamWriter(gzipOut, Utils.UTF8_CHARSET);
// Create a JSONEmitter that will write its output to the writer above and generate
// the JSON output.
JSONEmitter json = new JSONEmitter(writer);
json.startDocument();
toJSON(json);
json.endDocument();
// Ensure the output stream is flushed and the GZIP is finished, then the output
// buffer is complete.
writer.flush();
gzipOut.finish();
return bytesOut.toByteArray();
} // toCompressedJSON
/**
* Convert the DOM tree rooted at this UNode into an XML document.
*
* @return XML document for the DOM tree rooted at this UNode.
* @throws IllegalArgumentException If an XML construction error occurs.
*/
public String toXML() throws IllegalArgumentException {
XMLBuilder xml = new XMLBuilder();
xml.startDocument();
toXML(xml);
xml.endDocument();
return xml.toString();
} // toXML
/**
* Convert the DOM tree rooted at this UNode into an XML document, optionally
* indenting each XML level to product a "pretty" structured output.
*
* @param bPretty True to indent XML output.
* @return XML document for the DOM tree rooted at this UNode.
* @throws IllegalArgumentException If an XML construction error occurs.
*/
public String toXML(boolean bPretty) throws IllegalArgumentException {
int indent = bPretty ? 3 : 0;
XMLBuilder xml = new XMLBuilder(indent);
xml.startDocument();
toXML(xml);
xml.endDocument();
return xml.toString();
} // toXML
/**
* Add the XML required for this node to the given XMLBuilder.
*
* @param xml An in-progress XMLBuilder.
* @throws IllegalArgumentException If an XML construction error occurs.
*/
public void toXML(XMLBuilder xml) throws IllegalArgumentException {
assert xml != null;
// Determine what tag name to use for the generated element.
Map attrMap = new LinkedHashMap<>();
String elemName = m_name;
if (m_tagName.length() > 0) {
// Place m_name into a "name" attribute and use m_tagName
attrMap.put("name", m_name);
elemName = m_tagName;
}
// Add child VALUE nodes marked as "attribute" in its own map.
addXMLAttributes(attrMap);
switch (m_type) {
case ARRAY:
// Start an element with or without attributes.
if (attrMap.size() > 0) {
xml.startElement(elemName, attrMap);
} else {
xml.startElement(elemName);
}
// Add XML for non-attribute child nodes.
if (m_children != null) {
for (UNode childNode : m_children) {
if (childNode.m_type != NodeType.VALUE || !childNode.m_bAttribute) {
childNode.toXML(xml);
}
}
}
xml.endElement();
break;
case MAP:
// Start an element with or without attributes.
if (attrMap.size() > 0) {
xml.startElement(elemName, attrMap);
} else {
xml.startElement(elemName);
}
// Add XML for non-attribute child nodes in name order.
if (m_childNodeMap != null) {
assert m_childNodeMap.size() == m_children.size();
for (UNode childNode : m_childNodeMap.values()) {
if (childNode.m_type != NodeType.VALUE || !childNode.m_bAttribute) {
childNode.toXML(xml);
}
}
}
xml.endElement();
break;
case VALUE:
// Map to a simple element.
String value = m_value;
if (Utils.containsIllegalXML(value)) {
value = Utils.base64FromString(m_value);
attrMap.put("encoding", "base64");
}
if (attrMap.size() > 0) {
xml.addDataElement(elemName, value, attrMap);
} else {
xml.addDataElement(elemName, value);
}
break;
default:
assert false : "Unexpected NodeType: " + m_type;
}
} // toXML
/**
* Return a diagnostic string representing this UNode in the form:
*
* UNode: {type=[type], name=[name], value=[value]}
*
* @return A diagnostic string representing this UNode.
*/
@Override
public String toString() {
return toJSON(true);
//return String.format("UNode: {type=%s, name=%s, value=%s}",
// (m_type == null ? "" : m_type.toString()),
// (m_name == null ? "" : m_name),
// (m_value == null ? "" : m_value)
// );
} // toString
/**
* Return an indented diagnostic string of this UNode and the underlying tree. This
* method calls {@link #toString()}, appends a newline, and the recurses to each
* child node, which prepends an indent and adds its node tree.
*
* @return An indented diagnostic string of this node and its UNode tree.
*/
public String toStringTree() {
StringBuilder builder = new StringBuilder();
toStringTree(builder, 0);
return builder.toString();
} // printTree
///// Public update methods
/**
* Create a new ARRAY node with the given name and add it as a child of this node.
* This node must be a MAP or ARRAY. This is a convenience method that calls
* {@link UNode#createArrayNode(String)} and then {@link #addChildNode(UNode)}.
*
* @param name Name of new ARRAY node.
* @return New ARRAY node, added as a child of this node.
*/
public UNode addArrayNode(String name) {
return addChildNode(UNode.createArrayNode(name));
} // addArrayNode
/**
* Create a new ARRAY node with the given name and tag name and add it as a child of
* this node. This node must be a MAP or ARRAY. This is a convenience method that
* calls {@link UNode#createArrayNode(String, String)} and then
* {@link #addChildNode(UNode)}.
*
* @param name Name of new ARRAY node.
* @param tagName Tag name of new ARRAY node (for XML).
* @return New ARRAY node, added as a child of this node.
*/
public UNode addArrayNode(String name, String tagName) {
return addChildNode(UNode.createArrayNode(name, tagName));
} // addArrayNode
/**
* Create a new MAP node with the given name and add it as a child of this node. This
* node must be a MAP or ARRAY. This is a convenience method that calls
* {@link UNode#createMapNode(String)} and then {@link #addChildNode(UNode)}.
*
* @param name Name of new child MAP node.
* @return New MAP node, added as a child of this node.
*/
public UNode addMapNode(String name) {
return addChildNode(UNode.createMapNode(name));
} // addMapNode
/**
* Create a new MAP node with the given name and tag name add it as a child of this
* node. This node must be a MAP or ARRAY. This is a convenience method that calls
* {@link UNode#createMapNode(String, String)} and then {@link #addChildNode(UNode)}.
*
* @param name Name of new MAP node.
* @param tagName Tag name of new MAP node (for XML).
* @return New MAP node, added as a child of this node.
*/
public UNode addMapNode(String name, String tagName) {
return addChildNode(UNode.createMapNode(name, tagName));
} // addMapNode
/**
* Create a new VALUE node with the given name and value and add it as a child of this
* node. This node must be a MAP or ARRAY. This is convenience method that calls
* {@link UNode#createValueNode(String, String)} and then {@link #addChildNode(UNode)}.
*
* @param name Name of new VALUE node.
* @param value Value of new VALUE node.
* @return New VALUE node.
*/
public UNode addValueNode(String name, String value) {
return addChildNode(UNode.createValueNode(name, value));
} // addValueNode
/**
* Create a new VALUE node with the given name, value, and attribute flag and add it
* as a child of this node. This node must be a MAP or ARRAY. This is convenience
* method that calls {@link UNode#createValueNode(String, String, boolean)} and then
* {@link #addChildNode(UNode)}.
*
* @param name Name of new VALUE node.
* @param value Value of new VALUE node.
* @param bAttribute True to mark the new VALUE node as an attribute (for XML).
* @return New VALUE node.
*/
public UNode addValueNode(String name, String value, boolean bAttribute) {
return addChildNode(UNode.createValueNode(name, value, bAttribute));
} // addValueNode
/**
* Create a new VALUE node with the given name, value, and tag name and add it as a
* child of this node. This node must be a MAP or ARRAY. This is convenience method
* that calls {@link UNode#createValueNode(String, String, String)} and then
* {@link #addChildNode(UNode)}.
*
* @param name Name of new VALUE node.
* @param value Value of new VALUE node.
* @param tagName Tag name of new VALUE node (for XML).
* @return New VALUE node.
*/
public UNode addValueNode(String name, String value, String tagName) {
return addChildNode(UNode.createValueNode(name, value, tagName));
} // addValueNode
/**
* Add the given child node to this node, which must be a MAP or ARRAY. If this node
* is a MAP, the name of the child node must be unique among existing child nodes. The
* same node is returned as a result so the caller can do things like:
*
* UNode childNode = parentNode.addChildNode(UNode.createMapNode("foo"));
*
*
* @param childNode Child node to add.
* @return Same child node object passed as a parameter.
* @throws IllegalArgumentException If the child node name is not unique and this is
* a map.
*/
public UNode addChildNode(UNode childNode) throws IllegalArgumentException {
assert m_type == NodeType.ARRAY || m_type == NodeType.MAP;
assert childNode != null;
assert childNode.m_parent == null;
// Allocate on first child addition.
if (m_children == null) {
m_children = new ArrayList();
}
// Add to list first.
m_children.add(childNode);
childNode.m_parent = this;
// For MAPs, also add to the child name map and ensure the name is unique.
if (m_type == NodeType.MAP) {
if (m_childNodeMap == null) {
m_childNodeMap = new LinkedHashMap();
}
UNode priorNode = m_childNodeMap.put(childNode.m_name, childNode);
//assert priorNode == null : "Duplicate name ('" + childNode.m_name +
// "') added to the same parent: " + m_name;
if(priorNode != null) {
throw new RuntimeException("Duplicate name ('" + childNode.m_name +
"') added to the same parent: " + m_name);
}
}
return childNode;
} // addChildNode
/**
* Add all child nodes in the given collection to this node, which must be a MAP or
* ARRAY. This method merely iterates through the list and calls
* {@link #addChildNode(UNode)} for each one.
*
* @param childNodes List of child UNode objects to add to this one.
*/
public void addChildNodes(Collection childNodes) {
for (UNode childNode : childNodes) {
addChildNode(childNode);
}
} // addChildNodes
/**
* Delete the child node of this MAP node with the given name, if it exists. This node
* must be a MAP. The child node name may or may not exist.
*
* @param childName Name of child node to remove from this MAP node.
*/
public void removeMember(String childName) {
assert isMap() : "'removeMember' allowed only for MAP nodes";
if (m_childNodeMap != null) {
// Remove from child name map and then list if found.
UNode removeNode = m_childNodeMap.remove(childName);
if (removeNode != null) {
m_children.remove(removeNode);
}
}
} // removeMember
/**
* Set the alternate-format option for this VALUE UNode. This option is currently only
* used for JSON formatting. The default syntax generated for a VALUE node in JSON is:
*
* "{name}": "{value}"
*
* But if the parent node is an array and this node's name is "value", the VALUE node
* generates an unamed JSON value:
*
* "{name}"
*
* However, if the VALUE node has a tag name and the alternate-format option is set,
* the following JSON is generarted instead:
*
* "{tag}: {"{name}": "{value}"}
*
* This is used in cases where we want the XML to look like this:
*
* <field name="Tags">Customer</field>
*
* But we want the JSON to look like this:
*
* "field": {"Tags": "Customer"}
*
*
* @param bAltFormat True to enable the alternate format option for this UNode.
*/
public void setAltFormat(boolean bAltFormat) {
assert isValue();
m_bAltFormat = bAltFormat;
} // setAltFormat
////////// private JSON methods
// Add the appropriate JSON syntax for this UNode to the given JSONEmitter.
private void toJSON(JSONEmitter json) {
switch (m_type) {
case ARRAY:
json.startArray(m_name);
if (m_children != null) {
for (UNode childNode : m_children) {
if (childNode.isMap()) {
json.startObject();
childNode.toJSON(json);
json.endObject();
} else {
childNode.toJSON(json);
}
}
}
json.endArray();
break;
case MAP:
// Return map child modes in name order.
json.startGroup(m_name);
if (m_childNodeMap != null) {
assert m_childNodeMap.size() == m_children.size();
for (UNode childNode : m_childNodeMap.values()) {
childNode.toJSON(json);
}
}
json.endGroup();
break;
case VALUE:
if (m_bAltFormat && m_tagName != null) {
// Generate as ": {"": ""}
json.startGroup(m_tagName);
json.addValue(m_name, m_value);
json.endGroup();
} else if (m_parent != null && m_parent.isArray()) {
if (m_name.equals("value")) {
// nameless node: ""
json.addValue(m_value);
} else {
// value as an object: {"name": "value"}
json.addObject(m_name, m_value);
}
} else {
// Simple case: "": ""
json.addValue(m_name, m_value);
}
break;
default:
assert false : "Unknown NodeType: " + m_type;
}
} // toJSON
////////// private XML methods
// Parse the XML structure rooted at the given element and return the appropriate
// UNode object. Note that element content is only allowed in "leaf" elements, hence
// such elements cannot have child elements. The two rules that allow element
// content are:
//
// 1) If the element has no attributes and no child nodes, it becomes a VALUE node
// whose name is the tag name and whose value is the element's content. Example:
//
// Stellar1
//
// This becomes a VALUE UNode with name="key" and value="Stellar1".
//
// 2) If the element has a single attribute called "name" and no child elements, it
// becomes a VALUE node named with the attribute's value and the element content as
// the node value. Example:
//
// lollapalooza
//
// This becomes a VALUE UNode with name="_ID" and value="lollapalooza". The element
// name ("field") is saved in the "tag name" member.
//
// A variant of rules 1) and 2) is that the attribute encoding="Base64" can be present
// to denote that the value is Base64-encoded (and UTF-8 encoded under that). Hence,
// the following two are identical to the previous examples except that the element
// data is Base64 and UTF-8 decoded:
//
// Stellar1
// lollapalooza
//
// The remaining rules cannot have element content:
//
// 3) If the element has exactly two attributes called "name" and "value", it becomes
// a VALUE node using the "name" and "value" attribute values respectively. Example:
//
//
//
// This becomes a VALUE UNode with name="AutoTables" and value="false". This case
// is not allowed to have child elements. The element name ("option") is saved in
// the "tag name" member. The attribute encoding="Base64" can be used for this
// case as well, causing the "value" attribute to be Base64 and UTF-8 decoded.
//
// 4) In all remaining cases, the element becomes a MAP or an ARRAY. A MAP is created
// if the node has no duplicate child element names, otherwise an ARRAY is created.
// If the element has a "name" attribute, its value is used for the node name.
// Otherwise, the tag name is used. All other attributes and child elements, if
// any, become child nodes of the MAP. Attributes are mapped to VALUE nodes; child
// elements are mapped on their own accord. Example:
//
//
//
// This becomes a MAP named "Children" with two child members, both VALUE UNodes
// with the name/value pairs "type/link" and "inverse/Parents". Another example:
//
//
// 123
// 456
//
//
// This becomes an ARRAY named "add" with a child VALUE member for each
// element (based on rule 2).
private static UNode parseXMLElement(Element elem) {
assert elem != null;
// Get the element's content and attributes, if any.
String content = Utils.getElementText(elem);
NamedNodeMap attrMap = elem.getAttributes();
// Map the element's child elements, if any, into a UNode list. This also tells us
// if there are any child nodes.
List childUNodeList = new ArrayList();
boolean bDupNodeNames = parseXMLChildElems(elem, childUNodeList);
// Decide what the element becomes as documented above.
UNode result = null;
// Detect base64 encoding attribute and remove it.
String encoding = elem.getAttribute("encoding");
if (!Utils.isEmpty(encoding) && encoding.equalsIgnoreCase("base64")) {
attrMap.removeNamedItem("encoding");
content = Utils.base64ToString(content);
}
// Case 1): Stellar1
if (attrMap.getLength() == 0 && childUNodeList.size() == 0) {
// No attributes and no child elements. Use the tag name and the element
// content (which may be empty) as the node value.
result = createValueNode(elem.getTagName(), content);
// Case 2): lollapalooza
} else if (attrMap.getLength() == 1 &&
elem.getAttribute("name").length() > 0 &&
childUNodeList.size() == 0) {
// Only a "name" attribute and no child elements. Use attribute value as the
// name and content as the value.
result = createValueNode(elem.getAttribute("name"), content, elem.getTagName());
// Case 3):
} else if (attrMap.getLength() == 2 &&
elem.getAttribute("name").length() > 0 &&
elem.getAttribute("value").length() > 0 &&
childUNodeList.size() == 0) {
// Use values of "name" and "value" attributes. No content allowed.
assert content.length() == 0 : "Content is not allowed for 'name/value' element: " + content;
result = createValueNode(elem.getAttribute("name"), elem.getAttribute("value"), elem.getTagName());
// Case 4): All other cases generate a MAP or an ARRAY.
} else {
// We shouldn't have any content for these nodes.
assert content.length() == 0 : "Unexpected content for element '" +
elem.getTagName() + "': " + content;
// Use the "name" attribute value for the UNode name, if present. Otherwise,
// use the element name.
if (elem.getAttribute("name").length() > 0) {
if (bDupNodeNames) {
result = createArrayNode(elem.getAttribute("name"), elem.getTagName());
} else {
result = createMapNode(elem.getAttribute("name"), elem.getTagName());
}
attrMap.removeNamedItem("name");
} else {
if (bDupNodeNames) {
result = createArrayNode(elem.getTagName());
} else {
result = createMapNode(elem.getTagName());
}
}
// Map remaining attributes, if any, into child UNode objects and add all
// child UNodes to this node's list.
parseXMLAttributes(attrMap, childUNodeList);
result.addChildNodes(childUNodeList);
}
return result;
} // parseXMLElement
// Parse the given attribute map, creating a VALUE UNode for each attribute annd adding
// if to the given child node list.
private static void parseXMLAttributes(NamedNodeMap attrMap, List childUNodeList) {
for (int index = 0; index < attrMap.getLength(); index++) {
Attr attr = (Attr)attrMap.item(index);
UNode childNode = createValueNode(attr.getName(), attr.getValue(), true);
childUNodeList.add(childNode);
}
} // parseXMLAttributes
// Parse the given element's child elements, creating appropriate UNode objects for
// each one and storing them in the given list. Indicate if any duplicate node names
// are found while scanning.
private static boolean parseXMLChildElems(Element elem, List childUNodeList) {
assert elem != null;
assert childUNodeList != null;
// Scan for Element nodes (there could be Comment and other nodes).
boolean bDupNodeNames = false;
Set nodeNameSet = new HashSet();
NodeList nodeList = elem.getChildNodes();
for (int index = 0; index < nodeList.getLength(); index++) {
Node childNode = nodeList.item(index);
if (childNode instanceof Element) {
// Create the appropriate child UNode for this element.
UNode childUNode = parseXMLElement((Element)childNode);
childUNodeList.add(childUNode);
if (nodeNameSet.contains(childUNode.getName())) {
bDupNodeNames = true;
} else {
nodeNameSet.add(childUNode.getName());
}
}
}
return bDupNodeNames;
} // parseXMLChildElems
// Get the child nodes of this UNode that are VALUE nodes marked as attributes.
private void addXMLAttributes(Map attrMap) {
if (m_children != null) {
for (UNode childNode : m_children) {
// A child node must not contain a tag name to be considered an attribute.
if (childNode.m_type == NodeType.VALUE && childNode.m_bAttribute && Utils.isEmpty(childNode.m_tagName)) {
assert m_name != null && m_name.length() > 0;
attrMap.put(childNode.m_name, childNode.m_value);
}
}
}
} // addXMLAttributes
////////// Common private methods
// Add this node's toString() the given buffer, indented by the given count, and
// appended with a newline.
private void toStringTree(StringBuilder builder, int indent) {
for (int count = 0; count < indent; count++) {
builder.append(" ");
}
builder.append(this.toString());
builder.append("\n");
if (m_children != null) {
for (UNode childNode : m_children) {
childNode.toStringTree(builder, indent + 3);
}
}
} // toStringTree
} // class UNode