使用DOM解析XML时,DOCTYPE会被删除。

14

用 Java 操作 DOM 编辑 XML 时,为什么会擦除文档类型声明?

这是我得到的 XML 文件:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE map[ <!ELEMENT map (station*) >
                <!ATTLIST station  id   ID    #REQUIRED> ]>
<favoris>
<station id="5">test1</station>
<station id="6">test1</station>
<station id="8">test1</station>
</favoris> 

我的函数非常基础:

public static void EditStationName(int id, InputStream is, String path, String name) throws ParserConfigurationException, SAXException, IOException, TransformerFactoryConfigurationError, TransformerException{
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

    DocumentBuilder builder = factory.newDocumentBuilder();
    Document dom = builder.parse(is);

    Element e = dom. getElementById(String.valueOf(id));
    e.setTextContent(name);
    // Write the DOM document to the file
    Transformer xformer = TransformerFactory.newInstance().newTransformer();
    FileOutputStream fos = new FileOutputStream(path);
    Result result = new StreamResult(fos);  
    Source source = new DOMSource(dom);


        xformer.setOutputProperty(
                OutputKeys.STANDALONE,"yes"     
                );

    xformer.transform(source, result);
}

它是可以工作的,但文档类型被删除了!我只得到了整个文档,但没有文档类型部分,这对我很重要,因为它允许我通过id检索! 我们如何保留文档类型?为什么会擦除它? 我尝试了很多解决方案,例如outputkeys或omImpl.createDocumentType,但这些都没有起作用...

谢谢!


我很惊讶你能得到任何东西;你的XML无效。 - Daniel Haley
两件事情:1)你的文档类型(map)与根元素(favoris)不匹配。 2)元素“station”未声明。你应该添加一个元素声明来声明“station”,然后将“favoris”更改为“map”(或更改文档类型和元素声明)。 - Daniel Haley
抱歉,你能在这里写一下吗?因为我对文档类型的东西完全陌生... :=) - KitAndKat
<!DOCTYPE favoris[<!ELEMENT favoris (station*) > <!ELEMENT station (#PCDATA)> <!ATTLIST station id ID #REQUIRED> ]> - KitAndKat
4个回答

11

您输入的XML无效。应该是:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE favoris [
    <!ELEMENT favoris (station)+>
    <!ELEMENT station (#PCDATA)>
    <!ATTLIST station id ID #REQUIRED>
]>
<favoris>
    <station id="i5">test1</station>
    <station id="i6">test1</station>
    <station id="i8">test1</station>
</favoris>

如@DevNull所说,为了完全有效,您不能编写<station id="5">test1</station>(但是对于Java来说,即使存在这个问题,它仍然可以正常工作)。


DOCTYPE在输出的XML文档中被删除:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<favoris>
    <station id="i5">new value</station>
    <station id="i6">test1</station>
    <station id="i8">test1</station>
</favoris>

我还没有找到缺失DTD的解决方案,但是你可以通过设置外部DTD来解决:

xformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, "favoris.dtd");

结果(示例)文档:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE favoris SYSTEM "favoris.dtd">
<favoris>
    <station id="i5">new value</station>
    <station id="i6">test1</station>
    <station id="i8">test1</station>
</favoris>

编辑:

我认为使用 Transformer 类无法保存内联DTD(见此处)。如果您不能使用外部DTD引用,则可以使用DOM Level 3的 LSSerializer 类:

DOMImplementationLS domImplementationLS =
    (DOMImplementationLS) dom.getImplementation().getFeature("LS","3.0");
LSOutput lsOutput = domImplementationLS.createLSOutput();
FileOutputStream outputStream = new FileOutputStream("output.xml");
lsOutput.setByteStream((OutputStream) outputStream);
LSSerializer lsSerializer = domImplementationLS.createLSSerializer();
lsSerializer.write(dom, lsOutput);
outputStream.close();

如何使用 LSSerializer 输出指定的 DTD(我找不到任何选项可以添加 standalone="yes" ...):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE favoris [<!ELEMENT favoris (station)+>
<!ELEMENT station (#PCDATA)>
<!ATTLIST station id ID #REQUIRED>
]>
<favoris>
    <station id="i5">new value</station>
    <station id="i6">test1</station>
    <station id="i8">test1</station>
</favoris> 

另一种方法是使用Apache Xerces2-J的XMLSerializer类:

import org.apache.xml.serialize.OutputFormat;
import org.apache.xml.serialize.XMLSerializer;
...

XMLSerializer serializer = new XMLSerializer();
serializer.setOutputCharStream(new java.io.FileWriter("output.xml"));
OutputFormat format = new OutputFormat();
format.setStandalone(true);
serializer.setOutputFormat(format);
serializer.serialize(dom);

结果:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE favoris [<!ELEMENT favoris (station)+>
<!ELEMENT station (#PCDATA)>
<!ATTLIST station id ID #REQUIRED>
]>
<favoris>
    <station id="i5">new value</station>
    <station id="i6">test1</station>
    <station id="i8">test1</station>
</favoris>

8

这个回答只是对 @Grzegorz Szpetkowski 的回答的补充,解释为什么它起作用。

你失去了文档类型定义,因为你使用了 Transform 类,它会产生一个 XSL 转换。在 XSLT 树模型中,没有 DOCTYPE 声明或文档类型定义对象/节点。当解析器将文档交给 XSLT 处理器时,文档类型信息就丢失了,因此无法保留或复制。XSLT 提供了一些控制输出树序列化的方式,包括添加一个带有公共或系统标识符的 <!DOCTYPE ... > 声明。这些标识符的值需要事先知道,不能从输入树中读取。创建或保留嵌入的 DTD 或实体声明也不受支持(尽管可以通过使用 disable-output-escaping="yes" 将其作为文本输出来绕过这个障碍)。

为了保留 DTD,你需要使用 XML 序列化器而不是 XSL 转换,就像 Grzegorz 已经建议的那样。


非常感谢您提供这么精确的解释!现在我明白为什么不可能了...由于这是一个Android应用程序,我无法真正使用所有这些变通方法。所以...我手动将我的DOM转换为字符串,首先将doctype附加到我的stringbuilder中!:/谢谢! - KitAndKat

2

@Grzegorz Szpetkowski提出了使用外部DTD的好主意。然而,如果保留这些station/@id值,XML仍然无效。

任何类型为“ID”的属性都不能以数字开头。你需要加上一些东西,比如“s”代表station:

<!DOCTYPE favoris [
<!ELEMENT favoris (station*)      > 
<!ELEMENT station (#PCDATA)       > 
<!ATTLIST station 
          id       ID   #REQUIRED > 
]>
<favoris>
  <station id="s5">test1</station>
  <station id="s6">test1</station>
  <station id="s8">test1</station>
</favoris>

你说得很对,我忘记了那个规则 :) 然而即使有这个问题,输出的XML文档仍然使用LSSerializer类来内联DTD,而不是使用Transformer方法。 - Grzegorz Szpetkowski

0

我曾经遇到过几乎相同的问题,后来找到了this,它可以使用transform解决。但是它有一定的限制,因为它只允许引用DTD,并且如果文档的doctype可能会变化,则需要进行一些工作。不过在我的情况下已经足够了,我只需要在转换后硬编码xhtml doctype。

xformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "publicId");
xformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, "systemId");

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