Serializing supplementary unicode characters into XML documents with Java
Since I didn't see any answer coming, and other people seem to have the same problem, I looked into it further...
To find the origin of the bug, I used the serializer
source code from Xalan 2.7.1
, which is also used in Xerces
.
org.apache.xml.serializer.dom3.LSSerializerImpl
uses org.apache.xml.serializer.ToXMLStream
, which extends org.apache.xml.serializer.ToStream
.
ToStream.characters(final char chars[], final int start, final int length)
handles the characters, and does not support unicode characters properly (note: org.apache.xml.serializer.ToTextSream
(which can be used with a Transformer
) does a better job in the characters method, but it only handles plain text and ignores all markup; one would think that XML files are text, but for some reason ToXMLStream
does not extend ToTextStream
).
org.apache.xalan.transformer.TransformerIdentityImpl
is also using org.apache.xml.serializer.ToXMLStream
(which is returned by org.apache.xml.serializer.SerializerFactory.getSerializer(Properties format)
), so it suffers from the same bug.
ToStream
is using org.apache.xml.serializer.CharInfo
to check if a character should be replaced by a String
, so the bug could also be fixed there instead of directly in ToStream
. CharInfo
is using a propery file, org.apache.xml.serializer.XMLEntities.properties
, with a list of character entities, so changing this file could also be a way to fix the bug, although so far it is designed just for the special XML characters (quot
,amp
,lt
,gt
). The only way to make ToXMLStream
use a different property file than the one in the package would be to add a org.apache.xml.serializer.XMLEntities.properties
file before in the classpath, which would not be very clean...
With the default JDK (1.6 and 1.7), TransformerFactory
returns a com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl
, which uses com.sun.org.apache.xml.internal.serializer.ToXMLStream
. In com.sun.org.apache.xml.internal.serializer.ToStream
, characters()
is sometimes calling processDirty()
, which calls accumDefaultEscape()
, which could handle unicode characters better, but in practice it does not seem to work (maybe processDirty
is not called for unicode characters)...
com.sun.org.apache.xml.internal.serialize.DOMSerializerImpl
is using com.sun.org.apache.xml.internal.serialize.XMLSerializer
, which supports unicode. Strangely enough, XMLSerialize
r comes from Xerces
, and yet it is not used by Xerces
when xalan
or xsltc
are on the classpath. This is because org.apache.xerces.dom.CoreDOMImplementationImpl.createLSSerializer
is using org.apache.xml.serializer.dom3.LSSerializerImpl
when it is available instead of org.apache.xerces.dom.DOMSerializerImpl
. With serializer.jar
on the classpath, org.apache.xml.serializer.dom3.LSSerializerImpl
is used. Warning: xalan.jar
and xsltc.jar
both reference serializer.jar
in the manifest, so serializer.jar
ends up on the classpath if it is in the same directory and either xalan.jar
or xsltc.jar
is on the classpath ! If only xercesImpl.jar
and xml-apis.jar
are on the classpath, org.apache.xerces.dom.DOMSerializerImpl
is used as the LSSerializer
, and unicode characters are properly handled.
CONCLUSION AND WORKAROUND: the bug lies in Apache's org.apache.xml.serializer.ToStream
class (renamed com.sun.org.apache.xml.internal.serializer.ToStream
inside the JDK). A serializer that handles unicode characters properly is org.apache.xml.serialize.DOMSerializerImpl
(renamed com.sun.org.apache.xml.internal.serialize.DOMSerializerImpl
inside the JDK). However, Apache prefers ToStream
instead of DOMSerializerImpl
when it is available, so maybe it behaves better for other things (or maybe it's just a reorganization). On top of that, they went as far as deprecating DOMSerializerImpl
in Xerces 2.9.0
. Hence the following workaround, which might have side effects :
when
Xerces
and Apache'sserializer
are on the classpath, replace "(doc.getImplementation()).createLSSerializer()
" by "new org.apache.xerces.dom.DOMSerializerImpl()
"when Apache's
serializer
is on the classpath (for instance because ofxalan
) but notXerces
, try to replace "(doc.getImplementation()).createLSSerializer()
" by "newcom.sun.org.apache.xml.internal.serialize.DOMSerializerImpl()
" (a fallback is necessary because this class might disappear in the future)
These 2 workarounds produce a warning when compiling.
I don't have a workaround for XSLT transforms
, but this is beyond the scope of the question. I guess one could do a transform to another DOM document and use DOMSerializerImpl
to serialize.
Some other workarounds, which might be a better solution for some people :
use
Saxon
with aTransformer
use XML documents with
UTF-16
encoding
Here is an example that worked for me. Code is written in Groovy running on Java 7, which you can easily translate to Java since I've used all Java APIs in the example. If you pass in a DOM document that has supplementary (plane 1) unicode characters and you will get back out a String which has those characters properly serialized. For example, if the document has a unicode Script L (see http://www.fileformat.info/info/unicode/char/1d4c1/index.htm), it will be serialized in the returned String as 𝓁
instead of ��
(which is what you will get with a 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()
}