web-dev-qa-db-ja.com

Java DOMは、無効なXML文字を含む任意の文字列を変換および解析しますか?

まず、これは 無効な(悪い/整形式ではない)XMLを解析する方法? の重複ではないことを述べておきます。これは、指定された無効な(または整形式でない)ためです。 )XMLファイルではなく、指定された任意のJava Stringに無効なXML文字が含まれている場合と含まれていない場合があります。指定されたDocumentTextノードを含むDOM Stringを作成し、それを次のように変換しますファイル。ファイルがDOMに解析される場合Document最初に指定されたStringと等しいStringを取得したい。org.w3c.dom.Document#createTextNode(String data)を使用してTextノードを作成し、org.w3c.dom.Node#getTextContent()

https://stackoverflow.com/a/28152666/3882565 で確認できるように、XMLファイルのTextノードには無効な文字がいくつかあります。実際には、Textノードには2つの異なるタイプの「無効な」文字があります。 "&'<>&quot;&amp;&apos;&lt;&gt;などの事前定義されたエンティティがあり、結果ファイルがDOMで解析されたときに、ファイルがAPIで解析されると、ファイルがAPIで解析されると、そのファイルがDOMによって解析されます。ここでの問題は、これが'\u0000''\uffff'などの他の無効な文字には当てはまらないことです。 '\u0000'および'\uffff'は無効な文字であるため、ファイルの解析時に例外が発生します。

おそらく、DOM APIに送信する前に、特定のStringのこれらの文字を独自の方法でエスケープするメソッドを実装し、後でStringを取得したときに元に戻す必要があります。これを行うより良い方法はありますか?誰かが過去にそれらまたは同様の方法を実装しましたか?

編集:この質問は JavaでXMLのテキストデータをエンコードする最良の方法? の重複としてマークされました。これですべての回答を読みましたが、どれも私の問題を解決しません。すべての回答が示唆しています:

  • 私がすでに行っているDOM APIなどのXMLライブラリを使用すると、"&'<>などを除いて、実際には無効な文字は置き換えられません。
  • すべての無効な文字を"&#number;"で置き換えると、ファイルの解析時に"&#0;"などの無効な文字の例外が発生します。
  • "&#0;"などの不正な文字をサポートしないXMLエンコードメソッドでサードパーティライブラリを使用する(一部のライブラリではスキップされる)。
  • 無効な文字もサポートしないCDATAセクションの使用。
4
stonar96

@VGRと@kjhughesが質問の下のコメントで指摘しているように、Base64は確かに私の質問に対する可能な回答です。これで、エスケープに基づいた問題の解決策がわかりました。私は2つの関数escapeInvalidXmlCharacters(String string)unescapeInvalidXmlCharacters(String string)を作成しました。これらは次のように使用できます。

_    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
_

escapeInvalidXmlCharacters(String string)およびunescapeInvalidXmlCharacters(String string)

_/**
 * 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();
}
_

これらの関数はおそらく非常に非効率的であり、より良い方法で書くことができることに注意してください。コメントでコードを改善するための提案を自由に投稿してください。

0
stonar96

1つの手法は、文字列全体をBase64でエンコードされたUTF8としてエンコードすることです。

しかし、「特殊」文字がまれである場合、それは可読性とファイルサイズを大幅に犠牲にします。

別の手法は、特殊文字を処理命令として表すことです。たとえば、<?U 0000?>(コードポイント0)。

もう1つは、バックスラッシュエスケープを使用することです。たとえば、コードポイント0には\ u0000、バックスラッシュ自体には\を使用します。これには、これを実行する既存のライブラリルーチン(JSON変換ライブラリなど)を見つけることができるという利点があります。あなたの要件がそのようなライブラリーを使用できないと言っている理由を私は想像できません。しかし、本当にそれができない場合でも、自分でコードを書くことは難しくありません。

1
Michael Kay

最も簡単な解決策は、XML 1.1(org.w3c.dom)このプリプロセッサを使用して:

<?xml version=1.1 encoding=UTF-8 standalone=yes?>

Wikipedia によると、XML 1.1の無効な文字はU + 0000、 サロゲート+ FFFEおよびU + FFFF のみです。

このコードスニペットを使用すると、不正な文字を省略して、常に正しいXML 1.1文字列を取得できます(まったく同じ文字列が必要な場合は、探しているものとは異なる場合があります)。

public static String escape(String orig) {
    StringBuilder builder = new StringBuilder();

    for (char c : orig.toCharArray()) {
        if (c == 0x0 || c == 0xfffe || c == 0xffff || (c >= 0xd800 && c <= 0xdfff)) {
            continue;
        } else if (c == '\'') {
            builder.append("&apos;");
        } else if (c == '"') {
            builder.append("&quot;");
        } else if (c == '&') {
            builder.append("&amp;");
        } else if (c == '<') {
            builder.append("&lt;");
        } else if (c == '>') {
            builder.append("&gt;");
        } else if (c <= 0x1f) {
            builder.append("&#" + ((int) c) + ";");
        } else {
            builder.append(c);
        }
    }

    return builder.toString();
}
1
Mordechai