Ignoring undefined element references during unmarshalling in JAXB

Viewed 608

I'm currently working on a project requesting web services using CXF framework.

For some reasons, I started to receive invalid XML SOAP(missing element with ID referenced from ) response that results in throwing exception during unmarshalling to POJO instance.

Example:

XML excerpt where attribute ref references to an element with identifier Person1 that does not exist in XML.

<ext:Applicant s:ref="Person1"/>

where ref is IDREF type in XSD schema

<attribute name="ref" type="IDREF"/>

Exception thrown by JAXB

javax.xml.ws.soap.SOAPFaultException: Unmarshalling Error: Undefined ID "Person1". ] with root cause
javax.xml.bind.UnmarshalException: Undefined ID "Person1".
    at com.sun.xml.bind.v2.runtime.unmarshaller.UnmarshallingContext.handleEvent(UnmarshallingContext.java:744) ~[jaxb-impl-2.3.1.jar!/:2.3.1]
    at com.sun.xml.bind.v2.runtime.unmarshaller.UnmarshallingContext.errorUnresolvedIDREF(UnmarshallingContext.java:795) ~[jaxb-impl-2.3.1.jar!/:2.3.1]
    at com.sun.xml.bind.v2.runtime.reflect.TransducedAccessor$IDREFTransducedAccessorImpl$1.run(TransducedAccessor.java:330) ~[jaxb-impl-2.3.1.jar!/:2.3.1]
    at...

Is there a way how to make JAXB ignore missing references and unmarshall incoming XML response without throwing exception?

2 Answers

The XML object is not provided in the request to discuss more specifically. There is a handy workaround to create CustomAdapter for JAX-B objects to determine how to marshal and unmarshal the object.

As an example, the below CustomAdapter could be implemeted:

public static class CustomAdapter extends XmlAdapter<Object, Person1> {

    @Override
    public Object marshal(Person1 value) {
        // your implementation to marshal
    }

    @Override
    public Person1 unmarshal(Object value) {
        // your implementation to unmarshal
    }
}

XmlAdapter is in javax.xml.bind.annotation.adapters.XmlAdapter package.

If you are using Person1 as a field in other objects (composition), you could make it annotated with @XmlJavaTypeAdapter as below:

  @XmlJavaTypeAdapter(CustomAdapter.class)
  private Person1 person1;

Please let me know if it helps out.

Possible solution could be creating a decorator for XMLEventReader, which will filter out the IDREF attributes (or other attributes if needed):

public class IdRefFilteringReader implements XMLEventReader {

    /**
     * QName of the attribute to be removed
     */
    private final static QName QNAME = new QName("http://www.w3.org/2001/XMLSchema", "ref");
    
    /**
     * Delegate XML event reader
     */
    private final XMLEventReader delegate;

    /**
     * XML event factory
     */
    private final XMLEventFactory eventFactory = XMLEventFactory.newInstance();

    /**
     * Constructor injects delegate
     */
    public IdRefFilteringReader(XMLEventReader delegate) {
        this.delegate = delegate;
    }

    /**
     * Remove attributes with matching QName
     */
    @Override
    public XMLEvent nextEvent() throws XMLStreamException {
        XMLEvent event = delegate.nextEvent();
        if (event.isStartElement()) {
            StartElement startElement = event.asStartElement();
            Attribute attr = startElement.getAttributeByName(QNAME);
            // if attribute is present, create new XMLEvent with same
            // prefix, namespace, name and other attributes except one
            // which should be removed
            if(attr != null) {
                String prefix = startElement.getName().getPrefix();
                String uri = startElement.getName().getNamespaceURI();
                String localname = startElement.getName().getLocalPart();
                List<Attribute> attributes = new ArrayList<>();
                startElement.getAttributes().forEachRemaining(a -> {
                    if(!a.getName().equals(attr.getName())) {
                        attributes.add(a);
                    }
                });
                return eventFactory.createStartElement(
                        prefix, 
                        uri, 
                        localname, 
                        attributes.iterator(),
                        startElement.getNamespaces()
                );
            }
        }
        return event;
    }

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

    @Override
    public XMLEvent peek() throws XMLStreamException {
        return delegate.peek();
    }

    @Override
    public String getElementText() throws XMLStreamException {
        return delegate.getElementText();
    }

    @Override
    public XMLEvent nextTag() throws XMLStreamException {
        return delegate.nextTag();
    }

    @Override
    public Object getProperty(String name) throws IllegalArgumentException {
        return delegate.getProperty(name);
    }

    @Override
    public void close() throws XMLStreamException {
        delegate.close();
    }

    @Override
    public Object next() {
        return delegate.next();
    }

}

To use this reader it's needed to pass it to unmarshaller instance, e.g.:

    // create unmarshaller
    JAXBContext ctx = JAXBContext.newInstance(Applicant.class);
    Unmarshaller unmarshaller = ctx.createUnmarshaller();
    
    // create regular XML event reader and filtered XML event reader
    XMLInputFactory xif = XMLInputFactory.newInstance();
    XMLEventReader reader = xif.createXMLEventReader(new StreamSource(new StringReader(xml))); 
    XMLEventReader filteringReader = new IdRefFilteringReader(reader);
    
    // unmarshall XML using filtering reader
    Applicant applicant = unmarshaller.unmarshal(filteringReader, Applicant.class);
Related