DomWriter.java

/*
 *    Copyright 2006-2026 the original author or authors.
 *
 *    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
 *
 *       https://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.mybatis.generator.merge.xml;

import static org.mybatis.generator.internal.util.messages.Messages.getString;

import java.io.PrintWriter;
import java.io.StringWriter;

import org.jspecify.annotations.Nullable;
import org.mybatis.generator.exception.MergeException;
import org.w3c.dom.Attr;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Element;
import org.w3c.dom.EntityReference;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.ProcessingInstruction;
import org.w3c.dom.Text;

/**
 * This class is used to generate a String representation of an XML document. It
 * is very much based on the class dom.Writer from the Apache Xerces examples,
 * but I've simplified and updated it.
 *
 * @author Andy Clark, IBM (Original work)
 * @author Jeff Butler (derivation)
 */
public class DomWriter {
    protected final PrintWriter printWriter;
    protected final StringWriter sw;
    protected final Document document;
    protected boolean isXML11;

    public DomWriter(Document document) {
        sw = new StringWriter();
        printWriter = new PrintWriter(sw);
        this.document = document;
    }

    public synchronized String getFormattedDocument() throws MergeException {
        write(document);
        return sw.toString();
    }

    protected Attr[] sortAttributes(@Nullable NamedNodeMap attrs) {
        int len = (attrs != null) ? attrs.getLength() : 0;
        Attr[] array = new Attr[len];
        for (int i = 0; i < len; i++) {
            array[i] = (Attr) attrs.item(i);
        }
        for (int i = 0; i < len - 1; i++) {
            String name = array[i].getNodeName();
            int index = i;
            for (int j = i + 1; j < len; j++) {
                String curName = array[j].getNodeName();
                if (curName.compareTo(name) < 0) {
                    name = curName;
                    index = j;
                }
            }
            if (index != i) {
                Attr temp = array[i];
                array[i] = array[index];
                array[index] = temp;
            }
        }

        return array;
    }

    protected void normalizeAndPrint(@Nullable String s, boolean isAttValue) {
        int len = (s != null) ? s.length() : 0;
        for (int i = 0; i < len; i++) {
            char c = s.charAt(i);
            normalizeAndPrint(c, isAttValue);
        }
    }

    protected void normalizeAndPrint(char c, boolean isAttValue) {

        switch (c) {
        case '<':
            handleLessThan();
            break;
        case '>':
            handleGreaterThan();
            break;
        case '&':
            handleAmpersand();
            break;
        case '"':
            handleDoubleQuote(isAttValue);
            break;
        case '\r':
            handleCarriageReturn();
            break;
        case '\n':
            handleLineFeed();
            break;
        default:
            handleDefault(c, isAttValue);
        }
    }

    private void handleDefault(char c, boolean isAttValue) {
        // In XML 1.1, control chars in the ranges [#x1-#x1F, #x7F-#x9F]
        // must be escaped.
        //
        // Escape space characters that would be normalized to #x20 in
        // attribute values
        // when the document is reparsed.
        //
        // Escape NEL (0x85) and LSEP (0x2028) that appear in content
        // if the document is XML 1.1, since they would be normalized to LF
        // when the document is reparsed.
        if (isXML11
                && ((c >= 0x01 && c <= 0x1F && c != 0x09 && c != 0x0A)
                        || (c >= 0x7F && c <= 0x9F) || c == 0x2028)
                || isAttValue && (c == 0x09 || c == 0x0A)) {
            printWriter.print("&#x"); //$NON-NLS-1$
            printWriter.print(Integer.toHexString(c).toUpperCase());
            printWriter.print(';');
        } else {
            printWriter.print(c);
        }
    }

    private void handleLineFeed() {
        // If LF is part of the document's content, it
        // should be printed back out with the system default
        // line separator.  XML parsing forces \n only after a parse,
        // but we should write it out as it was to avoid whitespace
        // commits on some version control systems.
        printWriter.print(System.lineSeparator());
    }

    private void handleCarriageReturn() {
        // If CR is part of the document's content, it
        // must be printed as a literal otherwise
        // it would be normalized to LF when the document
        // is reparsed.
        printWriter.print("&#xD;"); //$NON-NLS-1$
    }

    private void handleDoubleQuote(boolean isAttValue) {
        // A '"' that appears in character data
        // does not need to be escaped.
        if (isAttValue) {
            printWriter.print("&quot;"); //$NON-NLS-1$
        } else {
            printWriter.print('"');
        }
    }

    private void handleAmpersand() {
        printWriter.print("&amp;"); //$NON-NLS-1$
    }

    private void handleGreaterThan() {
        printWriter.print("&gt;"); //$NON-NLS-1$
    }

    private void handleLessThan() {
        printWriter.print("&lt;"); //$NON-NLS-1$
    }

    /**
     * Extracts the XML version from the Document.
     *
     * @param document
     *            the document
     * @return the version
     */
    protected String getVersion(Document document) {
        return document.getXmlVersion();
    }

    protected void writeAnyNode(Node node) throws MergeException {
        short type = node.getNodeType();
        switch (type) {
        case Node.DOCUMENT_NODE:
            write((Document) node);
            break;

        case Node.DOCUMENT_TYPE_NODE:
            write((DocumentType) node);
            break;

        case Node.ELEMENT_NODE:
            write((Element) node);
            break;

        case Node.ENTITY_REFERENCE_NODE:
            write((EntityReference) node);
            break;

        case Node.CDATA_SECTION_NODE:
            write((CDATASection) node);
            break;

        case Node.TEXT_NODE:
            write((Text) node);
            break;

        case Node.PROCESSING_INSTRUCTION_NODE:
            write((ProcessingInstruction) node);
            break;

        case Node.COMMENT_NODE:
            write((Comment) node);
            break;

        default:
            throw new MergeException(getString("RuntimeError.18", Short.toString(type))); //$NON-NLS-1$
        }
    }

    protected void write(Document node) throws MergeException {
        isXML11 = "1.1".equals(getVersion(node)); //$NON-NLS-1$
        if (isXML11) {
            printWriter.println("<?xml version=\"1.1\" encoding=\"UTF-8\"?>"); //$NON-NLS-1$
        } else {
            printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); //$NON-NLS-1$
        }
        printWriter.flush();
        write(node.getDoctype());
        write(node.getDocumentElement());
    }

    protected void write(DocumentType node) {
        printWriter.print("<!DOCTYPE "); //$NON-NLS-1$
        printWriter.print(node.getName());
        String publicId = node.getPublicId();
        String systemId = node.getSystemId();
        if (publicId != null) {
            printWriter.print(" PUBLIC \""); //$NON-NLS-1$
            printWriter.print(publicId);
            printWriter.print("\" \""); //$NON-NLS-1$
            printWriter.print(systemId);
            printWriter.print('\"');
        } else if (systemId != null) {
            printWriter.print(" SYSTEM \""); //$NON-NLS-1$
            printWriter.print(systemId);
            printWriter.print('"');
        }

        String internalSubset = node.getInternalSubset();
        if (internalSubset != null) {
            printWriter.println(" ["); //$NON-NLS-1$
            printWriter.print(internalSubset);
            printWriter.print(']');
        }
        printWriter.println('>');
    }

    protected void write(Element node) throws MergeException {
        printWriter.print('<');
        printWriter.print(node.getNodeName());
        Attr[] attrs = sortAttributes(node.getAttributes());
        for (Attr attr : attrs) {
            printWriter.print(' ');
            printWriter.print(attr.getNodeName());
            printWriter.print("=\""); //$NON-NLS-1$
            normalizeAndPrint(attr.getNodeValue(), true);
            printWriter.print('"');
        }

        if (node.getChildNodes().getLength() == 0) {
            printWriter.print(" />"); //$NON-NLS-1$
            printWriter.flush();
        } else {
            printWriter.print('>');
            printWriter.flush();

            Node child = node.getFirstChild();
            while (child != null) {
                writeAnyNode(child);
                child = child.getNextSibling();
            }

            printWriter.print("</"); //$NON-NLS-1$
            printWriter.print(node.getNodeName());
            printWriter.print('>');
            printWriter.flush();
        }
    }

    protected void write(EntityReference node) {
        printWriter.print('&');
        printWriter.print(node.getNodeName());
        printWriter.print(';');
        printWriter.flush();
    }

    protected void write(CDATASection node) {
        printWriter.print("<![CDATA["); //$NON-NLS-1$
        String data = node.getNodeValue();
        // XML parsers normalize line endings to '\n'. We should write
        // it out as it was in the original to avoid whitespace commits
        // on some version control systems
        int len = (data != null) ? data.length() : 0;
        for (int i = 0; i < len; i++) {
            char c = data.charAt(i);
            if (c == '\n') {
                handleLineFeed();
            } else {
                printWriter.print(c);
            }
        }
        printWriter.print("]]>"); //$NON-NLS-1$
        printWriter.flush();
    }

    protected void write(Text node) {
        normalizeAndPrint(node.getNodeValue(), false);
        printWriter.flush();
    }

    protected void write(ProcessingInstruction node) {
        printWriter.print("<?"); //$NON-NLS-1$
        printWriter.print(node.getNodeName());
        String data = node.getNodeValue();
        if (data != null && !data.isEmpty()) {
            printWriter.print(' ');
            printWriter.print(data);
        }
        printWriter.print("?>"); //$NON-NLS-1$
        printWriter.flush();
    }

    protected void write(Comment node) {
        printWriter.print("<!--"); //$NON-NLS-1$
        String comment = node.getNodeValue();
        if (comment != null && !comment.isEmpty()) {
            normalizeAndPrint(comment, false);
        }
        printWriter.print("-->"); //$NON-NLS-1$
        printWriter.flush();
    }
}