使用Java将补充的Unicode字符序列化为XML文档

12
我试图åº�列化包å�«è¡¥å……Unicode字符(例如U+1D49C,数学手写体大写字æ¯�A“ğ�’œâ€�)的DOM文档。创建一个具有这样字符的节点并ä¸�是问题(我å�ªéœ€å°†èŠ‚点值设置为UTF-16等效值"\uD835\uDC9C")。但是,在åº�列化时,Xalanå’ŒXSLTC(使用Transformer)以å�ŠXerces(使用LSSerializer)都会创建无效的字符å®�体,如"��"而ä¸�是"𝒜"。我å°�试了LSSerializerçš„"normalize-characters"å�‚数,但它ä¸�被支æŒ�。å�ªæœ‰Saxon在编ç �为Unicodeæ—¶æ‰�能正确处ç�†ï¼Œè€Œä¸�使用字符å®�体。
我�能在�践中使用Saxon(除其他�因外,我使用Java�程�,�想加载�一个jar),因此我正在寻找使用默认JDK库解决该问题的方法。是��能�带有补充Unicode字符的DOM文档中��有效的XML文档�列化?
[编辑] 我��还有其他人�到过这个问题: http://www.dragishak.com/?p=131 [编辑2] �际上,当我没有将xerces放在类路径上(使用的类是com.sun.org.apache.xml.internal.serialize.DOMSerializerImpl)时,LSSerializer似��以正常工作。对�com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl和Transformer�说,则�行。

你是否同时处理两端?如果是这样,您为什么不能在序列化端自己进行编码(base64、urlencode等),然后在反序列化端将其反转呢? - Chris Moran
1
这看起来像是序列化器中的明显错误。字符范围包括补充字符,明确排除代理项,因此XML中的“char”是Unicode标量值,并且存在一个有关字符引用的格式正确性约束:“使用字符引用引用的字符必须与Char的生成匹配。”这被Xalan和XSLTC违反了。 - Mike Samuel
1
我发现避免xalan序列化程序错误的一种方法是使用UTF-16编码... LSSerializer.writeToString也使用UTF-16... - Damien
你是说在这个XML文档上使用Xalan应用身份转换会产生其他结果吗?我简直不敢相信。 - Dimitre Novatchev
@Damien,问题是我没有安装Xalan。:( 另外,我没有看到Dragisa使用任何XSLT转换。我问过身份变换是否有这个问题。 - Dimitre Novatchev
显示剩余2条评论
2个回答

6

由于我没看到任何答案,并且其他人似乎也有同样的问题,所以我进一步研究了这个问题...

为了找出错误的起源,我使用了Xalan 2.7.1中的serializer源代码,该源代码也用于Xerces

org.apache.xml.serializer.dom3.LSSerializerImpl使用org.apache.xml.serializer.ToXMLStream,后者扩展org.apache.xml.serializer.ToStream

ToStream.characters(final char chars[], final int start, final int length)处理字符,但不能正确地支持Unicode字符(注意:org.apache.xml.serializer.ToTextSream(可用于Transformer)在characters方法中执行得更好,但它仅处理纯文本并忽略所有标记;人们会认为XML文件是文本,但由于某些原因,ToXMLStream没有扩展ToTextStream)。

org.apache.xalan.transformer.TransformerIdentityImpl也使用org.apache.xml.serializer.ToXMLStream(由org.apache.xml.serializer.SerializerFactory.getSerializer(Properties format)返回),因此它也受到相同错误的影响。

ToStream使用org.apache.xml.serializer.CharInfo来检查是否应将字符替换为String,因此也可以在其中修复错误,而不是直接在ToStream中。 CharInfo使用属性文件org.apache.xml.serializer.XMLEntities.properties,其中列出了字符实体的列表,因此更改此文件也可能是修复错误的一种方法,尽管到目前为止,它仅用于特殊的XML字符(quot,amp,lt,gt)。使ToXMLStream使用与包中不同的属性文件的唯一方法是在类路径中添加一个org.apache.xml.serializer.XMLEntities.properties文件,这并不是很干净...

使用默认的JDK(1.6和1.7),TransformerFactory返回一个com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl,其使用com.sun.org.apache.xml.internal.serializer.ToXMLStream。在com.sun.org.apache.xml.internal.serializer.ToStream中,characters()有时会调用processDirty(),后者调用accumDefaultEscape(),该方法可以更好地处理Unicode字符,但在实践中似乎不起作用(也许不能为Unicode字符调用processDirty)...

com.sun.org.apache.xml.internal.serialize.DOMSerializerImpl使用支持unicode的com.sun.org.apache.xml.internal.serialize.XMLSerializer。奇怪的是,XMLSerializer来自于Xerces,但当类路径中存在xalanxsltc时,Xerces不会使用它。这是因为org.apache.xerces.dom.CoreDOMImplementationImpl.createLSSerializer在可用时使用org.apache.xml.serializer.dom3.LSSerializerImpl而不是org.apache.xerces.dom.DOMSerializerImpl。如果类路径上有serializer.jar,则会使用org.apache.xml.serializer.dom3.LSSerializerImpl。警告:xalan.jarxsltc.jar都在清单文件中引用了serializer.jar,因此如果它们位于同一目录中并且其中一个在类路径上,则serializer.jar将出现在类路径上!如果类路径上只有xercesImpl.jarxml-apis.jar,则org.apache.xerces.dom.DOMSerializerImpl将作为LSSerializer使用,并正确处理unicode字符。

结论和解决方法:问题出在Apache的org.apache.xml.serializer.ToStream类(在JDK内部重命名为com.sun.org.apache.xml.internal.serializer.ToStream)。一个正确处理unicode字符的序列化器是org.apache.xml.serialize.DOMSerializerImpl(在JDK内部重命名为com.sun.org.apache.xml.internal.serialize.DOMSerializerImpl)。然而,当ToStream可用时,Apache更喜欢使用它,因此可能对其他事情有更好的表现(或者只是一种重新组织)。此外,他们甚至在Xerces 2.9.0中弃用了DOMSerializerImpl。因此,以下解决方法可能会产生副作用:

  • 当类路径上存在Xerces和Apache的serializer时,请将"(doc.getImplementation()).createLSSerializer()"替换为"new org.apache.xerces.dom.DOMSerializerImpl()"

  • 当类路径上存在Apache的serializer(例如由于xalan)但不存在Xerces时,请尝试将"(doc.getImplementation()).createLSSerializer()"替换为"new com.sun.org.apache.xml.internal.serialize.DOMSerializerImpl()"(需要回退,因为这个类可能会在将来消失)

这两种解决方法在编译时会产生警告。
我没有关于XSLT转换的解决方法,但这超出了问题的范围。我猜可以将其转换为另一个DOM文档,并使用DOMSerializerImpl进行序列化。
其他一些解决方法可能更适合某些人:
- 使用带有Transformer的Saxon - 使用UTF-16编码的XML文档

2
我在Jira上创建了一个错误报告:https://issues.apache.org/jira/browse/XALANJ-2560 - Damien
Christopher Taylor在jira上发表评论:“可能是XALANJ-2419的重复,该问题已附带了一个补丁。还在http://stackoverflow.com/questions/10511474/surrogate-pair-handling-in-xalan-2-7-1中提到。” Xalan最近又重新活跃起来,因此有望最终修复这个错误。 - Damien
你帮我找对了方向 :) 我使用的一个库本身就使用了xalan,而xalan-serializer是不必要的。尽管xalan带有xalan-serializer的依赖项,但放弃这个依赖项让我的库工作了,太棒了! - dr0i
嗯,实际上我认为它对xercesImpl的依赖需要xml-apis,去掉这个依赖使我的库工作。无论如何:万岁!注意:尽可能少地使用依赖项。 - dr0i

2

以下是我成功使用的示例。代码是用Groovy编写的,运行在Java 7上,你可以很容易地将其翻译成Java,因为我在示例中使用了所有的Java API。如果传入了一个包含补充(平面1)Unicode字符的DOM文档,则会返回一个正确序列化这些字符的字符串。例如,如果文档中有一个Unicode Script L(请参见http://www.fileformat.info/info/unicode/char/1d4c1/index.htm),它将被序列化为返回的字符串&#x1d4c1,而不是��(这是使用Xalan Transformer时得到的结果)。

import org.w3c.dom.Document
...

def String writeToStringLS( Document doc ) {
  def domImpl = doc.getImplementation()
  def implLS = domImpl.getFeature("LS", "3.0")
  def lsOutput = implLS.createLSOutput()
  lsOutput.encoding = "UTF-8"
  def bo = new ByteArrayOutputStream()
  def out = new BufferedWriter( new OutputStreamWriter( bo, "UTF-8") )
  lsOutput.characterStream = out
  def lsWriter = implLS.createLSSerializer()
  def result = lsWriter.write(doc, lsOutput)
  return bo.toString()
}

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