从Java 8到Java 11,处理CDATA的XML转换中换行符的变化

14

自Java 9版本起,javax.xml.transform.TransformerOutputKeys.INDENT对CDATA标签的处理方式发生了变化。简而言之,在Java 8中,一个包含字符数据的名为'test'的标签会导致以下结果:

<test><![CDATA[data]]></test>

但是使用Java 9则会产生相同的结果

<test>
    <![CDATA[data]]>
</test>

这并不是相同的XML。

我曾经从一份已失效的资料中了解到,Java 9 可以使用 DocumentBuilderFactory 并设置 setIgnoringElementContentWhitespace=true 来解决此问题,但在 Java 11 中无法使用。

有人知道如何在Java 11中处理此问题吗?我要么寻找一种方法来防止额外的换行符(但仍然能够格式化我的XML),要么能够在解析XML时忽略它们(最好使用SAX)。

不幸的是,我不知道CDATA标记在我的应用程序中实际包含什么。它可能以空格或换行符开头或结尾,因此我不能在读取XML时只是删除它们,也不能在生成的对象中实际设置该值。

演示此问题的示例程序:

public static void main(String[] args) throws TransformerException, ParserConfigurationException, IOException, SAXException
{
    String data = "data";

    StreamSource source = new StreamSource(new StringReader("<foo><bar><![CDATA[" + data + "]]></bar></foo>"));
    StreamResult result = new StreamResult(new StringWriter());

    Transformer tform = TransformerFactory.newInstance().newTransformer();
    tform.setOutputProperty(OutputKeys.INDENT, "yes");
    tform.transform(source, result);

    String xml = result.getWriter().toString();

    System.out.println(xml); // I expect bar and CDATA to be on same line. This is true for Java 8, false for Java 11


    Document document = DocumentBuilderFactory.newInstance()
        .newDocumentBuilder()
        .parse(new InputSource(new StringReader(xml)));

    String resultData = document.getElementsByTagName("bar")
        .item(0)
        .getTextContent();

    System.out.println(data.equals(resultData)); // True for Java 8, false for Java 11
}

编辑:供后续参考,我已向Oracle提交了一个缺陷报告,并在Java 14中修复了此问题:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8223291


2
你应该编辑你的问题,并添加一个示例Java代码,以演示问题(生成一个小的XML+转换)。使用一个工作示例很容易入手。 - Robert
2个回答

5
由于您的代码依赖于未指定的行为,因此添加额外的显式代码似乎更好。
  • You want indentation like:

      tform.setOutputProperty(OutputKeys.INDENT, "yes");
      tform.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
    
  • However not for elements containing a CDATA tag:

      String xml = result.getWriter().toString();
      // No indentation (whitespace) for elements with a CDATA section.
      xml = xml.replaceAll("(?s)>\\s*(<\\!\\[CDATA\\[.*?]]>)\\s*</", ">$1</");
    

正则表达式使用:

  • (?s) DOT_ALL,使.匹配任何字符,包括换行符。
  • .*? 最短的匹配序列,以不匹配"...]]>...]];"。

另外:在保留CDATA的DOM树中,您可以通过XPath检索所有CDATA部分,并使用父元素删除空白同级元素。


谢谢!这实际上是一个相当干净的解决方法。我想知道你所说的我的代码依赖于未指定的行为是什么意思? - Rick
你说转换应该进行漂亮的打印,缩进每个元素。但是最新的Java版本确实做到了:还缩进了CDATA部分。所以这似乎是为CDATA而做的一个早期例外。在任何情况下,规范都无可指责。 - Joop Eggen
CDATA可以跟随“正常”的数据。例如,这是有效的:<test> <![CDATA[data]]> foo </test>。通过添加额外的空格,XML的内容会发生变化。因此,我认为这是转换器的问题。 - Rick
那么为什么要使用INDENT=yes呢?在DTD / XSD中可以限制允许的内容,但我不认为这在这里起作用(或者在验证中起作用)。如果您之后要读取DOM,则INDENT =“no”是否就足够了呢? - Joop Eggen
3
CDATA的问题已在Java 14中得到修复。我在ea版本中进行了测试:openjdk版本“14-ea”2020-03-17 OpenJDK运行时环境(构建14-ea + 6-171) - JuanMoreno
已验证它确实与OpenJDK 14的ea版本兼容。谢谢! - Rick

1

Joop Eggen的解决方案非常出色。

我只是想稍微扩展一下这个解决方案。

xml = xml.replaceAll(">\\s*(<\\!\\[CDATA\\[(.|\\n|\\r\\n)*?]\\]>)\\s*</", ">$1</");

在这个正则表达式中,我包括了 CDATA 标签内允许存在换行符的可能性。因此,我测试了 \n 和 Windows 风格的 \r\n
XML 示例:
<test>
   <![CDATA[com.foo.test]]>
</test
<test>
 <![CDATA[2st Line   
2nd Line]]>
</test>

Joop Eggen提到在正则表达式前加上(?s)可以使.*匹配换行符。虽然他在回答中实际上没有包含它在正则表达式中,但我认为我当时使用了它来解决我的问题。 - Rick
我已经编辑了Joop Eggen的答案,将(?s)包含在正则表达式中,我将让未来的读者决定他们更喜欢使用哪个正则表达式 :) - Rick

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接