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

com.landawn.abacus.util.XmlUtil Maven / Gradle / Ivy

Go to download

A general programming library in Java/Android. It's easy to learn and simple to use with concise and powerful APIs.

The newest version!
/*
 * Copyright (C) 2015 HaiYang Li
 *
 * 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.landawn.abacus.util;

import java.beans.XMLDecoder;
import java.beans.XMLEncoder;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.stream.StreamFilter;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import com.landawn.abacus.annotation.MayReturnNull;
import com.landawn.abacus.exception.ParseException;
import com.landawn.abacus.exception.UncheckedIOException;
import com.landawn.abacus.logging.Logger;
import com.landawn.abacus.logging.LoggerFactory;
import com.landawn.abacus.parser.XMLConstants;
import com.landawn.abacus.type.Type;

import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.Marshaller;
import jakarta.xml.bind.Unmarshaller;

public final class XmlUtil {

    protected static final Logger logger = LoggerFactory.getLogger(XmlUtil.class);

    // ... it has to be big enough to make it's safety to add element to
    // ArrayBlockingQueue.
    static final String NAME = "name";

    static final String TYPE = "type";

    private static final int POOL_SIZE = 1000;

    // ...
    private static final SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();

    private static final Queue saxParserPool = new ArrayBlockingQueue<>(POOL_SIZE);

    // ...
    private static final DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();

    private static final Queue contentDocBuilderPool = new ArrayBlockingQueue<>(POOL_SIZE);

    private static final XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();

    static {
        try {
            if (!Class.forName("com.ctc.wstx.stax.WstxInputFactory").isAssignableFrom(xmlInputFactory.getClass()) && logger.isWarnEnabled()) {
                logger.warn("It's recommended to use woodstox: https://github.com/FasterXML/woodstox");
            }
        } catch (final Throwable e) {
            if (logger.isWarnEnabled()) {
                logger.warn("It's recommended to use woodstox: https://github.com/FasterXML/woodstox");
            }
        }
    }

    // private static final Queue xmlInputPool = new ArrayBlockingQueue<>(POOL_SIZE);
    private static final XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newInstance();
    // private static final Queue xmlOutputPool = new ArrayBlockingQueue<>(POOL_SIZE);

    // ...
    private static final TransformerFactory transferFactory = TransformerFactory.newInstance();
    // private static final Queue xmlTransferPool = new ArrayBlockingQueue<>(POOL_SIZE);

    // ...
    private static final Map pathJaxbContextPool = new ConcurrentHashMap<>(POOL_SIZE);

    private static final Map, JAXBContext> classJaxbContextPool = new ConcurrentHashMap<>(POOL_SIZE);

    private static final Map nodeTypePool = new HashMap<>();

    static {
        nodeTypePool.put(XMLConstants.ARRAY, NodeType.ARRAY);
        nodeTypePool.put(XMLConstants.LIST, NodeType.COLLECTION);
        nodeTypePool.put(XMLConstants.E, NodeType.ELEMENT);
        nodeTypePool.put(XMLConstants.MAP, NodeType.MAP);
        nodeTypePool.put(XMLConstants.ENTRY, NodeType.ENTRY);
        nodeTypePool.put(XMLConstants.KEY, NodeType.KEY);
        nodeTypePool.put(XMLConstants.VALUE, NodeType.VALUE);
    }

    private XmlUtil() {
        // singleton.
    }

    /**
     * Marshals the given JAXB bean into an XML string.
     * {@link Marshaller#marshal(Object, java.io.Writer)} is called.
     *
     * @param jaxbBean The JAXB bean to be marshaled.
     * @return The XML string representation of the JAXB bean.
     * @see JAXBContext#newInstance(Class...)
     * @see Marshaller#marshal(Object, java.io.OutputStream)
     */
    public static String marshal(final Object jaxbBean) {
        final Class cls = jaxbBean.getClass();
        JAXBContext jc = classJaxbContextPool.get(cls);
        final ByteArrayOutputStream writer = Objectory.createByteArrayOutputStream();

        try {
            if (jc == null) {
                jc = JAXBContext.newInstance(cls);
                classJaxbContextPool.put(cls, jc);
            }

            final Marshaller marshaller = jc.createMarshaller();
            marshaller.marshal(jaxbBean, writer);
            writer.flush();

            return writer.toString();
        } catch (final JAXBException e) {
            throw ExceptionUtil.toRuntimeException(e, true);
        } catch (final IOException e) {
            throw new UncheckedIOException(e);
        } finally {
            Objectory.recycle(writer);
        }
    }

    /**
     * Unmarshal the given XML string into an object of the specified class.
     * {@link  Unmarshaller#unmarshal(Reader)} is called.
     *
     * @param  The type of the object to be returned.
     * @param cls The class of the object to be returned.
     * @param xml The XML string to be unmarshalled.
     * @return The unmarshalled object of the specified class.
     * @see JAXBContext#newInstance(Class...)
     * @see Unmarshaller#unmarshal(Reader)
     */
    @SuppressWarnings("unchecked")
    public static  T unmarshal(final Class cls, final String xml) {
        JAXBContext jc = classJaxbContextPool.get(cls);

        try {
            if (jc == null) {
                jc = JAXBContext.newInstance(cls);
                classJaxbContextPool.put(cls, jc);
            }

            final Unmarshaller unmarshaller = jc.createUnmarshaller();
            final StringReader reader = new StringReader(xml);

            return (T) unmarshaller.unmarshal(reader);
        } catch (final JAXBException e) {
            throw ExceptionUtil.toRuntimeException(e, true);
        }
    }

    /**
     * Creates a JAXB Marshaller for the given context path.
     *
     * @param contextPath The context path for which to create the Marshaller.
     * @return The created Marshaller.
     * @see JAXBContext#newInstance(String)
     * @see JAXBContext#createMarshaller()
     */
    public static Marshaller createMarshaller(final String contextPath) {
        JAXBContext jc = pathJaxbContextPool.get(contextPath);

        try {
            if (jc == null) {
                jc = JAXBContext.newInstance(contextPath);
                pathJaxbContextPool.put(contextPath, jc);
            }

            return jc.createMarshaller();
        } catch (final JAXBException e) {
            throw ExceptionUtil.toRuntimeException(e, true);
        }
    }

    /**
     * Creates a JAXB Marshaller for the given class.
     *
     * @param cls The class for which to create the Marshaller.
     * @return The created Marshaller.
     * @see JAXBContext#newInstance(Class...)
     * @see JAXBContext#createMarshaller()
     */
    public static Marshaller createMarshaller(final Class cls) {
        JAXBContext jc = classJaxbContextPool.get(cls);

        try {
            if (jc == null) {
                jc = JAXBContext.newInstance(cls);
                classJaxbContextPool.put(cls, jc);
            }

            return jc.createMarshaller();
        } catch (final JAXBException e) {
            throw ExceptionUtil.toRuntimeException(e, true);
        }
    }

    /**
     * Creates a JAXB Unmarshaller for the given context path.
     *
     * @param contextPath The context path for which to create the Unmarshaller.
     * @return The created Unmarshaller.
     * @see JAXBContext#newInstance(String)
     * @see JAXBContext#createUnmarshaller()
     */
    public static Unmarshaller createUnmarshaller(final String contextPath) {
        JAXBContext jc = pathJaxbContextPool.get(contextPath);

        try {
            if (jc == null) {
                jc = JAXBContext.newInstance(contextPath);
                pathJaxbContextPool.put(contextPath, jc);
            }

            return jc.createUnmarshaller();
        } catch (final JAXBException e) {
            throw ExceptionUtil.toRuntimeException(e, true);
        }
    }

    /**
     * Creates a JAXB Unmarshaller for the given class.
     *
     * @param cls The class for which to create the Unmarshaller.
     * @return The created Unmarshaller.
     * @see JAXBContext#newInstance(Class...)
     * @see JAXBContext#createUnmarshaller()
     */
    public static Unmarshaller createUnmarshaller(final Class cls) {
        JAXBContext jc = classJaxbContextPool.get(cls);

        try {
            if (jc == null) {
                jc = JAXBContext.newInstance(cls);
                classJaxbContextPool.put(cls, jc);
            }

            return jc.createUnmarshaller();
        } catch (final JAXBException e) {
            throw ExceptionUtil.toRuntimeException(e, true);
        }
    }

    /**
     * Creates a new instance of {@code DocumentBuilder}.
     *
     * @return The filtered XMLStreamReader.
     * @see DocumentBuilderFactory#newDocumentBuilder()
     */
    public static DocumentBuilder createDOMParser() {
        synchronized (docBuilderFactory) {
            try {
                return docBuilderFactory.newDocumentBuilder();
            } catch (final ParserConfigurationException e) {
                throw ExceptionUtil.toRuntimeException(e, true);
            }
        }
    }

    /**
     * Creates a new instance of {@code DocumentBuilder} with the specified configuration.
     *
     * @param ignoreComments Whether to ignore comments in the XML.
     * @param ignoringElementContentWhitespace Whether to ignore whitespace in element content.
     * @return A new instance of {@code DocumentBuilder} with the specified configuration.
     * @see DocumentBuilderFactory#newDocumentBuilder()
     */
    public static DocumentBuilder createDOMParser(final boolean ignoreComments, final boolean ignoringElementContentWhitespace) {
        DocumentBuilder documentBuilder = null;

        synchronized (docBuilderFactory) {
            try {
                final boolean orgIgnoreComments = docBuilderFactory.isIgnoringComments();
                final boolean orgIgnoringElementContentWhitespace = docBuilderFactory.isIgnoringElementContentWhitespace();

                docBuilderFactory.setIgnoringComments(ignoreComments);
                docBuilderFactory.setIgnoringElementContentWhitespace(ignoringElementContentWhitespace);

                documentBuilder = docBuilderFactory.newDocumentBuilder();

                docBuilderFactory.setIgnoringComments(orgIgnoreComments);
                docBuilderFactory.setIgnoringElementContentWhitespace(orgIgnoringElementContentWhitespace);
            } catch (final ParserConfigurationException e) {
                throw ExceptionUtil.toRuntimeException(e, true);
            }
        }

        return documentBuilder;
    }

    /**
     * Creates a new instance of {@code DocumentBuilder} for parsing content.
     * Call {@code recycleContentParser} to reuse the instance.
     *
     * @return A new instance of {@code DocumentBuilder}.
     */
    public static DocumentBuilder createContentParser() {
        synchronized (contentDocBuilderPool) {
            DocumentBuilder documentBuilder = contentDocBuilderPool.poll();

            try {
                if (documentBuilder == null) {
                    final boolean orgIgnoreComments = docBuilderFactory.isIgnoringComments();
                    final boolean orgIgnoringElementContentWhitespace = docBuilderFactory.isIgnoringElementContentWhitespace();

                    if (!orgIgnoreComments) {
                        docBuilderFactory.setIgnoringComments(true);
                    }

                    if (!orgIgnoringElementContentWhitespace) {
                        docBuilderFactory.setIgnoringElementContentWhitespace(true);
                    }

                    documentBuilder = docBuilderFactory.newDocumentBuilder();

                    docBuilderFactory.setIgnoringComments(orgIgnoreComments);
                    docBuilderFactory.setIgnoringElementContentWhitespace(orgIgnoringElementContentWhitespace);
                }
            } catch (final ParserConfigurationException e) {
                throw ExceptionUtil.toRuntimeException(e, true);
            }

            return documentBuilder;
        }
    }

    /**
     * Recycles the given DocumentBuilder instance by resetting it and adding it back to the pool.
     *
     * @param docBuilder The DocumentBuilder instance to be recycled.
     */
    public static void recycleContentParser(final DocumentBuilder docBuilder) {
        if (docBuilder == null) {
            return;
        }

        synchronized (contentDocBuilderPool) {
            if (contentDocBuilderPool.size() < POOL_SIZE) {
                docBuilder.reset();
                contentDocBuilderPool.add(docBuilder);
            }
        }
    }

    /**
     * Creates a new instance of {@code SAXParser}.
     * Call {@code recycleSAXParser} to reuse the instance.
     *
     * @return A new instance of {@code SAXParser}.
     * @see SAXParserFactory#newSAXParser()
     */
    public static SAXParser createSAXParser() {
        synchronized (saxParserPool) {
            SAXParser saxParser = saxParserPool.poll();

            if (saxParser == null) {
                try {
                    saxParser = saxParserFactory.newSAXParser();
                } catch (final ParserConfigurationException e) {
                    throw ExceptionUtil.toRuntimeException(e, true);
                } catch (final SAXException e) {
                    throw new ParseException(e);
                }
            }

            return saxParser;
        }
    }

    /**
     * Recycles the given SAXParser instance by resetting it and adding it back to the pool.
     *
     * @param saxParser The SAXParser instance to be recycled.
     */
    public static void recycleSAXParser(final SAXParser saxParser) {
        if (saxParser == null) {
            return;
        }

        synchronized (saxParserPool) {
            if (saxParserPool.size() < POOL_SIZE) {
                saxParser.reset();
                saxParserPool.add(saxParser);
            }
        }
    }

    /**
     * Creates an XMLStreamReader from the given Reader source.
     *
     * @param source The Reader source from which to create the XMLStreamReader.
     * @return The created XMLStreamReader.
     * @throws RuntimeException if an XMLStreamException occurs.
     * @see XMLInputFactory#createXMLStreamReader(Reader)
     */
    public static XMLStreamReader createXMLStreamReader(final Reader source) {
        try {
            return xmlInputFactory.createXMLStreamReader(source);
        } catch (final XMLStreamException e) {
            throw ExceptionUtil.toRuntimeException(e, true);
        }
    }

    /**
     * Creates an XMLStreamReader from the given InputStream source.
     *
     * @param source The InputStream source from which to create the XMLStreamReader.
     * @return The created XMLStreamReader.
     * @throws RuntimeException if an XMLStreamException occurs.
     * @see XMLInputFactory#createXMLStreamReader(InputStream)
     */
    public static XMLStreamReader createXMLStreamReader(final InputStream source) {
        try {
            return xmlInputFactory.createXMLStreamReader(source);
        } catch (final XMLStreamException e) {
            throw ExceptionUtil.toRuntimeException(e, true);
        }
    }

    /**
     * Creates an XMLStreamReader from the given InputStream source with the specified encoding.
     *
     * @param source The InputStream source from which to create the XMLStreamReader.
     * @param encoding The character encoding to be used.
     * @return The created XMLStreamReader.
     * @throws RuntimeException if an XMLStreamException occurs.
     * @see XMLInputFactory#createXMLStreamReader(InputStream, String)
     */
    public static XMLStreamReader createXMLStreamReader(final InputStream source, final String encoding) {
        try {
            return xmlInputFactory.createXMLStreamReader(source, encoding);
        } catch (final XMLStreamException e) {
            throw ExceptionUtil.toRuntimeException(e, true);
        }
    }

    /**
     * Creates a filtered XMLStreamReader from the given source XMLStreamReader and StreamFilter.
     *
     * @param source The source XMLStreamReader to be filtered.
     * @param filter The StreamFilter to apply to the source.
     * @return The filtered XMLStreamReader.
     * @throws RuntimeException if an XMLStreamException occurs.
     * @see XMLInputFactory#createFilteredReader(XMLStreamReader, StreamFilter)
     */
    public static XMLStreamReader createFilteredStreamReader(final XMLStreamReader source, final StreamFilter filter) {
        try {
            return xmlInputFactory.createFilteredReader(source, filter);
        } catch (final XMLStreamException e) {
            throw ExceptionUtil.toRuntimeException(e, true);
        }
    }

    /**
     * Creates an XMLStreamWriter from the given Writer output.
     *
     * @param output The Writer output to which the XMLStreamWriter will write.
     * @return The created XMLStreamWriter.
     * @throws RuntimeException if an XMLStreamException occurs.
     * @see XMLOutputFactory#createXMLStreamWriter(Writer)
     */
    public static XMLStreamWriter createXMLStreamWriter(final Writer output) {
        try {
            return xmlOutputFactory.createXMLStreamWriter(output);
        } catch (final XMLStreamException e) {
            throw ExceptionUtil.toRuntimeException(e, true);
        }
    }

    /**
     * Creates an XMLStreamWriter from the given OutputStream.
     *
     * @param output The OutputStream to which the XMLStreamWriter will write.
     * @return The created XMLStreamWriter.
     * @throws RuntimeException if an XMLStreamException occurs.
     * @see XMLOutputFactory#createXMLStreamWriter(OutputStream)
     */
    public static XMLStreamWriter createXMLStreamWriter(final OutputStream output) {
        try {
            return xmlOutputFactory.createXMLStreamWriter(output);
        } catch (final XMLStreamException e) {
            throw ExceptionUtil.toRuntimeException(e, true);
        }
    }

    /**
     * Creates an XMLStreamWriter from the given OutputStream with the specified encoding.
     *
     * @param output The OutputStream to which the XMLStreamWriter will write.
     * @param encoding The character encoding to be used.
     * @return The created XMLStreamWriter.
     * @throws RuntimeException if an XMLStreamException occurs.
     * @see XMLOutputFactory#createXMLStreamWriter(OutputStream, String)
     */
    public static XMLStreamWriter createXMLStreamWriter(final OutputStream output, final String encoding) {
        try {
            return xmlOutputFactory.createXMLStreamWriter(output, encoding);
        } catch (final XMLStreamException e) {
            throw ExceptionUtil.toRuntimeException(e, true);
        }
    }

    /**
     * Creates a new instance of Transformer.
     *
     * @return A new instance of Transformer.
     * @throws RuntimeException if a TransformerConfigurationException occurs.
     * @see TransformerFactory#newTransformer()
     */
    public static Transformer createXMLTransformer() {
        try {
            return transferFactory.newTransformer();
        } catch (final TransformerConfigurationException e) {
            throw ExceptionUtil.toRuntimeException(e, true);
        }
    }

    /**
     * Transforms the given XML Document to the specified output file.
     *
     * @param source The XML Document to be transformed.
     * @param output The output file where the transformed XML will be written.
     * @throws UncheckedIOException if an IOException occurs during file operations.
     * @see Transformer#transform(Source, Result)
     */
    public static void transform(final Document source, File output) {
        output = Configuration.formatPath(output);

        Writer writer = null;

        try {
            IOUtil.createNewFileIfNotExists(output);

            writer = IOUtil.newFileWriter(output);

            transform(source, writer);

            writer.flush();
        } catch (final IOException e) {
            throw new UncheckedIOException(e);
        } finally {
            IOUtil.close(writer);
        }
    }

    /**
     * Transforms the given XML Document to the specified OutputStream.
     *
     * @param source The XML Document to be transformed.
     * @param output The OutputStream where the transformed XML will be written.
     * @throws RuntimeException if a TransformerException occurs.
     * @see Transformer#transform(Source, Result)
     */
    public static void transform(final Document source, final OutputStream output) {
        // Prepare the DOM document for writing
        final Source domSource = new DOMSource(source);

        final Result result = new StreamResult(output);

        try {
            createXMLTransformer().transform(domSource, result);
        } catch (final TransformerException e) {
            throw ExceptionUtil.toRuntimeException(e, true);
        }
    }

    /**
     * Transforms the given XML Document to the specified Writer.
     *
     * @param source The XML Document to be transformed.
     * @param output The Writer where the transformed XML will be written.
     * @throws RuntimeException if a TransformerException occurs.
     * @see Transformer#transform(Source, Result)
     */
    public static void transform(final Document source, final Writer output) {
        // Prepare the DOM document for writing
        final Source domSource = new DOMSource(source);

        final Result result = new StreamResult(output);

        try {
            createXMLTransformer().transform(domSource, result);
        } catch (final TransformerException e) {
            throw ExceptionUtil.toRuntimeException(e, true);
        }
    }

    /**
     * Encodes the given bean object into an XML string.
     *
     * @param bean The object to be encoded into XML.
     * @return The XML string representation of the given object.
     * @see XMLEncoder#writeObject(Object)
     */
    public static String xmlEncode(final Object bean) {
        final ByteArrayOutputStream os = Objectory.createByteArrayOutputStream();

        try (XMLEncoder xmlEncoder = new XMLEncoder(os)) {
            xmlEncoder.writeObject(bean);
            xmlEncoder.flush();
        }

        final String result = os.toString();
        Objectory.recycle(os);

        return result;
    }

    /**
     * Decodes the given XML string into an object.
     *
     * @param  The type of the object to be returned.
     * @param xml The XML string to be decoded.
     * @return The decoded object.
     * @see XMLDecoder#readObject()
     */
    public static  T xmlDecode(final String xml) {
        final InputStream is = new ByteArrayInputStream(xml.getBytes(Charsets.UTF_8));
        try (XMLDecoder xmlDecoder = new XMLDecoder(is)) {
            return (T) xmlDecoder.readObject();
        }
    }

    /**
     * Gets the elements by tag name.
     *
     * @param node The parent element to search within.
     * @param tagName The tag name of the elements to find.
     * @return A list of elements with the specified tag name that are direct children of the given node.
     */
    public static List getElementsByTagName(final Element node, final String tagName) {
        final List result = new ArrayList<>();
        final NodeList nodeList = node.getElementsByTagName(tagName);

        for (int i = 0; i < nodeList.getLength(); i++) {
            if (nodeList.item(i).getParentNode().isSameNode(node)) {
                result.add((Element) nodeList.item(i));
            }
        }

        return result;
    }

    /**
     * Gets the nodes by name.
     *
     * @param node The parent node to search within.
     * @param nodeName The name of the nodes to find.
     * @return A list of nodes with the specified name.
     */
    public static List getNodesByName(final Node node, final String nodeName) {
        final List nodes = new ArrayList<>();

        getNodesByName(node, nodeName, nodes);

        return nodes;
    }

    private static void getNodesByName(final Node node, final String nodeName, final List output) {
        if (node.getNodeName().equals(nodeName)) {
            output.add(node);
        }

        final NodeList nodeList = node.getChildNodes();

        for (int i = 0; i < nodeList.getLength(); i++) {
            getNodesByName(nodeList.item(i), nodeName, output);
        }
    }

    /**
     * Gets the next node by name.
     *
     * @param node The parent node to search within.
     * @param nodeName The name of the node to find.
     * @return The next node with the specified name, or {@code null} if no such node is found.
     */
    public static Node getNextNodeByName(final Node node, final String nodeName) {
        if (node.getNodeName().equals(nodeName)) {
            return node;
        } else {
            final NodeList nodeList = node.getChildNodes();

            Node subNode = null;

            for (int i = 0; i < nodeList.getLength(); i++) {
                subNode = nodeList.item(i);

                if (subNode.getNodeName().equals(nodeName)) {
                    return subNode;
                }
            }

            Node nextNode = null;

            for (int i = 0; i < nodeList.getLength(); i++) {
                nextNode = getNextNodeByName(nodeList.item(i), nodeName);

                if (nextNode != null) {
                    return nextNode;
                }
            }
        }

        return null;
    }

    /**
     * Gets the attribute value of the specified attribute name from the given XML node.
     *
     * @param node The XML node from which to get the attribute.
     * @param attrName The name of the attribute to retrieve.
     * @return The value of the specified attribute, or {@code null} if the attribute does not exist.
     */
    @MayReturnNull
    public static String getAttribute(final Node node, final String attrName) {
        final NamedNodeMap attrsNode = node.getAttributes();

        if (attrsNode == null) {
            return null;
        }

        final Node attrNode = attrsNode.getNamedItem(attrName);

        return (attrNode == null) ? null : attrNode.getNodeValue();
    }

    /**
     * Reads the attributes of the given XML node and returns them as a map.
     *
     * @param node The XML node from which to read the attributes.
     * @return A map containing the attributes of the given node, where the keys are attribute names and the values are attribute values.
     */
    public static Map readAttributes(final Node node) {
        return readAttributes(Strings.EMPTY_STRING, node, new LinkedHashMap<>());
    }

    private static Map readAttributes(final String parentNodeName, final Node node, final Map output) {
        final NamedNodeMap attrNodes = node.getAttributes();

        if (attrNodes == null || attrNodes.getLength() == 0) {
            return output;
        }

        final boolean isEmptyParentNodeName = Strings.isEmpty(parentNodeName);

        for (int i = 0; i < attrNodes.getLength(); i++) {
            final String attrName = attrNodes.item(i).getNodeName();
            final String attrValue = attrNodes.item(i).getNodeValue();

            if (isEmptyParentNodeName) {
                output.put(attrName, attrValue);
            } else {
                output.put(parentNodeName + "." + attrName, attrValue);
            }
        }

        return output;
    }

    /**
     * Reads the given XML element and returns its attributes and text content as a map.
     *
     * @param element The XML element to be read.
     * @return A map containing the attributes and text content of the given element, where the keys are attribute names and the values are attribute values.
     */
    public static Map readElement(final Element element) {
        return readElement(Strings.EMPTY_STRING, element, new LinkedHashMap<>());
    }

    private static Map readElement(final String parentNodeName, final Element element, final Map output) {
        readAttributes(parentNodeName, element, output);

        final boolean isEmptyParentNodeName = Strings.isEmpty(parentNodeName);

        if (isTextElement(element)) {
            final String nodeName = element.getNodeName();
            final String nodeText = Strings.strip(getTextContent(element));

            if (isEmptyParentNodeName) {
                output.put(nodeName, nodeText);
            } else {
                output.put(parentNodeName + "." + nodeName, nodeText);
            }
        }

        final String nextParentNodeName = isEmptyParentNodeName ? element.getNodeName() : parentNodeName + "." + element.getNodeName();
        final NodeList childNodeList = element.getChildNodes();

        for (int childNodeIndex = 0; childNodeIndex < childNodeList.getLength(); childNodeIndex++) {
            final Node childNode = childNodeList.item(childNodeIndex);

            if (childNode instanceof final Element childElement) {
                readElement(nextParentNodeName, childElement, output);
            }
        }

        return output;
    }

    /**
     * Checks if the given node is a text element.
     * A text element is defined as an element that does not contain any child elements.
     *
     * @param node The node to be checked.
     * @return {@code true} if the node is a text element, {@code false} otherwise.
     */
    public static boolean isTextElement(final Node node) {
        final NodeList childNodeList = node.getChildNodes();

        for (int i = 0; i < childNodeList.getLength(); i++) {
            if (childNodeList.item(i).getNodeType() == Document.ELEMENT_NODE) {
                return false;
            }
        }

        return true;
    }

    /**
     * Gets the text content of the given XML node.
     *
     * @param node The XML node from which to get the text content.
     * @return The text content of the specified node, with leading and trailing whitespace removed.
     */
    public static String getTextContent(final Node node) {
        return node.getTextContent();
    }

    /**
     * Gets the text content of the given XML node, optionally ignoring whitespace characters.
     *
     * @param node The XML node from which to get the text content.
     * @param ignoreWhiteChar Whether to ignore whitespace characters in the text content. if {@code true}, all the whitespace within text content will be removed.
     * @return The text content of the specified node, with all the whitespace within text content will be removed if {@code ignoreWhiteChar} is {@code true}.
     */
    public static String getTextContent(final Node node, final boolean ignoreWhiteChar) {
        String textContent = node.getTextContent();

        if (ignoreWhiteChar && Strings.isNotEmpty(textContent)) {
            final StringBuilder sb = Objectory.createStringBuilder();

            for (final char c : textContent.toCharArray()) {
                switch (c) {
                    case '\t':
                    case '\b':
                    case '\n':
                    case '\r':
                    case '\f':

                        if ((!sb.isEmpty()) && (sb.charAt(sb.length() - 1) != ' ')) {
                            sb.append(' ');
                        }

                        break;

                    default:
                        sb.append(c);
                }
            }

            final int length = sb.length();

            if ((length > 0) && ((sb.charAt(0) == ' ') || (sb.charAt(length - 1) == ' '))) {
                int from = 0;

                do {
                    if (sb.charAt(from) != ' ') {
                        break;
                    }

                    from++;
                } while (from < length);

                int to = length - 1;

                do {
                    if (sb.charAt(to) != ' ') {
                        break;
                    }

                    to--;
                } while (to >= 0);

                if (from <= to) {
                    textContent = sb.substring(from, to + 1);
                } else {
                    textContent = "";
                }
            } else {
                textContent = sb.toString();
            }

            Objectory.recycle(sb);
        }

        return textContent;
    }

    /**
     * Writes the characters from the specified character array to the given StringBuilder.
     *
     * @param cbuf The character array containing the characters to be written.
     * @param output The StringBuilder to which the characters will be written.
     * @throws IOException If an I/O error occurs.
     */
    public static void writeCharacters(final char[] cbuf, final StringBuilder output) throws IOException {
        writeCharacters(cbuf, 0, cbuf.length, output);
    }

    /**
     * Writes a portion of a character array to the given StringBuilder.
     *
     * @param cbuf The character array containing the characters to be written.
     * @param off The start offset in the character array.
     * @param len The number of characters to write.
     * @param output The StringBuilder to which the characters will be written.
     * @throws IOException If an I/O error occurs.
     */
    public static void writeCharacters(final char[] cbuf, final int off, final int len, final StringBuilder output) throws IOException {
        writeCharacters(cbuf, off, len, IOUtil.stringBuilder2Writer(output));
    }

    /**
     * Writes the characters from the specified string to the given StringBuilder.
     *
     * @param str The string containing the characters to be written.
     * @param output The StringBuilder to which the characters will be written.
     * @throws IOException If an I/O error occurs.
     */
    public static void writeCharacters(String str, final StringBuilder output) throws IOException {
        str = (str == null) ? Strings.NULL_STRING : str;
        writeCharacters(str, 0, str.length(), output);
    }

    /**
     * Writes a portion of a string to the given StringBuilder.
     *
     * @param str The string containing the characters to be written.
     * @param off The start offset in the string.
     * @param len The number of characters to write.
     * @param output The StringBuilder to which the characters will be written.
     * @throws IOException If an I/O error occurs.
     */
    public static void writeCharacters(final String str, final int off, final int len, final StringBuilder output) throws IOException {
        writeCharacters(str, off, len, IOUtil.stringBuilder2Writer(output));
    }

    /**
     * Writes the characters from the specified character array to the given OutputStream.
     *
     * @param cbuf The character array containing the characters to be written.
     * @param output The OutputStream to which the characters will be written.
     * @throws IOException If an I/O error occurs.
     */
    public static void writeCharacters(final char[] cbuf, final OutputStream output) throws IOException {
        writeCharacters(cbuf, 0, cbuf.length, output);
    }

    /**
     * Writes a portion of a character array to the given OutputStream.
     *
     * @param cbuf The character array containing the characters to be written.
     * @param off The start offset in the character array.
     * @param len The number of characters to write.
     * @param output The OutputStream to which the characters will be written.
     * @throws IOException If an I/O error occurs.
     */
    public static void writeCharacters(final char[] cbuf, final int off, final int len, final OutputStream output) throws IOException {
        final BufferedXMLWriter bufWriter = Objectory.createBufferedXMLWriter(output); //NOSONAR

        try {
            bufWriter.writeCharacter(cbuf, off, len);
            bufWriter.flush();
        } finally {
            Objectory.recycle(bufWriter);
        }
    }

    /**
     * Writes the characters from the specified string to the given OutputStream.
     *
     * @param str The string containing the characters to be written.
     * @param output The OutputStream to which the characters will be written.
     * @throws IOException If an I/O error occurs.
     */
    public static void writeCharacters(String str, final OutputStream output) throws IOException {
        str = (str == null) ? Strings.NULL_STRING : str;
        writeCharacters(str, 0, str.length(), output);
    }

    /**
     * Writes a portion of a string to the given OutputStream.
     *
     * @param str The string containing the characters to be written.
     * @param off The start offset in the string.
     * @param len The number of characters to write.
     * @param output The OutputStream to which the characters will be written.
     * @throws IOException If an I/O error occurs.
     */
    public static void writeCharacters(final String str, final int off, final int len, final OutputStream output) throws IOException {
        final BufferedXMLWriter bufWriter = Objectory.createBufferedXMLWriter(output); //NOSONAR

        try {
            bufWriter.writeCharacter(str, off, len);
            bufWriter.flush();
        } finally {
            Objectory.recycle(bufWriter);
        }
    }

    /**
     * Writes the characters from the specified character array to the given Writer.
     *
     * @param cbuf The character array containing the characters to be written.
     * @param output The Writer to which the characters will be written.
     * @throws IOException If an I/O error occurs.
     */
    public static void writeCharacters(final char[] cbuf, final Writer output) throws IOException {
        writeCharacters(cbuf, 0, cbuf.length, output);
    }

    /**
     * Writes a portion of a character array to the given Writer.
     *
     * @param cbuf The character array containing the characters to be written.
     * @param off The start offset in the character array.
     * @param len The number of characters to write.
     * @param output The Writer to which the characters will be written.
     * @throws IOException If an I/O error occurs.
     */
    public static void writeCharacters(final char[] cbuf, final int off, final int len, final Writer output) throws IOException {
        final boolean isBufferedWriter = output instanceof BufferedXMLWriter;
        final BufferedXMLWriter bw = isBufferedWriter ? (BufferedXMLWriter) output : Objectory.createBufferedXMLWriter(output); //NOSONAR

        try {
            bw.writeCharacter(cbuf, off, len);
            bw.flush();
        } finally {
            if (!isBufferedWriter) {
                Objectory.recycle(bw);
            }
        }
    }

    /**
     * Writes the characters from the specified string to the given Writer.
     *
     * @param str The string containing the characters to be written.
     * @param output The Writer to which the characters will be written.
     * @throws IOException If an I/O error occurs.
     */
    public static void writeCharacters(String str, final Writer output) throws IOException {
        str = (str == null) ? Strings.NULL_STRING : str;
        writeCharacters(str, 0, str.length(), output);
    }

    /**
     * Writes a portion of a string to the given Writer.
     *
     * @param str The string containing the characters to be written.
     * @param off The start offset in the string.
     * @param len The number of characters to write.
     * @param output The Writer to which the characters will be written.
     * @throws IOException If an I/O error occurs.
     */
    public static void writeCharacters(final String str, final int off, final int len, final Writer output) throws IOException {
        final boolean isBufferedWriter = output instanceof BufferedXMLWriter;
        final BufferedXMLWriter bw = isBufferedWriter ? (BufferedXMLWriter) output : Objectory.createBufferedXMLWriter(output); //NOSONAR

        try {
            bw.writeCharacter(str, off, len);
            bw.flush();
        } finally {
            if (!isBufferedWriter) {
                Objectory.recycle(bw);
            }
        }
    }

    /**
     * Gets the attribute type class.
     *
     * @param node
     * @return
     */
    static Class getAttributeTypeClass(final Node node) {
        final String typeAttr = XmlUtil.getAttribute(node, TYPE);

        if (typeAttr == null) {
            return null;
        }

        final Type type = N.typeOf(typeAttr);

        if (type != null) {
            return type.clazz();
        }

        try {
            return ClassUtil.forClass(typeAttr);
        } catch (final RuntimeException e) {
            return null;
        }
    }

    /**
     * Gets the concrete class.
     *
     * @param targetClass
     * @param typeClass
     * @return
     */
    /*
     * static Class getAttributeTypeClass(Attributes attrs) { if (attrs == null) { return null; }
     *
     * String typeAttr = attrs.getValue(TYPE);
     *
     * if (typeAttr == null) { return null; }
     *
     * Type type = N.getType(typeAttr);
     *
     * if (type != null) { return type.getTypeClass(); }
     *
     * try { return N.forClass(typeAttr); } catch (RuntimeException e) { return null; } }
     */
    static Class getConcreteClass(final Class targetClass, final Class typeClass) {
        if ((typeClass == null) || ((targetClass != null) && !targetClass.isAssignableFrom(typeClass))) {
            return targetClass;
        } else {
            return typeClass;
        }
    }

    /**
     * Gets the concrete class.
     *
     * @param targetClass
     * @param node
     * @return
     */
    static Class getConcreteClass(final Class targetClass, final Node node) {
        if (node == null) {
            return targetClass;
        }

        final Class typeClass = getAttributeTypeClass(node);

        return getConcreteClass(targetClass, typeClass);
    }

    /**
     * Gets the node type.
     *
     * @param nodeName
     * @param previousNodeType
     * @return
     */
    /*
     * static Class getConcreteClass(Class targetClass, Attributes attrs) { if (attrs == null) { return targetClass;
     * }
     *
     * Class typeClass = getAttributeTypeClass(attrs);
     *
     * return getConcreteClass(targetClass, typeClass); }
     */
    static NodeType getNodeType(final String nodeName, final NodeType previousNodeType) {
        if (previousNodeType == NodeType.ENTITY) {
            return NodeType.PROPERTY;
        }

        final NodeType nodeType = nodeTypePool.get(nodeName);

        if (nodeType == null) {
            return NodeType.ENTITY;
        }

        return nodeType;
    }

    /**
     * The Enum NodeType.
     */
    enum NodeType {

        /** The bean. */
        ENTITY,
        /** The property. */
        PROPERTY,
        /** The array. */
        ARRAY,
        /** The element. */
        ELEMENT,
        /** The collection. */
        COLLECTION,
        /** The map. */
        MAP,
        /** The entry. */
        ENTRY,
        /** The key. */
        KEY,
        /** The value. */
        VALUE
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy