org.easyxml.xml.Element.java Source code

Java tutorial

Introduction

Here is the source code for org.easyxml.xml.Element.java

Source

/*
 * Created on May 15, 2015
 *
 * 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 org.easyxml.xml;

import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.easyxml.util.Utility;
import org.xml.sax.SAXException;

/**
 * 
 * Base class of XML element.
 * 
 * @author William JIANG
 * 
 *         15 May 2015
 * 
 * 
 * 
 * @version $Id$
 */

public class Element {

    // Default behavior of if show empty attribute when it is empty.

    public static final Boolean DefaultDisplayEmptyAttribute = true;

    // Default behavior of if show empty attribute when it is empty.

    public static final Boolean DefaultDisplayEmeptyElement = true;

    // Keywords

    public static final String DefaultElementPathSign = ">";

    public static final String Value = "Value";

    public static final String NewLine = "\n";

    public static final String ElementValueHolder = null;

    public static final String ClosingTagFormat = "</%s>";

    public static final String AppendInnerTextFormat = "%s %s";

    public static final String DefaultIndentHolder = "    ";

    public static final String DefaultListLeading = "{";

    public static final String DefaultListEnding = "}";

    public static final String DefaultListSeperator = ", ";

    public static final Boolean IgnoreLeadingSpace = true;

    public static final char[] EntityReferenceKeys = { '<', '>', '&', '\'', '\"' };

    public static final String[] EntityReferences = { "&lt;", "&gt;", "&amp;", "&apos;", "&quot;" };

    /**
     * 
     * Get the unique path of an Element.
     * 
     * @param element
     *            - Element under evaluation.
     * 
     * @return The path within the whole XML document.
     * 
     *         For example: supposing the common SOAP document (<SOAP:Envelope>
     *         as root, with <SOAP:Header> and <SOAP:Body>)
     * 
     *         contains one Element <Child> under <SOAP:Body>, and two
     *         <GrandChild> under 'Child', then
     * 
     *         For <Child> element, the result would be
     *         "SOAP:Envelope>SOAP:Body>Child";
     * 
     *         For both <GrandChild> element, the result would be
     *         "SOAP:Envelope>SOAP:Body>Child>GrandChild" with default output
     *         format.
     */

    public static String getElementPath(Element element) {

        return getElementPath(element, null);

    }

    /**
     * 
     * Get the unique path of an Element.
     * 
     * @param element
     *            - Element under evaluation.
     * 
     * @param refParent
     *            - Relative root for evaluation.
     * 
     * @return The path within the parent element of the whole XML document.
     * 
     *         For example: supposing the common SOAP document (<SOAP:Envelope>
     *         as root, with <SOAP:Header> and <SOAP:Body>)
     * 
     *         contains one Element <Child> under <SOAP:Body>, and two
     *         <GrandChild> under 'Child', then if 'refParent'
     * 
     *         is set to <SOAP:Body>
     * 
     *         For <Child> element, the result would be "Child";
     * 
     *         For both <GrandChild> element, the result would be
     *         "Child>GrandChild" with default output format.
     */

    public static String getElementPath(Element element, Element refParent) {

        StringBuilder sb = new StringBuilder(element.getName());

        Element directParent = element.getParent();

        while (directParent != null && directParent != refParent) {

            sb.insert(0, directParent.getName() + DefaultElementPathSign);

            directParent = directParent.getParent();

        }
        ;

        return sb.toString();

    }

    /**
     * 
     * Get the name of the element specified by the path.
     * 
     * @param path
     *            - The path within the parent element.
     * 
     * @return Name of the target element.
     */

    public static String elementNameOf(String path) {

        if (StringUtils.isBlank(path))

            return "";

        String[] elementNames = path.split(DefaultElementPathSign);

        return elementNames[elementNames.length - 1];

    }

    // Name or Tag of the element

    protected String name;

    // Keep the innerText value

    protected String value;

    // Container Element

    protected Element parent = null;

    // Map to keep its attributes

    protected Map<String, Attribute> attributes = null;

    // Map to keep its direct children elements

    protected Map<String, List<Element>> children = null;

    public String getName() {

        return name;

    }

    public void setName(String name) {

        this.name = name;

    }

    /**
     * 
     * Get the innerText of the element
     * 
     * @return innerText un-escaped
     */

    public String getValue() {

        return StringEscapeUtils.unescapeXml(value);

    }

    /**
     * 
     * Set the innerText of the element
     * 
     * @param value
     *            - New innerText value.
     */

    public void setValue(String value) {

        this.value = StringEscapeUtils.escapeXml10(StringUtils.trim(value));

    }

    /**
     * 
     * Append new Text Node to existing innerText with default format, it is
     * called to merge multiple text node as the innerText.
     * 
     * That is, if the value has been set to "oldValue", then
     * appendValue("newValue") would set it to "oldValue newValue".
     * 
     * @param value
     */

    public void appendValue(String value) {

        appendValue(value, AppendInnerTextFormat);

    }

    /**
     * 
     * Append new Text Node to existing innerText, it is called to merge
     * multiple text node as the innerText.
     * 
     * @param value
     *            - Text to be appended.
     * 
     * @param appendFormat
     *            - Format of how to append new text node to existing text node.
     * 
     *            The first '%s' denote the existing text, the second '%s' would
     *            be replaced with formatted value.
     */

    public void appendValue(String value, String appendFormat) {

        String formattedValue = StringEscapeUtils.escapeXml10(StringUtils.trim(value));

        if (this.value == null || this.value.length() == 0) {

            this.value = formattedValue;

        } else if (!StringUtils.isBlank(formattedValue)) {

            this.value = String.format(appendFormat, this.value, formattedValue);

        }

    }

    /**
     * 
     * Get the parent Element.
     * 
     * @return
     */

    public Element getParent() {

        return parent;

    }

    /**
     * 
     * Set the parent Element after detecting looping reference, and update the
     * Children map to keep reference of the new child element.
     * 
     * @param parent
     */

    public void setParent(Element parent) {

        // Check to prevent looping reference

        Element itsParent = parent.getParent();

        while (itsParent != null) {

            if (itsParent == this)

                throw new InvalidParameterException("The parent cannot be a descendant of this element!");

            itsParent = itsParent.getParent();

        }

        this.parent = parent;

        Element container = this;

        String path = this.name;

        while (container.getParent() != null) {

            container = container.getParent();

            Map<String, List<Element>> upperChildren = container.getChildren();

            if (upperChildren == null) {

                upperChildren = new LinkedHashMap<String, List<Element>>();

            }

            if (!upperChildren.containsKey(path)) {

                List<Element> newList = new ArrayList<Element>();

                newList.add(this);

                upperChildren.put(path, newList);

            } else {

                List<Element> upperList = upperChildren.get(path);

                if (!upperList.contains(this)) {

                    upperList.add(this);

                }

            }

            //Append this.children to parent.children with adjusted path
            if (this.children != null) {
                for (Map.Entry<String, List<Element>> entry : this.children.entrySet()) {
                    String childPath = String.format("%s%s%s", path, Element.DefaultElementPathSign,
                            entry.getKey());
                    if (!upperChildren.containsKey(childPath)) {
                        List<Element> newList = new ArrayList<Element>(entry.getValue());
                        upperChildren.put(childPath, newList);

                    } else {
                        List<Element> upperList = upperChildren.get(childPath);
                        upperList.addAll(entry.getValue());
                    }

                }

            }

            String containerName = container.getName();

            path = containerName + DefaultElementPathSign + path;

        }

    }

    public Map<String, Attribute> getAttributes() {

        return attributes;

    }

    public Map<String, List<Element>> getChildren() {

        return children;

    }

    /**
     * 
     * Constructor with Element tag name, its parent element and innerText.
     * 
     * @param name
     *            - Tag name of the element.
     * 
     * @param parent
     *            - Container of this element.
     * 
     * @param value
     *            - InnerText, notice that is would be escaped before stored to
     *            this.value.
     */

    public Element(String name, Element parent, String value) {

        if (StringUtils.isBlank(name)) {

            throw new InvalidParameterException("Name of an element cannot be null or blank!");

        }

        if (!StringUtils.containsNone(name, EntityReferenceKeys)) {

            throw new InvalidParameterException(
                    String.format("\"{s}\" cannot contain any of the 5 chars: \'<\', \'>\', \'&\', \'\'\', \'\"\'"

                            , name));

        }

        this.name = name;

        setValue(value);

        if (parent != null) {

            setParent(parent);

            // Update the Children map of the parent element after setting name

            this.parent.addChildElement(this);

        }

    }

    public Element(String name, Element parent) {

        this(name, parent, ElementValueHolder);

    }

    public Element(String name) {

        this(name, null, ElementValueHolder);

    }

    /**
     * 
     * Method to append one constructed Element as its direct child by updating
     * its Children Map and the child's parent.
     * 
     * @param child
     *            - Constructed Element.
     * 
     * @return This element for cascading processing.
     */

    public Element addChildElement(Element child) {

        // Do nothing if child is null

        if (child == null)
            return this;

        if (this.children == null) {

            this.children = new LinkedHashMap<String, List<Element>>();

        }

        String childName = child.getName();

        if (!this.children.containsKey(childName)) {

            List<Element> elements = new ArrayList<Element>();

            elements.add(child);

            this.children.put(childName, elements);

        }

        child.setParent(this);

        if (!this.children.get(childName).contains(child))

            this.children.get(childName).add(child);

        return this;

    }

    /**
     * 
     * Update the Children map to keep reference of the new child element.
     * 
     * @param childName
     */

    protected void updateChildrenMap(Element child) {

        String childName = child.getName();

        List<Element> elements = new ArrayList<Element>();

        this.children.put(childName, elements);

        Element container = this;

        String path = childName;

        while (container.getParent() != null) {

            String containerName = container.getName();

            path = containerName + DefaultElementPathSign + path;

            // Map<String, List<Element>> childrenOfParent =
            // container.getParent().getChildren();

            // if (childrenOfParent == null ||
            // !childrenOfParent.containsKey(containerName)) {

            // try {

            // throw new
            // InvalidAttributesException("The parent doesn't contain key of " +
            // containerName);

            // } catch (InvalidAttributesException e) {

            // e.printStackTrace();

            // continue;

            // }

            // }

            container = container.getParent();

            Map<String, List<Element>> upperChildren = container.getChildren();

            if (!upperChildren.containsKey(path)) {

                upperChildren.put(path, elements);

            } else {

                List<Element> upperList = upperChildren.get(path);

                if (!upperList.contains(child)) {

                    upperList.add(child);

                }

            }

        }

    }

    /**
     * 
     * Check to see if there is a direct child with the name specified.
     * 
     * Notice that since there is no operation of removing child element, for
     * simplicity, there is no checking of if the map is empty.
     * 
     * @param name
     *            - Name of the element under evaluation.
     * 
     * @return 'true' if there exist such direct children.
     */

    public Boolean containsElement(String name) {

        return (this.children != null && this.children.containsKey(name)) || name.length() == 0;

    }

    /**
     * 
     * Get all elements with the path specified.
     * 
     * Notice this path is not the absolute path of the target element.
     * 
     * @param path
     *            - The path uniquely identify the relative location of the
     *            element expected within the DOM tree.
     * 
     * @return
     * 
     *         null when path is null;
     * 
     *         this when path is an empty String;
     * 
     *         null if no such element exists
     * 
     *         a list of elements matched with the path.
     */

    public List<Element> getElementsOf(String path) {

        if (this.children == null)

            return null;

        else if (path.length() == 0) {

            List<Element> result = new ArrayList<Element>();

            result.add(this);

            return result;

        }

        else if (!children.containsKey(path))

            return null;

        return children.get(path);

    }

    /**
     * 
     * Method to evaluate the path by split the path to one path to locate the
     * element, and optional another part to locate its attribute.
     * 
     * @param path
     *            - The whole path of a Element or a Attribute.
     * 
     *            For example, "SOAP:Envelope>SOAP:Body>Child" denotes <Child>
     *            element/elements under <SOAP:Body>.
     * 
     *            "SOAP:Envelope>SOAP:Body>Child<Attr" denotes the "Attr"
     *            attribute of the above <Child> element/elements.
     * 
     * @return At most two segments, first for the element, optional second for
     *         the attribute.
     * 
     *         When the path denotes some elements, then only one segment would
     *         be returned.
     * 
     *         When the path denotes an attribute of some elements, then two
     *         segments would be returned.
     */

    protected String[] parsePath(String path) {

        if (StringUtils.isBlank(path))

            throw new InvalidParameterException("Blank path cannot be used to locate elements!");

        String[] segments = path.split(Attribute.DefaultAttributePathSign);

        int segmentLength = segments.length;

        if (segmentLength > 2)

            throw new InvalidParameterException(String.format(
                    "The path \'%s\' shall have at most one \'%s\' to denote the attribute of a kind of element."

                    , path, Attribute.DefaultAttributePathSign));

        return segments;

    }

    /**
     * 
     * Get the values of innerText/attribute/childElements/childAttributes
     * specified by 'path'.
     * 
     * @param path
     *            - The whole path of a Element or a Attribute.
     * 
     *            For example, "SOAP:Envelope>SOAP:Body>Child" denotes <Child>
     *            element/elements under <SOAP:Body>.
     * 
     *            "SOAP:Envelope>SOAP:Body>Child<Attr" denotes the "Attr"
     *            attribute of the above <Child> element/elements.
     * 
     * @return When path
     * 
     *         1) equals to "Value": then only the innerText of this element
     *         would be returned.
     * 
     *         2) equals to some existing attribute name: then only value of
     *         that attribute would be returned.
     * 
     *         3) is parsed as some elements, then their innerText would be
     *         returned as an array.
     * 
     *         4) is parsed as some attributes, then these attribute values
     *         would be returned as an array.
     * 
     *         5) Otherwise returns null.
     */

    public String[] getValuesOf(String path) {

        String[] values = null;

        if (path == Value) {

            // When path is "Value", then only the innerText of this element
            // would be returned.

            values = new String[1];

            values[0] = getValue();

            return values;

        } else if (attributes != null && attributes.containsKey(path)) {

            // If the path denotes one existing attribute of this element, then
            // only value of that attribute would be returned.

            values = new String[1];

            values[0] = getAttributeValue(path);

            return values;

        }

        try {

            String[] segments = parsePath(path);

            // Get the target elements or elements of the target attributes
            // first

            String elementPath = segments[0];

            List<Element> elements = getElementsOf(elementPath);

            if (elements == null)

                return null;

            int size = elements.size();

            values = new String[size];

            if (segments.length == 1) {

                // The path identify Element, thus return their innerText as an
                // array

                for (int i = 0; i < size; i++) {

                    values[i] = elements.get(i).getValue();

                }

            } else {

                // Otherwise, the path denotes some attributes, then return the
                // attributes values as an array

                String attributeName = segments[1];

                for (int i = 0; i < size; i++) {

                    values[i] = elements.get(i).getAttributeValue(attributeName);

                }

            }

        } catch (InvalidParameterException ex) {

            // If there is anything unexpected, return null

            ex.printStackTrace();

            return null;

        }

        return values;

    }

    /**
     * 
     * Set the values of innerText/attribute/childElements/childAttributes
     * specified by 'path'.
     * 
     * This would facilitate creation of a layered DOM tree conveniently.
     * 
     * For example, run setValuesOf("Child>GrandChild<Id", "1st", "2nd") of an
     * empty element would:
     * 
     * 1) Create a new <Child> element;
     * 
     * 2) Create TWO new <GrandChild> element under the newly created <Child>
     * element;
     * 
     * 3) First <GrandChild> element would have "Id" attribute of "1st", and
     * second <GrandChild> of "2nd".
     * 
     * If then call setValuesOf("Child>GrandChild<Id", "1", "2", "3"), no a new
     * <GrandChild> element would be created
     * 
     * and the attribute values of these <GrandChild> elements would be set to
     * "1", "2" and "3" respectively.
     * 
     * @param path
     *            - The whole path of a Element or a Attribute.
     * 
     *            For example, "SOAP:Envelope>SOAP:Body>Child" denotes <Child>
     *            element/elements under <SOAP:Body>.
     * 
     *            "SOAP:Envelope>SOAP:Body>Child<Attr" denotes the "Attr"
     *            attribute of the above <Child> element/elements.
     * 
     * @param values
     *            - The values to be set when 'path':
     * 
     *            1) equals to "Value": then set the new value of the innerText
     *            of this element.
     * 
     *            2) equals to some existing attribute name: then set the new
     *            value of that attribute.
     * 
     *            3) is parsed as some elements, then their innerText would be
     *            set.
     * 
     *            4) is parsed as some attributes, then these attribute values
     *            would be set.
     * 
     * @return 'true' if setting is successful, 'false' if failed.
     */

    public Boolean setValuesOf(String path, String... values) {

        try {

            if (path == Value) {

                if (values.length != 1)

                    throw new InvalidParameterException(
                            "Only one String value is expected to set the value of this element");

                // Change the innerText value of this element

                setValue(values[0]);

                return true;

            } else if (attributes != null && attributes.containsKey(path)) {

                if (values.length != 1)

                    throw new InvalidParameterException(
                            "Only one String value is expected to set the attribute value.");

                // Set the attribute value of this element

                setAttributeValue(path, values[0]);

                return true;

            }

            String[] segments = parsePath(path);

            String elementPath = segments[0];

            // Try to treat the path as a key to its direct children elements
            // first

            List<Element> elements = getElementsOf(elementPath);

            Element newElement = null;

            if (elements == null) {

                // 'path' is not a key for its own direct children, thus split
                // it with '>'

                // For instance, "Child>GrandChild" would be split to {"Child",
                // "GrandChild"} then this element would:

                // 1) search "Child" as its direct children elements;

                // 2) if no <Child> element exists, create one;

                // 3) search "Child>GrandChild" as its direct children elements;

                // 4) if no exists, create new <GrandChild> as a new child of
                // <Child> element.

                String[] containers = elementPath.split(DefaultElementPathSign);

                String next = "";

                Element last = this;

                for (int i = 0; i < containers.length; i++) {

                    next += containers[i];

                    if (getElementsOf(next) == null) {

                        newElement = new Element(containers[i], last);

                    }

                    last = getElementsOf(next).get(0);

                    next += DefaultElementPathSign;

                }

                elements = getElementsOf(elementPath);

            }

            int size = values.length;

            if (size > elements.size()) {

                // In case the existing elements are less than values.length,
                // create new elements under the last container

                int lastChildMark = elementPath.lastIndexOf(DefaultElementPathSign);

                String lastContainerPath = lastChildMark == -1 ? "" : elementPath.substring(0, lastChildMark);

                String lastElementName = elementPath.substring(lastChildMark + 1);

                Element firstContainer = getElementsOf(lastContainerPath).get(0);

                for (int i = elements.size(); i < size; i++) {

                    newElement = new Element(lastElementName, firstContainer);

                }

                elements = getElementsOf(elementPath);

            }

            if (segments.length == 1) {

                // The path identify Element, thus set their innerText

                for (int i = 0; i < size; i++) {

                    elements.get(i).setValue(values[i]);

                }

            } else {

                // The path identify Attribute, thus set values of these
                // attribute accordingly

                String attributeName = segments[1];

                for (int i = 0; i < size; i++) {

                    elements.get(i).setAttributeValue(attributeName, values[i]);

                }

            }

            return true;

        } catch (InvalidParameterException ex) {

            ex.printStackTrace();

            return false;

        }

    }

    /**
     * 
     * Method to add a new Attribute to this element.
     * 
     * @param name
     *            - Name of the new Attribute
     * 
     * @param value
     *            - Value of the new Attribute
     * 
     * @return This element for cascading processing.
     * 
     * @throws SAXException
     */

    public Element addAttribute(String name, String value) throws SAXException {

        if (this.attributes == null)

            this.attributes = new LinkedHashMap<String, Attribute>();

        // The name of an attribute cannot be duplicated according to XML
        // specification

        if (this.attributes.containsKey(name))

            throw new SAXException("Attribute name must be unique within an Element.");

        if (value != null)

            this.attributes.put(name, new Attribute(this, name, value));

        return this;

    }

    /**
     * 
     * Method to get the value of an attribute specified by the attributeName.
     * 
     * @param attributeName
     *            - Name of the concerned attribute.
     * 
     * @return null if there is no such attribute, or its value when it exists.
     */

    public String getAttributeValue(String attributeName) {

        if (this.attributes != null && this.attributes.containsKey(attributeName)) {

            return this.attributes.get(attributeName).getValue();

        }

        return null;

    }

    /**
     * 
     * Set the attribute value.
     * 
     * @param attributeName
     *            - Name of the concerned attribute.
     * 
     * @param newValue
     *            - New value of the attribute.
     * 
     * @return
     * 
     *         'false' if there is any confliction with XML specification.
     * 
     *         'true' set attribute value successfully.
     */

    public Boolean setAttributeValue(String attributeName, String newValue) {

        if (this.attributes != null && this.attributes.containsKey(attributeName)) {

            this.attributes.get(attributeName).setValue(newValue);

            return true;

        }

        try {

            this.addAttribute(attributeName, newValue);

            return true;

        } catch (SAXException e) {

            e.printStackTrace();

            return false;

        }

    }

    /**
     * 
     * Get the level of this element in the DOM tree. 0 for the root element.
     * 
     * @return
     */

    public int getLevel() {

        if (this.parent == null)

            return 0;

        return this.parent.getLevel() + 1;

    }

    /**
     * 
     * Evaluate if this element contain any innerText, non-empty attribute or
     * children.
     * 
     * @return 'true' for empty.
     */

    public Boolean isEmpty() {

        // If this element has some non-blank innerText, returns false.

        if (!StringUtils.isBlank(getValue()))

            return false;

        // If this element has some non-blank attribute, returns false.

        if (attributes != null && !attributes.isEmpty()) {

            Iterator<Entry<String, Attribute>> iterator = attributes.entrySet().iterator();

            while (iterator.hasNext()) {

                Attribute attribute = iterator.next().getValue();

                if (!attribute.isEmpty())

                    return false;

            }

        }

        // If this element has some non-blank element, returns false.

        if (children != null && !children.isEmpty()) {

            Iterator<Entry<String, List<Element>>> iterator2 = children.entrySet().iterator();

            while (iterator2.hasNext()) {

                List<Element> elements = iterator2.next().getValue();

                for (int i = 0; i < elements.size(); i++) {

                    if (!elements.get(i).isEmpty())

                        return false;

                }

            }

        }

        // Otherwise, treat this element as empty.

        return true;

    }

    /**
     * 
     * Get path relative to the root.
     * 
     * @return Path as a formatted string.
     */

    public String getPath() {

        return getElementPath(this);

    }

    private String getAttributeString() {

        return allAttributesAsString(DefaultDisplayEmptyAttribute);

    }

    private String allAttributesAsString(Boolean outputEmptyAttribute) {

        Iterator<Entry<String, Attribute>> iterator = attributes.entrySet().iterator();

        StringBuilder sb = new StringBuilder();

        while (iterator.hasNext()) {

            Attribute attribute = iterator.next().getValue();

            if (outputEmptyAttribute || !attribute.isEmpty()) {

                sb.append(" " + attribute);

            }

        }

        return sb.toString();

    }

    @Override
    public String toString() {

        return toString(0, true, true);

    }

    /**
     * 
     * Method to display this element as a well-formatted String.
     * 
     * @param indent
     *            - Indent count for this element.
     * 
     * @param outputEmptyAttribute
     *            - Specify if empty attribute shall be displayed.
     * 
     * @param outputEmptyElement
     *            - Specify if empty children element shall be displayed.
     * 
     * @return String form of this XML element.
     */

    public String toString(int indent, Boolean outputEmptyAttribute, Boolean outputEmptyElement) {

        // If this is an empty element and no need to output empty element,
        // return "" immediately.

        if (!outputEmptyElement && this.isEmpty())

            return "";

        String indentString = StringUtils.repeat(DefaultIndentHolder, indent);

        StringBuilder sb = new StringBuilder(indentString + "<" + name);

        // If this element has only attributes

        if ((children == null || children.size() == 0) && (value == null || value.trim().length() == 0)) {

            // Compose the empty element tag

            if (attributes != null) {

                sb.append(allAttributesAsString(outputEmptyAttribute));

            }

            sb.append("/>");

        } else {

            // Compose the opening tag

            if (attributes != null) {

                sb.append(allAttributesAsString(outputEmptyAttribute));

            }

            sb.append('>');

            // Include the children elements by order

            if (children != null && !children.isEmpty()) {

                sb.append(NewLine);

                Iterator<Entry<String, List<Element>>> iterator2 = children.entrySet().iterator();

                while (iterator2.hasNext()) {

                    Entry<String, List<Element>> next = iterator2.next();

                    String path = next.getKey();

                    if (path.contains(DefaultElementPathSign))

                        continue;

                    List<Element> elements = next.getValue();

                    for (int i = 0; i < elements.size(); i++) {

                        Element element = elements.get(i);

                        if (outputEmptyElement || !elements.get(i).isEmpty()) {

                            sb.append(element.toString(indent + 1, outputEmptyAttribute, outputEmptyElement)
                                    + NewLine);

                        }

                    }

                }

            }

            // Include the inner text of this element

            if (StringUtils.isNotBlank(value)) {

                if (children != null && !children.isEmpty()) {

                    sb.append(IgnoreLeadingSpace ? indentString + DefaultIndentHolder + value + NewLine
                            : value + NewLine);

                } else {

                    sb.append(value);

                }

            }

            // Include the closing tag

            if (children != null && !children.isEmpty()) {

                sb.append(indentString);

            }

            sb.append(String.format(ClosingTagFormat, name));

        }

        return sb.toString();

    }

    protected String getJsonStringOf(List<Element> objectElements, Map<String, String> pathToKeys) {

        int elementSize = objectElements.size();

        String[] elementJSONs = new String[elementSize];

        for (int i = 0; i < elementSize; i++) {

            Element element = objectElements.get(i);

            Map<String, String> elementMap = new LinkedHashMap<>();

            for (Map.Entry<String, String> entry : pathToKeys.entrySet()) {

                String relativePath = entry.getKey();

                String displayName = entry.getValue();

                if (relativePath.equals(Value)) {

                    elementMap.put(displayName, this.getValue());

                } else if (relativePath.length() == 0) {

                    elementMap.put(relativePath, displayName);

                } else {

                    String[] pathValues = element.getValuesOf(relativePath);

                    if (pathValues == null)

                        continue;

                    String theValue = pathValues.length == 1 ? pathValues[0] : Utility.toJSON(pathValues);

                    elementMap.put(displayName, theValue);

                }

            }

            if (!elementMap.containsKey("")) {

                elementMap.put("", element.getName());

            }

            String elementValue = Utility.toJSON(elementMap);

            elementJSONs[i] = elementValue;

        }

        String json = String.format("{%s}", StringUtils.join(elementJSONs, ",\n"));

        return json;

    }

    public String toJSON() {

        return toJSON(0);

    }

    public String toJSON(int indent) {

        String indentString = StringUtils.repeat(DefaultIndentHolder, indent);

        StringBuilder sb = new StringBuilder();

        sb.append(String.format("%s\"%s\":", indentString, this.getName()));

        String thisPath = this.getPath();

        // If there is valid text node, return it immediately

        if (!StringUtils.isBlank(this.value)) {

            sb.append(String.format("\"%s\"", this.getValue()));

            return sb.toString();

        }

        // Output as an object

        sb.append("{");

        // Otherwise, consider both the attributes and its children elements for
        // output

        if (this.attributes != null) {

            String attrIndent = StringUtils.repeat(DefaultIndentHolder, indent + 1);

            for (Map.Entry<String, Attribute> entry : this.attributes.entrySet()) {

                String attrName = entry.getKey();

                String attrValue = this.getAttributeValue(attrName);

                sb.append(String.format("\n%s\"%s\": \"%s\",", attrIndent, attrName, attrValue));

            }

        }

        if (this.children != null) {

            List<String> firstLevelPathes = new ArrayList<String>();

            for (Map.Entry<String, List<Element>> entry : this.children.entrySet()) {

                String originalPath = entry.getKey();

                int firstSignPos = StringUtils.indexOfAny(originalPath, '>', '<');

                String elementName = elementNameOf(originalPath);

                if (firstSignPos == -1) {

                    firstLevelPathes.add(originalPath);

                }

            }

            for (String firstLevelPath : firstLevelPathes) {

                List<Element> elements = this.getElementsOf(firstLevelPath);

                // Check to see if elements could be converted to JSON array

                if (elements.size() > 1) {

                    Boolean asArray = true;

                    for (Element e : elements) {

                        if (!StringUtils.isBlank(e.getValue())) {

                            asArray = false;

                            break;

                        }

                    }

                    // If they could be treated as array

                    if (asArray) {

                        String jsonKey = String.format("\"%s\":", elements.get(0).getName());

                        sb.append(String.format("\n%s%s[\n",

                                StringUtils.repeat(DefaultIndentHolder, indent + 1), jsonKey));

                        for (Element e : elements) {

                            sb.append(String.format("%s,\n", e.toJSON(indent + 2).replace(jsonKey, "")));

                        }

                        // Remove the last ','

                        sb.setLength(sb.length() - 2);

                        sb.append(String.format("\n%s],", StringUtils.repeat(DefaultIndentHolder, indent + 1)));

                        continue;

                    }

                }

                // Otherwise, output the elements one by one

                for (Element e : elements) {

                    sb.append(String.format("\n%s,", e.toJSON(indent + 1)));

                }

            }

            //

            // //Remove the last ',' and append '}'

            // sb.setLength(sb.length()-1);

            // sb.append("\n" + indentString + "}");

        }

        if (sb.toString().endsWith(",")) {

            sb.setLength(sb.toString().length() - 1);

            sb.append(String.format("\n%s}", indentString));

        }

        return sb.toString();

    }

}