4

I was looking into Using BigDecimal in JAXB marshalling

What is happening by default, is that Exception gets thrown, but no handler is listening to it. As a result, such request reaches JAR-RS method, but with field thad was submitted with "incorrect" value, set to NULL.

I read in the JAVADOC, that a handler must be set, so that I could do something when such case happens. But I can't find an information on how to setup such handler.

My endpoint is something like:

@Path("/path")
public class MyResource {

    @POST
    @Path("something")
    public Response postSomething(JaxbAnnotatedRequest request) {
        //processing....
    }
}
Community
  • 1
  • 1
LaRRy
  • 626
  • 5
  • 20

2 Answers2

4

I don't see any real elegant way to handle this. But to answer your question

"I read in the JAVADOC, that a handler must be set, so that I could do something when such case happens. But I can't find an information on how to setup such handler."

the unmarshalling will be done in a MessageBodyReader. The ValidationEventHandler should be registered with the Unmarshaller, which is created for each request. So we would need to somehow tap into the currently used MessageBodyReader, which I am not really sure how to do, or if adding a handler this way is even configurable. If it is, I'm sure it's implementation specific.

One way though is to write your own MessageBodyReader to handle this specific type. For example

import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import javax.ws.rs.Consumes;
import javax.ws.rs.InternalServerErrorException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.Provider;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.ValidationEvent;
import javax.xml.bind.ValidationEventHandler;

@Provider
@Consumes(MediaType.APPLICATION_XML)
public class JaxbAnnotatedRequestMessageBodyReader
        implements MessageBodyReader<JaxbAnnotatedRequest> {

    JAXBContext context;

    public JaxbAnnotatedRequestMessageBodyReader() {
        try {
            context = JAXBContext.newInstance(JaxbAnnotatedRequest.class);
        } catch (JAXBException ex) {
            throw new InternalServerErrorException();
        }
    }

    @Override
    public boolean isReadable(Class<?> type, Type type1,
            Annotation[] antns, MediaType mt) {
        return JaxbAnnotatedRequest.class == type;
    }

    @Override
    public JaxbAnnotatedRequest readFrom(Class<JaxbAnnotatedRequest> type, Type type1,
            Annotation[] antns, MediaType mt, MultivaluedMap<String, String> mm,
            InputStream in) throws IOException, WebApplicationException {

        try {
            Unmarshaller unmarshaller = context.createUnmarshaller();
            unmarshaller.setEventHandler(new ValidationEventHandler() {

                @Override
                public boolean handleEvent(ValidationEvent event) {
                    String nfe = NumberFormatException.class.getCanonicalName();
                    if (nfe.equals(event.getMessage())) {
                        throw new WebApplicationException(Response.status(400)
                                .entity("Bad Number Formatting").build());
                    }
                    System.out.println(event.getMessage());
                    return false;
                }

            });
            JaxbAnnotatedRequest request
                    = (JaxbAnnotatedRequest) unmarshaller.unmarshal(in);
            return request;

        } catch (JAXBException ex) {
            throw new WebApplicationException(Response.status(400).entity(
                    "Caught JAXBEception: " + ex.toString()).build());
        }
    }
}

The ValidationEventHandler is set up on the Unmarshaller in the readFrom method.

Another option (though also with limitations), is to use Bean Validation, which is part of the JAX-RS 2.0 spec. You may want to look into your implementation's documentation, if you want to use this feature. The only problem with this is the say you were to use the @NotNull annotation, you would know if the request value was actually null, or if it was set null by JAXB, so you would need to send some generic message.

Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
2

In my opinion the problem you have is a validation issue. Input validation today is an inevitable responsibility of the developer. In case of data files like JSON or XML this can easily be done by using schema validation. This validation needs to be integrated into the data conversion process, recommendable in both marshalling and unmarhalling. In case of null values or any other inappropriate input, like a to short post code, etc. an JAXBException will be thrown, which in turn can be wrapped into a more expressive exception. Finally you have to declare an ExceptionMapper which will catch the exception and responde with an appropriaten message to the caller. Below you will find an example.


The entity class Address.java

@XmlRootElement(name = "address", namespace = DataEntity.NSP)
public class Address implements Serializable {

    /**
     * 
     */
    private static final long serialVersionUID = 2508433924387468026L;

    private Integer id;
    private String street;
    private String number;
    private Short zipCode;
    private String city;

    protected Address() {

    }

    public Address(final Integer id, final String street, final String number,
            final Short zipCode, final String city) {
        setId(id);
        setStreet(street);
        setNumber(number);
        setZipCode(zipCode);
        setCity(city);
    }

    @XmlAttribute
    public Integer getId() {
        return id;
    }

    private void setId(Integer id) {
        ValidationHelper.validateId(id);

        this.id = id;
    }

    @XmlElement(namespace = DataEntity.NSP)
    public String getStreet() {
        return street;
    }

    private void setStreet(String street) {
        ValidationHelper.validateAddressStreet(street);

        this.street = street;
    }

    @XmlElement(namespace = DataEntity.NSP)
    public String getNumber() {
        return number;
    }

    private void setNumber(String number) {
        ValidationHelper.validateAddressNumber(number);

        this.number = number;
    }

    @XmlElement(namespace = DataEntity.NSP)
    public Short getZipCode() {
        return zipCode;
    }

    private void setZipCode(Short zipCode) {
        ValidationHelper.validateAddressZipCode(zipCode);

        this.zipCode = zipCode;
    }

    @XmlElement(namespace = DataEntity.NSP)
    public String getCity() {
        return city;
    }

    private void setCity(String city) {
        ValidationHelper.validateAddressCity(city);

        this.city = city;
    }
}

The schema validation files:

<?xml version="1.0" encoding="ISO-8859-1"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns="https://localhost/JAX-RS_BookStore_Service/xsd/" targetNamespace="https://localhost/JAX-RS_BookStore_Service/xsd/"
    elementFormDefault="qualified" xmlns:pref="https://localhost/JAX-RS_BookStore_Service/xsd/">
    <xs:include schemaLocation="Types.xsd" />
    <xs:element name="address" type="addressType" />
</xs:schema>

The generic xsd types:

<?xml version="1.0" encoding="ISO-8859-1"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns="https://localhost/JAX-RS_BookStore_Service/xsd/"
    targetNamespace="https://localhost/JAX-RS_BookStore_Service/xsd/"
    elementFormDefault="qualified" xmlns:pref="https://localhost/JAX-RS_BookStore_Service/xsd/">

    <xs:simpleType name="unsignedInteger">
        <xs:restriction base="xs:integer">
            <xs:minInclusive value="1" />
        </xs:restriction>
    </xs:simpleType>

    <xs:simpleType name="addressStreet">
        <xs:restriction base="xs:string">
            <xs:pattern value="[a-zA-ZöäüÖÄÜß -]{2,32}" />
        </xs:restriction>
    </xs:simpleType>

    <xs:simpleType name="addressNumber">
        <xs:restriction base="xs:string">
            <xs:pattern value="[0-9]{1}[0-9a-z/\\ -]{0,7}" />
        </xs:restriction>
    </xs:simpleType>

    <xs:simpleType name="addressCity">
        <xs:restriction base="xs:string">
            <xs:pattern value="[a-zA-ZöäüÖÄÜß -]{2,32}" />
        </xs:restriction>
    </xs:simpleType>

    <xs:simpleType name="zipCodeMoreThan4Digits">
        <xs:restriction base="xs:short">
            <xs:minInclusive value="1000"></xs:minInclusive>
            <xs:maxInclusive value="9999"></xs:maxInclusive>
        </xs:restriction>
    </xs:simpleType>

    <xs:complexType name="addressType">
        <xs:sequence>
            <xs:element name="city" type="addressCity" />
            <xs:element name="number" type="addressNumber" />
            <xs:element name="street" type="addressStreet" />
            <xs:element name="zipCode" type="zipCodeMoreThan4Digits" />
        </xs:sequence>
        <xs:attribute name="id" type="unsignedInteger" use="required">
        </xs:attribute>
    </xs:complexType>
</xs:schema>

The abstract MessageBodyReader which is used by several implementations. In this class the XML input is validated against the schema file.

    public abstract class AbstractXmlValidationReader<T> implements
        MessageBodyReader<T> {

    private final Providers providers;
    private final Schema schema;

    /**
     * Instantiates the XML validation class (MessageBodyReader)
     * 
     * @param providers
     * @param servletContext
     * @param xsdFileName
     */
    public AbstractXmlValidationReader(final Providers providers,
            final ServletContext servletContext, final String xsdFileName) {
        this.providers = providers;

        try {
            SchemaFactory sf = SchemaFactory
                    .newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
            File xsd = new File(servletContext.getRealPath(xsdFileName));
            schema = sf.newSchema(xsd);
        } catch (Exception e) {
            throw new RuntimeException(
                    "Unable to create XSD validation schema", e);
        }
    }

    @Override
    public boolean isReadable(Class<?> type, Type genericType,
            Annotation[] annotations, MediaType mediaType) {

        @SuppressWarnings("unchecked")
        Class<T> readableClass = (Class<T>) ((ParameterizedType) getClass()
                .getGenericSuperclass()).getActualTypeArguments()[0];

        if (type == readableClass
                && type.isAnnotationPresent(XmlRootElement.class)) {
            return true;
        }
        return false;
    }

    /**
     * Source adapted from: <a href=
     * "http://stackoverflow.com/questions/3428273/validate-jaxbelement-in-jpa-jax-rs-web-service"
     * >stackoverflow.com</a>
     */
    @Override
    public T readFrom(Class<T> type, Type genericType,
            Annotation[] annotations, MediaType mediaType,
            MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
            throws IOException, WebApplicationException {

        try {
            JAXBContext jaxbContext = null;
            ContextResolver<JAXBContext> resolver = providers
                    .getContextResolver(JAXBContext.class, mediaType);
            if (null != resolver) {
                jaxbContext = resolver.getContext(type);
            }
            if (null == jaxbContext) {
                jaxbContext = JAXBContext.newInstance(type);
            }
            Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
            unmarshaller.setSchema(schema);

            @SuppressWarnings("unchecked")
            T entity = (T) unmarshaller.unmarshal(entityStream);
            return entity;
        } catch (JAXBException e) {
            throw new MessageBodyReaderValidationException(
                    "Failure while performing xml validation or xml marhalling!",
                    e);
        }
    }
}

The concreate MeassagBodyReader implementation, which provides the specific XSD file.

@Provider
@Consumes(MediaType.APPLICATION_XML)
public class AddressXmlValidationReader extends
        AbstractXmlValidationReader<Address> {

    private final static String xsdFileName = "/xsd/Address.xsd";

    public AddressXmlValidationReader(@Context Providers providers,
            @Context ServletContext servletContext) {
        super(providers, servletContext, xsdFileName);
    }
}

And finally the specific ExceptionMapper. The response-body in this example just contains the exception message. Probably this should be changed to sth. different, like HTML.

Don't forget to declare this class within the service Application class! classes.add(MessageBodyReaderValidationExceptionMapper.class);

@Provider
public class MessageBodyReaderValidationExceptionMapper implements
        ExceptionMapper<MessageBodyReaderValidationException> {

    @Override
    public Response toResponse(MessageBodyReaderValidationException e) {
        return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                .entity(e.getMessage()).type(MediaType.TEXT_PLAIN).build();
    }
}

The result of a POST request by using Firefox RESTClient plugin.

Note that the content of the <street> tag is empty, NULL value!

enter image description here

My-Name-Is
  • 4,814
  • 10
  • 44
  • 84