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);
}
}
}