1
\$\begingroup\$

I want to save and load an arbitrary Java String in a Text node in a XML file using the Java DOM API.

The Transformer and the DocumentBuilder already escape and unescape predefined entities such as ", &, ', <, > and a few other characters by replacing them with with &quot;, &amp;, &apos;, &lt;, &gt; and &#c;, where c is the Unicode code point.

So this is not the kind of escaping I am talking about. I am talking about the characters which are not in the ranges #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] because these are according to the XML specifications invalid characters in XML files, even if escaped.

However, I want to be able to save an arbitrary String which may or may not contains an invalid character. The idea is to use two functions escapeInvalidXmlCharacters and unescapeInvalidXmlCharacters before and after using the DOM API as in the following example. Invalid characters are replaced by #c;, where c is the Unicode code point and # is escaped by a leading #.

    // should not be reviewed
    String string = "text#text##text#0;text" + '\u0000' + "text<text&text#";
    Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
    Element element = document.createElement("element");
    element.appendChild(document.createTextNode(escapeInvalidXmlCharacters(string)));
    document.appendChild(element);
    TransformerFactory.newInstance().newTransformer().transform(new DOMSource(document), new StreamResult(new File("test.xml")));
    // creates <?xml version="1.0" encoding="UTF-8" standalone="no"?><element>text##text####text##0;text#0;text&lt;text&amp;text##</element>
    document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new File("test.xml"));
    System.out.println(unescapeInvalidXmlCharacters(document.getDocumentElement().getTextContent()).equals(string));
    // prints true

Here are the functions to be reviewed.

    /**
     * Escapes invalid XML Unicode code points in a <code>{@link String}</code>. The
     * DOM API already escapes predefined entities, such as {@code "}, {@code &},
     * {@code '}, {@code <} and {@code >} for
     * <code>{@link org.w3c.dom.Text Text}</code> nodes. Therefore, these Unicode
     * code points are ignored by this function. However, there are some other
     * invalid XML Unicode code points, such as {@code '\u0000'}, which are even
     * invalid in their escaped form, such as {@code "&#0;"}.
     * <p>
     * This function replaces all {@code '#'} by {@code "##"} and all Unicode code
     * points which are not in the ranges #x9 | #xA | #xD | [#x20-#xD7FF] |
     * [#xE000-#xFFFD] | [#x10000-#x10FFFF] by the <code>{@link String}</code>
     * {@code "#c;"}, where <code>c</code> is the Unicode code point.
     * 
     * @param string the <code>{@link String}</code> to be escaped
     * @return the escaped <code>{@link String}</code>
     * @see <code>{@link #unescapeInvalidXmlCharacters(String)}</code>
     */
    public static String escapeInvalidXmlCharacters(String string) {
        StringBuilder stringBuilder = new StringBuilder();

        for (int i = 0, codePoint = 0; i < string.length(); i += Character.charCount(codePoint)) {
            codePoint = string.codePointAt(i);

            if (codePoint == '#') {
                stringBuilder.append("##");
            } else if (codePoint == 0x9 || codePoint == 0xA || codePoint == 0xD || codePoint >= 0x20 && codePoint <= 0xD7FF || codePoint >= 0xE000 && codePoint <= 0xFFFD || codePoint >= 0x10000 && codePoint <= 0x10FFFF) {
                stringBuilder.appendCodePoint(codePoint);
            } else {
                stringBuilder.append("#" + codePoint + ";");
            }
        }

        return stringBuilder.toString();
    }

    /**
     * Unescapes invalid XML Unicode code points in a <code>{@link String}</code>.
     * Makes <code>{@link #escapeInvalidXmlCharacters(String)}</code> undone.
     * 
     * @param string the <code>{@link String}</code> to be unescaped
     * @return the unescaped <code>{@link String}</code>
     * @see <code>{@link #escapeInvalidXmlCharacters(String)}</code>
     */
    public static String unescapeInvalidXmlCharacters(String string) {
        StringBuilder stringBuilder = new StringBuilder();
        boolean escaped = false;

        for (int i = 0, codePoint = 0; i < string.length(); i += Character.charCount(codePoint)) {
            codePoint = string.codePointAt(i);

            if (escaped) {
                stringBuilder.appendCodePoint(codePoint);
                escaped = false;
            } else if (codePoint == '#') {
                StringBuilder intBuilder = new StringBuilder();
                int j;

                for (j = i + 1; j < string.length(); j += Character.charCount(codePoint)) {
                    codePoint = string.codePointAt(j);

                    if (codePoint == ';') {
                        escaped = true;
                        break;
                    }

                    if (codePoint >= 48 && codePoint <= 57) {
                        intBuilder.appendCodePoint(codePoint);
                    } else {
                        break;
                    }
                }

                if (escaped) {
                    try {
                        codePoint = Integer.parseInt(intBuilder.toString());
                        stringBuilder.appendCodePoint(codePoint);
                        escaped = false;
                        i = j;
                    } catch (IllegalArgumentException e) {
                        codePoint = '#';
                        escaped = true;
                    }
                } else {
                    codePoint = '#';
                    escaped = true;
                }
            } else {
                stringBuilder.appendCodePoint(codePoint);
            }
        }

        return stringBuilder.toString();
    }

You can additionally post your answer under my related SO question https://stackoverflow.com/q/59447599/3882565 where I have started a +200 reputation bounty.

\$\endgroup\$
2
  • \$\begingroup\$ How would an answer that is acceptable here be acceptable on SO? \$\endgroup\$ – FreezePhoenix Jan 1 '20 at 21:47
  • \$\begingroup\$ @FreezePhoenix Just wanted to link this question here and if someone has some major improvements, why shouldn't they be posted on SO too? Of course not in the form of a review, but rather as a finished version. If you don't think your answer is acceptable on SO, then just don't post it there. \$\endgroup\$ – stonar96 Jan 1 '20 at 23:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.