Home > Software design >  JAXB - Load XML file with different namespaces
JAXB - Load XML file with different namespaces

Time:12-25

I need to load an XML files, but there exists two identical formats of the file, save for the namespace being different - in my simplified example, apple:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns2:container xmlns:ns2="apple">
</ns2:container>

pear:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns2:container xmlns:ns2="pear">
</ns2:container>

The XmlRootElement references a specific namespace, and so I can't process both files the same way:

public class NamespaceTest {
    @XmlRootElement(namespace = "apple")
    public static class Container {
    }

    public static void main(final String[] args) throws Exception {
        // Correct namespace - works
        unmarshall("""
                <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
                <ns2:container xmlns:ns2="apple">
                </ns2:container>
            """);

        // Incorrect namespace - doesn't work
        unmarshall("""
            <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
            <ns2:container xmlns:ns2="pear">
            </ns2:container>
            """);
        }

        private static void unmarshall(final String xml) throws Exception {
            try (Reader reader = new StringReader(xml)) {
                System.out.println(JAXBContext.newInstance(Container.class).createUnmarshaller().unmarshal(reader));
            }
        }
    }
}

Gives the output:

com.my.app.NameSpaceTest$Container@77167fb7
Exception in thread "main" javax.xml.bind.UnmarshalException: unexpected element (uri:"pear", local:"container"). Expected elements are <{apple}container>

At the moment I've got this working in a sub-optimal way by modifying the data as it's being read, using https://stackoverflow.com/a/50800021 - but I'd like to move this into JAXB if possible.

public class NameSpaceTest {
    @XmlRootElement(namespace = "apple")
    public static class Container {
    }

    public static void main(final String[] args) throws Exception {
        // Correct namespace
        unmarshall("""
            <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
            <ns2:container xmlns:ns2="apple">
            </ns2:container>
            """);

        // Incorrect namespace
        unmarshall("""
            <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
            <ns2:container xmlns:ns2="pear">
            </ns2:container>
            """);
        }

        private static void unmarshall(final String xml) throws Exception {
        try (Reader reader = new TranslatingReader(new BufferedReader(new StringReader(xml))) {
            @Override
            public String translate(final String line) {
            return line.replace("pear", "apple");
            }
        }) {
            System.out.println(JAXBContext.newInstance(Container.class).createUnmarshaller().unmarshal(reader));
        }
    }

    /** @see <a href="https://stackoverflow.com/a/50800021">Source</a> */
    private abstract static class TranslatingReader extends Reader {
        private final BufferedReader input;
        private StringReader output = new StringReader("");

        public TranslatingReader(final BufferedReader input) {
            this.input = input;
        }

        public abstract String translate(final String line);

        @Override
        public int read(final char[] cbuf, int off, int len) throws IOException {
            int read = 0;

            while (len > 0) {
            final int nchars = output.read(cbuf, off, len);

            if (nchars == -1) {
                final String line = input.readLine();

                if (line == null) {
                break;
                } else {
                output = new StringReader(translate(line)   System.lineSeparator());
                }
            } else {
                read  = nchars;
                off  = nchars;
                len -= nchars;
            }
            }

            if (read == 0) {
            read = -1;
            }

            return read;
        }

        @Override
        public void close() throws IOException {
            input.close();
            output.close();
        }
    }
}

Output:

com.my.app.NameSpaceTest$Container@6ce139a4
com.my.app.NameSpaceTest$Container@18ce0030

CodePudding user response:

Assumptions

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>jaxb-test</artifactId>
  <version>1.0-SNAPSHOT</version>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
  </properties>
  <dependencies>
    <dependency> 
      <groupId>com.sun.xml.bind</groupId>
      <artifactId>jaxb-impl</artifactId>
      <version>3.0.2</version> <!-- latest, depends on jakarta.xml.bind:jakarta.xml.bind-api:3.0.1 -->
    </dependency>
  </dependencies>
</project>

OOP-Solution

We abstract (public) Container, and introduce (private or the visibility of our choice(, empty)) implementations to it, with correct qName:

public class NamespaceTest {

  public static interface Container {
  }

  @XmlRootElement(namespace = "apple", name = "container")
  private static class ContainerApple implements Container {

  }

  @XmlRootElement(namespace = "pear", name = "container")
  private static class ContainerPear implements Container {

  }
  ...

..!

With identical main method, unmarshall would (still) look like:

  ...
  private static void unmarshall(final String xml) throws Exception {
    Unmarshaller umler = CTXT.createUnmarshaller();
    try ( Reader reader = new StringReader(xml)) {
      System.out.println(umler.unmarshal(reader)
      );
    }
  }

  private static final JAXBContext CTXT = initContext();

  private static JAXBContext initContext() {
    try {
      return JAXBContext.newInstance(ContainerApple.class, ContainerPear.class);
    } catch (JAXBException ex) {
      throw new IllegalStateException("Could not initialize jaxb context.");
    }
  }
}
  • Singleton JAXBContext.
  • (static) Initialization with:
    • catch exception and re-throw (runtime/unchecked).
    • all (known) jaxb classes/packages/context(configs).

Prints Us:

com.example.jaxb.test.NamespaceTest$ContainerApple@4493d195
com.example.jaxb.test.NamespaceTest$ContainerPear@2781e022

CodePudding user response:

Filtering the data as it's being read is the right approach (JAXB, or data binding in general, isn't an ideal technology choice if you have to handle versions and variants of the vocabulary). But filter it using a SAX filter, not at the stream level.

Alternatively, normalise the data using an XSLT transformation before processing it using JAXB.

CodePudding user response:

One option is to use a custom org.xml.sax.ContentHandler that rewrites the sax events for namespaces before it delegates to the "normal" Content Handler for jaxb.

Here a self contained example:

import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

public class JaxbSaxRewriteNamespaceExample {

    @XmlRootElement(name = "container", namespace = "apple")
    @XmlAccessorType(XmlAccessType.NONE)
    static class Container {

        @XmlAttribute(namespace = "apple")
        private String attribute;
        @XmlElement(namespace = "apple")
        private String element;

        public String getAttribute() {
            return attribute;
        }

        public String getElement() {
            return element;
        }
    }

    public static void main(String[] args) throws Exception {
        String orangeXml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n"
                  "<ns2:container xmlns:ns2=\"apple\" ns2:attribute=\"oranges\"><ns2:element>Orange Element</ns2:element>\r\n"
                  "</ns2:container>";
        String appleXml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n"
                  "<ns2:container xmlns:ns2=\"apple\" ns2:attribute=\"apples\"><ns2:element>Apple Element</ns2:element>\r\n"
                  "</ns2:container>";

        JAXBContext jc = JAXBContext.newInstance(Container.class);

        Container orange = read(jc, orangeXml, Collections.singletonMap("orange", "apple"));
        Container apple = read(jc, appleXml, Collections.emptyMap());
        System.out.println(orange.getAttribute());
        System.out.println(orange.getElement());

        System.out.println(apple.getAttribute());
        System.out.println(apple.getElement());

    }

    private static Container read(JAXBContext jc, String xml, Map<String, String> namespaceMapping) throws Exception {
        UnmarshallerHandler unmarshallerHandler = jc.createUnmarshaller().getUnmarshallerHandler();

        SAXParserFactory spf = SAXParserFactory.newInstance();
        spf.setNamespaceAware(true); // Make sure sax parser is namespace aware

        SAXParser sp = spf.newSAXParser();
        XMLReader xr = sp.getXMLReader();
        // Wrap the Jaxb ContentHandler with the custome NamespaceRenamer
        xr.setContentHandler(new RenameNamespaceContentHandler(unmarshallerHandler, namespaceMapping));

        // See javadoc of InputSource for more options to pass in data, e.g. InputStream
        InputSource inputSource = new InputSource(new StringReader(xml)); //
        xr.parse(inputSource);
        return (Container) unmarshallerHandler.getResult();
    }

    public static class RenameNamespaceContentHandler implements ContentHandler {

        private final ContentHandler delegate;

        private final Map<String, String> namespaceMapping;

        public RenameNamespaceContentHandler(ContentHandler delegate, Map<String, String> namespaceMapping) {
            this.delegate = delegate;
            this.namespaceMapping = namespaceMapping;
        }

        @Override
        public void setDocumentLocator(Locator locator) {
            delegate.setDocumentLocator(locator);
        }

        @Override
        public void startDocument() throws SAXException {
            delegate.startDocument();
        }

        @Override
        public void endDocument() throws SAXException {
            delegate.endDocument();
        }

        @Override
        public void startPrefixMapping(String prefix, String uri) throws SAXException {
            if (namespaceMapping.containsKey(uri)) {
                delegate.startPrefixMapping(prefix, namespaceMapping.get(uri));
            }
            delegate.startPrefixMapping(prefix, uri);
        }

        @Override
        public void endPrefixMapping(String prefix) throws SAXException {
            delegate.endPrefixMapping(prefix);
        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
            delegate.startElement(uri, localName, qName, atts);
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            delegate.endElement(uri, localName, qName);
        }

        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            delegate.characters(ch, start, length);
        }

        @Override
        public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
            delegate.ignorableWhitespace(ch, start, length);
        }

        @Override
        public void processingInstruction(String target, String data) throws SAXException {
            delegate.processingInstruction(target, data);
        }

        @Override
        public void skippedEntity(String name) throws SAXException {
            delegate.skippedEntity(name);
        }

    }

}
  • Related