I believe I found a glaring bug in JDK 10+. I've reported it to Oracle and since I've had no response I'm documenting here and maybe the community can confirm. This bug does NOT exist in apache xerces so that is the workaround. Document.normalizeDocument() returns inconsistent results past JDK 9.0.4 for namespaced attributes.
Ultimately the bug is that NamedNodeMapImpl.cloneMap(ArrayList list) changed implementation and DOMNormalizer is calling it relying on the list passed in as a method parameter to be populated rather than the return value. If it used the return value there would be no bug.
com.sun.org.apache.xerces.internal.dom.DOMNormalizer:885
// clone content of the attributes
attributes.cloneMap(fAttributeList);
Note this method is relying on fAttributeList being populated within cloneMap and ignoring the return value.
If we look at JDK 9 NamedNodeMapImpl.cloneMap
/**
* NON-DOM: copy content of this map into the specified ArrayList
*
* @param list ArrayList to copy information into.
* @return A copy of this node named map
*/
protected ArrayList cloneMap(ArrayList list) {
if (list == null) {
list = new ArrayList(5);
}
list.clear();
if (nodes != null) {
final int size = nodes.size();
for (int i = 0; i < size; ++i) {
list.add(nodes.get(i));
}
}
return list;
}
Now we look at JDK 10
/**
* NON-DOM: copy content of this map into the specified ArrayList
*
* @param list ArrayList to copy information into.
* @return A copy of this node named map
*/
protected List<Node> cloneMap(List<Node> list) {
if (nodes != null) {
list = new ArrayList<>(nodes);
}
return list;
}
JDK 9 clears the parameter passed in where JDK 10 "news" the parameter which loses it's reference in the calling DOMNoralizer and no contents are cloned. No contents cloned meaning no further processing on these attributes.
I see this a an obvious bug. Googling around got no hits except for someone saying it was questionable. https://bugs.openjdk.java.net/browse/JDK-8233750
Here is my test that I run under JDK 9 and then 10 to observes differences in output.
package test;
import java.io.StringWriter;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.bootstrap.DOMImplementationRegistry;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSInput;
import org.w3c.dom.ls.LSOutput;
import org.w3c.dom.ls.LSParser;
import org.w3c.dom.ls.LSSerializer;
public class main {
public static void main(final String[] args) {
try
{
final DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance();
final DOMImplementationLS impl = (DOMImplementationLS) registry.getDOMImplementation("LS");
final LSParser builder = impl.createLSParser(DOMImplementationLS.MODE_SYNCHRONOUS, null);
final LSInput input = impl.createLSInput();
input.setStringData("<xml/>");
final Document document = builder.parse(input);
final Element root = document.getDocumentElement();
// Generate a single element
final Element element = document.createElement("token");
final Attr attr = element.getOwnerDocument().createAttributeNS("http://blah.xsd", "wsu");
attr.setValue("Id");
element.setAttributeNodeNS(attr);
final Attr attr2 = element.getOwnerDocument().createAttributeNS("http://blah2.xsd", "wsu2");
element.setAttributeNodeNS(attr2);
final Attr attr3 = element.getOwnerDocument().createAttribute("aa");
element.setAttributeNodeNS(attr3);
final Attr attr4 = element.getOwnerDocument().createAttribute("zz");
element.setAttributeNodeNS(attr4);
final Attr attr5 = element.getOwnerDocument().createAttribute("tt");
element.setAttributeNodeNS(attr5);
root.appendChild(element);
for ( int i = 0; i < element.getAttributes().getLength(); i++ )
{
System.out.println("PRE normalise attr " + element.getAttributes().item(i).getNodeName());
}
System.out.println("PRE normalise " + prettyPrintFromDocument(document));
document.normalizeDocument();
for ( int i = 0; i < element.getAttributes().getLength(); i++ )
{
System.out.println("POST normalise attr " + element.getAttributes().item(i).getNodeName());
}
System.out.println("POST normalise " + prettyPrintFromDocument(document));
}
catch ( final Exception ex )
{
ex.printStackTrace();
}
}
/**
* Pretty prints a DOM document to a String.
*
* @param message the DOM document to serialise.
* @return a string representation of the DOM message parameter.
*/
private static String prettyPrintFromDocument(final Document message) {
try {
final DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance();
final DOMImplementationLS impl = (DOMImplementationLS) registry.getDOMImplementation("LS");
final LSSerializer writer = impl.createLSSerializer();
final LSOutput output = impl.createLSOutput();
final StringWriter sw = new StringWriter();
output.setEncoding("UTF-8");
output.setCharacterStream(sw);
try {
writer.getDomConfig().setParameter("format-pretty-print", Boolean.TRUE);
} catch (final Exception ignore) {
}
writer.write(message, output);
return sw.toString();
} catch (final RuntimeException e) {
throw e;
} catch (final Exception e) {
throw new RuntimeException(e);
}
}
}