Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8329424

StAXResult producing null StartDocument.location, causing NPE further

XMLWordPrintable

      ADDITIONAL SYSTEM INFORMATION :
      > java -version
      java version "21.0.2" 2024-01-16 LTS
      Java(TM) SE Runtime Environment (build 21.0.2+13-LTS-58)
      Java HotSpot(TM) 64-Bit Server VM (build 21.0.2+13-LTS-58, mixed mode, sharing)


      A DESCRIPTION OF THE PROBLEM :
      After transforming an XML source to a StAXResult, the produced StartDocument event location is null which further causes NPE in at least one scenario. The XMLEvent.getLocation() API doesn't suggest it could be null, so it should never be:

      * https://docs.oracle.com/en/java/javase/21/docs/api/java.xml/javax/xml/stream/events/XMLEvent.html#getLocation()


      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      1. Run the provided test case: `java TransformPipelineTest.java`


      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      No failure:

      ```
      Event buffer:
      StartDocumentEvent(location: <non-null>)
      ...

      <hello>World</hello>
      ```

      ACTUAL -
      Failing:

      ```
      Event buffer:
      StartDocumentEvent(location: null)
      StartElementEvent(location: DummyLocation)
      CharacterEvent(location: DummyLocation)
      EndElementEvent(location: DummyLocation)
      EndDocumentEvent(location: DummyLocation)

      Exception in thread "main" java.lang.NullPointerException: Cannot invoke "javax.xml.stream.Location.getSystemId()" because the return value of "javax.xml.stream.events.XMLEvent.getLocation()" is null
      at java.xml/javax.xml.transform.stax.StAXSource.<init>(StAXSource.java:122)
      at TransformPipelineTest.main(TransformPipelineTest.java:52)
      ```


      ---------- BEGIN SOURCE ----------
      //package ;

      import java.io.StringReader;
      import java.util.ArrayDeque;
      import java.util.Iterator;
      import java.util.Optional;
      import java.util.Queue;

      import javax.xml.XMLConstants;
      import javax.xml.namespace.NamespaceContext;
      import javax.xml.stream.Location;
      import javax.xml.stream.XMLEventReader;
      import javax.xml.stream.XMLEventWriter;
      import javax.xml.stream.XMLInputFactory;
      import javax.xml.stream.XMLStreamException;
      import javax.xml.stream.events.XMLEvent;
      import javax.xml.transform.OutputKeys;
      import javax.xml.transform.Result;
      import javax.xml.transform.Source;
      import javax.xml.transform.Transformer;
      import javax.xml.transform.TransformerConfigurationException;
      import javax.xml.transform.TransformerFactory;
      import javax.xml.transform.stax.StAXResult;
      import javax.xml.transform.stax.StAXSource;
      import javax.xml.transform.stream.StreamResult;
      import javax.xml.transform.stream.StreamSource;

      public class TransformPipelineTest {

          public static void main(String[] args) throws Exception {
              Source source = new StreamSource(new StringReader("<hello>World</hello>"));

              Transformer transformation1 = sampleTransformation();
              //Transformer transformation1 = defaultIdentity();
              XMLEventBufferWriter bufferWriter = new XMLEventBufferWriter();
              Result intermediate = new StAXResult(bufferWriter);
              transformation1.transform(source, intermediate);

              System.out.println("Event buffer:");
              for (XMLEvent event : bufferWriter.getBuffer()) {
                  Location location = event.getLocation();
                  System.out.printf("\t%s(location: %s)%n",
                          event.getClass().getSimpleName(),
                          (location == null) ? "null" :
                                  location.getClass().getSimpleName());
              }
              System.out.println();

              //Transformer transformation2 = sampleTransformation();
              Transformer transformation2 = defaultIdentity();
              XMLEventBufferReader bufferReader = new XMLEventBufferReader(bufferWriter.getBuffer());
              transformation2.transform(new StAXSource(bufferReader),
              //transformation2.transform(new NullLocationStAXSource(bufferReader),
                                        new StreamResult(System.out));
          }

          // An identity transform, really
          private static final String SAMPLE_XSLT = "<xsl:stylesheet "
                    + "version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform&#39;>"
                  + "<xsl:template match='@*|node()'>"
                    + "<xsl:copy>"
                      + "<xsl:apply-templates select='@*|node()' />"
                    + "</xsl:copy>"
                  + "</xsl:template>"
                + "</xsl:stylesheet>";

          static Transformer sampleTransformation()
                  throws TransformerConfigurationException {
              return transformation(Optional
                      .of(new StreamSource(new StringReader(SAMPLE_XSLT))));
          }

          static Transformer defaultIdentity()
                  throws TransformerConfigurationException {
              return transformation(Optional.empty());
          }

          private static Transformer transformation(Optional<Source> xslt)
                  throws TransformerConfigurationException {
              TransformerFactory tf = TransformerFactory.newInstance();
              tf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);

              Transformer transformer = xslt.isPresent()
                                        ? tf.newTransformer(xslt.get())
                                        : tf.newTransformer();
              transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
              return transformer;
          }

      } // class TransformPipelineTest


      class XMLEventBufferReader implements XMLEventReader {

          private Iterator<XMLEvent> events;
          private XMLEvent nextEvent;

          public XMLEventBufferReader(Iterable<XMLEvent> source) {
              this.events = source.iterator();
          }

          @Override
          public Object getProperty(String name) throws IllegalArgumentException {
              throw notImplementedException();
          }

          @Override
          public boolean hasNext() {
              return events.hasNext();
          }

          @Override
          public Object next() {
              return nextEvent();
          }

          @Override
          public XMLEvent nextEvent() {
              XMLEvent next = nextEvent;
              if (next == null) {
                  next = events.next();
              } else {
                  nextEvent = null;
              }
              return next;
          }

          @Override
          public XMLEvent peek() {
              XMLEvent next = nextEvent;
              if (next == null) {
                  if (!hasNext()) return null;

                  next = nextEvent();
                  nextEvent = next;
              }
              return next;
          }

          @Override
          public XMLEvent nextTag() throws XMLStreamException {
              throw notImplementedException();
          }

          @Override
          public String getElementText() throws XMLStreamException {
              throw notImplementedException();
          }

          private static IllegalStateException notImplementedException() {
              return new IllegalStateException("Not implemented");
          }

          @Override
          public void close() {
              // no-op
          }

      } // class XMLEventBufferReader


      class XMLEventBufferWriter implements XMLEventWriter {

          private final Queue<XMLEvent> buffer;

          public XMLEventBufferWriter() {
              buffer = new ArrayDeque<>();
          }

          public Iterable<XMLEvent> getBuffer() {
              return buffer;
          }

          @Override
          public void add(XMLEvent event) {
              buffer.add(event);
          }

          @Override
          public void add(XMLEventReader reader) {
              throw notImplementedException();
          }

          @Override
          public String getPrefix(String uri) {
              throw notImplementedException();
          }

          @Override
          public void setPrefix(String prefix, String uri) {
              throw notImplementedException();
          }

          @Override
          public void setDefaultNamespace(String uri) {
              throw notImplementedException();
          }

          @Override
          public void setNamespaceContext(NamespaceContext context) {
              throw notImplementedException();
          }

          @Override
          public NamespaceContext getNamespaceContext() {
              throw notImplementedException();
          }

          private static IllegalStateException notImplementedException() {
              return new IllegalStateException("Not implemented");
          }

          @Override
          public void flush() {
              // no-op
          }

          @Override
          public void close() {
              // no-op
          }

      } // class XMLEventBufferWriter


      class NullLocationStAXSource extends StAXSource {

          private final XMLEventReader xmlEventReader;

          public NullLocationStAXSource(XMLEventReader xmlEventReader)
                  throws XMLStreamException {
              this("tag:example.net,2024-03:dummy", xmlEventReader);
          }

          public NullLocationStAXSource(String systemId, XMLEventReader xmlEventReader)
                  throws XMLStreamException {
              super(dummyReader(systemId));
              this.xmlEventReader = xmlEventReader;
          }

          @Override
          public XMLEventReader getXMLEventReader() {
              return xmlEventReader;
          }

          private static XMLEventReader dummyReader(String systemId)
                  throws XMLStreamException {
              XMLInputFactory xif = XMLInputFactory.newInstance();
              return xif.createXMLEventReader(systemId, new StringReader("<dummy />"));
          }

      } // class NullLocationStAXSource

      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      More of a hack than an actual solution – see the NullLocationStAXSource class in the provided test case. It doesn't work with the Woodstox StAX provider plugged into the runtime:

      ```
      Event buffer:
              StartDocumentEventImpl(location: null)
              SimpleStartElement(location: null)
              CharactersEventImpl(location: null)
              EndElementEventImpl(location: null)
              EndDocumentEventImpl(location: null)

      Exception in thread "main" javax.xml.transform.TransformerException: javax.xml.transform.TransformerException: java.lang.NullPointerException: Cannot invoke "javax.xml.stream.Location.getSystemId()" because the return value of "javax.xml.stream.events.XMLEvent.getLocation()" is null
      ...
      java.lang.NullPointerException: Cannot invoke "javax.xml.stream.Location.getSystemId()" because the return value of "javax.xml.stream.events.XMLEvent.getLocation()" is null
      at java.xml/com.sun.org.apache.xalan.internal.xsltc.trax.StAXEvent2SAX$1.getSystemId(StAXEvent2SAX.java:286)
      at java.xml/com.sun.org.apache.xml.internal.dtm.ref.sax2dtm.SAX2DTM.setDocumentLocator(SAX2DTM.java:1610)
      at java.xml/com.sun.org.apache.xalan.internal.xsltc.trax.StAXEvent2SAX.handleStartDocument(StAXEvent2SAX.java:275)
      at java.xml/com.sun.org.apache.xalan.internal.xsltc.trax.StAXEvent2SAX.bridge(StAXEvent2SAX.java:160)
      at java.xml/com.sun.org.apache.xalan.internal.xsltc.trax.StAXEvent2SAX.parse(StAXEvent2SAX.java:114)
      at java.xml/com.sun.org.apache.xalan.internal.xsltc.dom.XSLTCDTMManager.getDTM(XSLTCDTMManager.java:294)
      at java.xml/com.sun.org.apache.xalan.internal.xsltc.dom.XSLTCDTMManager.getDTM(XSLTCDTMManager.java:214)
      at java.xml/com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl.getDOM(TransformerImpl.java:582)
      at java.xml/com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl.transform(TransformerImpl.java:783)
      at java.xml/com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl.transform(TransformerImpl.java:395)
      at TransformPipelineTest.main(TransformPipelineTest.java:53)
      at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
      at java.base/java.lang.reflect.Method.invoke(Method.java:580)
      at jdk.compiler/com.sun.tools.javac.launcher.Main.execute(Main.java:484)
      at jdk.compiler/com.sun.tools.javac.launcher.Main.run(Main.java:208)
      at jdk.compiler/com.sun.tools.javac.launcher.Main.main(Main.java:135)
      ```


      FREQUENCY : always

            joehw Joe Wang
            pnarayanaswa Praveen Narayanaswamy
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

              Created:
              Updated: