Search code examples
spring-wsjaxb2unmarshallingxml-validation

Custom validation error messages using Jaxb2marshaller and spring-ws


I have a running spring-ws project that can unmarshal requests using Jax2b, but when unmarshalling of integers/booleans fail I get an error message with little detail and often without the name of the invalid element. E.g.:

org.springframework.oxm.UnmarshallingFailureException: JAXB unmarshalling exception; nested     exception is javax.xml.bind.UnmarshalException
 - with linked exception:
[org.xml.sax.SAXParseException; cvc-datatype-valid.1.2.1: '' is not a valid value for 'integer'.]

This also becomes the content of the SOAPFault response from my webservice.

I am trying to change the message to include the element name. I'm using a ValidationEventHandler to change the message by throwing a RuntimeException from the event handler, but it only works i some cases.

ValidationEventHandler:

@Component
public class ValidationEventHandlerImpl implements ValidationEventHandler {

    @Override
    public boolean handleEvent(ValidationEvent event) {

        String message = event.getMessage();
        String linkedMessage = "";
        if(event.getLinkedException() != null)
            linkedMessage = event.getLinkedException().toString();

        boolean ignoreValidationEvent = true;            
        if(message.contains("NumberFormatException") || 
                message.contains("is not a valid value") || 
                linkedMessage.contains("NumberFormatException") || 
                linkedMessage.contains("is not a valid value")){
            ignoreValidationEvent = false;
        }

        if(ignoreValidationEvent){
            return true;
        }else{
            String nodeName = "";
            if(event.getLocator() != null && event.getLocator().getNode() != null)
                nodeName = event.getLocator().getNode().getNodeName();

            //This is the important line
            throw new RuntimeException("Error parsing '" + nodeName + "': " + event.getMessage());

        }
    }

}

It successfully changes:

JAXB unmarshalling exception; nested exception is javax.xml.bind.UnmarshalException: Not a number: 32g321
 - with linked exception:
[java.lang.NumberFormatException: Not a number: 32g321]

to: RuntimeException message: "java.lang.RuntimeException: Error parsing 'MyNodeName': Not a number: 32g321"

(Event Severity: ERROR)

But it does not work when I want it to change:

JAXB unmarshalling exception; nested exception is javax.xml.bind.UnmarshalException
 - with linked exception:
[org.xml.sax.SAXParseException; cvc-datatype-valid.1.2.1: '' is not a valid value for 'integer'.]

to: RuntimeException message: "java.lang.RuntimeException: Error parsing 'MyNodeName': '' is not a valid value for 'integer'".

The RuntimeException is ignored and the SAXParseException is thrown instead and added to the SOAPFault reponse.

(Event Severity: FATAL_ERROR)

Spring configuration for Jaxb2Marshalling:

<bean id="jaxb2MarshallerContact" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
    <property name="classesToBeBound">
        <list>
            <value>com.model.oxm.ContactRequest</value>
            <value>com.model.oxm.ContactResponse</value>
        </list>
    </property>
    <property name="marshallerProperties">
        <map>
            <entry>
                <key>
                    <util:constant static-field="javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT" />
                </key>
                <value type="boolean">true</value>
            </entry>
            <entry>
                <key>
                    <util:constant static-field="javax.xml.bind.Marshaller.JAXB_FRAGMENT" />
                </key>
                <value type="boolean">true</value>
            </entry>
        </map>
    </property>
    <property name="schema" ref="ContactServiceSchema" />
    <property name="validationEventHandler" ref="validationEventHandlerImpl" />
</bean>

<bean id="ContactServiceSchema" class="org.springframework.core.io.ClassPathResource">
    <constructor-arg value="WEB-INF/schemas/ContactService.xsd" />
</bean> 

Endpoint:

@Endpoint
public class ContactEndpoint {

    private Logger logger = Logger.getLogger(ContactEndpoint.class);

    @Autowired
    private ContactService contactService;

    private static final String NAMESPACE_URI = "http://mydomain/schemas";

    @PayloadRoot(namespace = NAMESPACE_URI, localPart = "ContactRequest")     
    @ResponsePayload
    public ContactResponse handleContactRequest(@RequestPayload ContactRequest contactRequest) throws Exception {
    ...
  • How can I return a custom message instead of the SAXParseException message?

  • Is there a better way of implementing this, e.g. using ValidationErrorHandler?

Thanks!


Solution

  • I finally found a way around this issue. Instead of throwing a new RuntimeException from the ValidationEventHandler, I added it as a suppressed exception on the events linked exception:

    event.getLinkedException().addSuppressed(new RuntimeException(errorMessage));
    

    and in the Endpoint I changed the RequestPayload the soapenvelope instead. The marshallingService wraps the jax2bmarshaller:

    @PayloadRoot(namespace = NAMESPACE_URI, localPart = "ContactRequest")     
    @ResponsePayload
    public ContactResponse handleContactRequest(@RequestPayload  SoapEnvelope soapEnvelope) throws Exception {
        ContactRequest contactRequest = marshallingService.unmarshalContact(soapEnvelope.getBody().getPayloadSource());
    

    the marshallingService catch the exception, extract my suppressed exception message and throws that instead:

    ((UnmarshalException) xmlMappingException.getCause()).getLinkedException().getSuppressed()[0].getLocalizedMessage();
    

    It is not an elegant solution but the endpoint produces much better error messages than before.