All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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: // //




© 2015 - 2025 Weber Informatics LLC | Privacy Policy