Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to treat exception thrown by setter on JAXB automatic unmarshal from request body?

I send the following JSON request body to my controller:

{"Game": {"url": "asd"}}

where Game is my model class, annotated with @XmlRootElement (and some JPA annotations which are not important in this context).

The controller:

@PUT
@Path("/{name}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response createRow(
    @PathParam("name") String name, 
    Game gameData) throws Exception{

    Game.createRow(gameData); // + exception handling etc.
}

Now, I understood that when Game gameData parameter of the controller method is created, my setters from the model class are called. The setter that requires attention is:

public void setUrl(String url) throws Exception{
  String regex = "^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]";
  Pattern pattern = Pattern.compile(regex);

  System.out.println("URL: " + url);
  if ( url == null || url.length() == 0) {
    throw new Exception("The url of the game is mandatory!");
  } else {
    Matcher matcher = pattern.matcher(url);
    if (!matcher.matches()) { 
      throw new Exception("The url is invalid! Please check its syntax!");
    } else {
      this.url = url;
    }
  }
}

What happens is that, during the deserialization of the JSON string to the Game object, the The url is invalid! exception is thrown, but only in the console of TomEE. What I want to do is to send this error to the client.

If I use an exception which extends WebApplicationException instead of the generic Exception, then I get an exception in the client, but not the one about the validity of the url. Instead, after the deserialization, gameData.url is NULL, and when I try to create a Game instance with the data from gameData, the setter will be called like gameToBeCreated.set(NULL), and I will get the exception Url is mandatory, when actually an URL was sent from the client, but with bad syntax. It was not NULL when sent from client.

So, can I somehow intercept the exceptions thrown when the automatic unmarshalling happens and forward them to the client?

like image 496
Sorin Adrian Carbunaru Avatar asked Dec 03 '13 17:12

Sorin Adrian Carbunaru


People also ask

What is Unmarshal exception?

This exception indicates that an error has occurred while performing an unmarshal operation that prevents the JAXB Provider from completing the operation. The ValidationEventHandler can cause this exception to be thrown during the unmarshal operations.

How do you Unmarshal string Jaxb?

To unmarshal an xml string into a JAXB object, you will need to create an Unmarshaller from the JAXBContext, then call the unmarshal() method with a source/reader and the expected root object.

What is ObjectFactory in JAXB?

jaxb package. An ObjectFactory allows you to programatically construct new instances of the Java representation for XML content. The Java representation of XML content can consist of schema derived interfaces and classes representing the binding of schema type definitions, element declarations and model groups.

What is JAXB Unmarshal?

The JAXB Unmarshaller interface is responsible for governing the process of deserializing the XML data to Java Objects. The unmarshalling to objects can be done to variety of input sources.


2 Answers

Interception Exceptions with JAXB

A ValidationEventHandler is used to intercept the exceptions that occur during unmarshalling. If you want to collect all the errors that occur you can use a ValidationEventCollector.

Java Model

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class Foo {

    private String bar;

    public String getBar() {
        return bar;
    }

    public void setBar(String bar) {
        throw new RuntimeException("Always throw an error");
    }

}

Demo

import java.io.StringReader;
import javax.xml.bind.*;
import javax.xml.bind.util.ValidationEventCollector;

public class Demo {

    private static String XML = "<foo><bar>Hello World</bar></foo>";

    public static void main(String[] args) throws Exception {
        JAXBContext jc = JAXBContext.newInstance(Foo.class);

        Unmarshaller unmarshaller = jc.createUnmarshaller();
        unmarshaller.unmarshal(new StringReader(XML));

        ValidationEventCollector vec = new ValidationEventCollector();
        unmarshaller.setEventHandler(vec);
        unmarshaller.unmarshal(new StringReader(XML));
        System.out.println(vec.getEvents().length);
    }

}

Output

1

Integrating with JAX-RS

MessageBodyReader

You could create an instance of MessageBodyReader where you can leverage a ValidationEventHandler during the unmarshal process. Below is a link giving an example of how to implement a MessageBodyReader.

  • Validate JAXBElement in JPA/JAX-RS Web Service

ContextResolver<Unmarshaller>

Slightly higher level, you could implement ContextResolver<Unmarshaller> and have the Unmarshaller returned have the appropriate ValidationEventHandler on it. It would look something like the following:

import javax.ws.rs.core.*;
import javax.ws.rs.ext.*;
import javax.xml.bind.*;
import javax.xml.bind.helpers.DefaultValidationEventHandler;

@Provider
public class UnmarshallerResolver implements ContextResolver<Unmarshaller> {

    @Context Providers providers;

    @Override
    public Unmarshaller getContext(Class<?> type) {
        try {
            ContextResolver<JAXBContext> resolver = providers.getContextResolver(JAXBContext.class, MediaType.APPLICATION_XML_TYPE);
            JAXBContext jaxbContext;
            if(null == resolver || null == (jaxbContext = resolver.getContext(type))) {
                jaxbContext = JAXBContext.newInstance(type);
            }
            Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
            unmarshaller.setEventHandler(new DefaultValidationEventHandler());
            return unmarshaller;
        } catch(JAXBException e) {
            throw new RuntimeException(e);
        }
    }

}
like image 67
bdoughan Avatar answered Oct 16 '22 21:10

bdoughan


It seems that there isn't a built-in solution.

My solution (must be adopted to your code and can be improved (submit response-code), of course):

Create an ErrorList class

This class will serve to store the errors, which occure in the setters of the game model class

    import javax.ws.rs.WebApplicationException;
    import javax.ws.rs.core.Response;

    public class ErrorList {

    private String errorString;
    private String errorObject; 

    public ErrorList() {
    }

    public ErrorList(String errorString, String errorObject) {
        this.setErrorString(errorString);
        this.setErrorObject(errorObject);
    }

    public String getErrorString() {
        return errorString;
    }

    public void setErrorString(String errorString) {
        this.errorString = errorString;
    }

    public String getErrorObject() {
        return errorObject;
    }

    public void setErrorObject(String errorObject) {
        this.errorObject = errorObject;
    }

    public Response sendError() {
        throw new WebApplicationException(
                Response.status(400)
                .header("Access-Control-Allow-Origin", "*")
                .entity(this.getErrorString() + " caused by " + this.getErrorObject())
                .build());  
    }
}

Add a new list property to the game model class

    @XmlTransient
    private ArrayList<ErrorList> errorList = new ArrayList<ErrorList>();

Now you can check for errors in your setters and add errors to the list

    public void setUrl(String url) {        
      String regex = "\\b(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]";
      Pattern pattern = Pattern.compile(regex);

      if (url == null || url.length() == 0) {
        // Create a new element for the errorlist with your explanations
        ErrorList error = new ErrorList("ERROR_PROPERTY","Game.url"); 
        // Add it to the errorList of the instance
        this.errorList.add(error);      
        return;
      }

      Matcher matcher = pattern.matcher(url);
      if (!matcher.matches()) {
        // Create a new element for the errorlist with your explanations
        ErrorList error = new ErrorList("ERROR_PROPERTY","Game.url"); 
        // Add it to the errorList of the instance
        this.errorList.add(error);  
        return;
      }

      this.url = url;
    }

Add a method to the game model class, that checks if the errorList is populated

public static void checkErrorList(Game gameData) {
    if (gameData.errorList != null && !gameData.errorList.isEmpty()) {
        gameData.errorList.get(0).sendError();
    }       
}

Finally

Now you can add

    checkErrorList(yourGameInstance);

to your methods at the beginning, for example

    public static Response createRow(Game gameData) {
      checkErrorList(gameData);
      ...
    }

and whenever you setSomething, you should call it again, before you try to send the data to a db or so.

    public static Response createRow(Game gameData, String newUrl) {
      checkErrorList(gameData);
      ...
      gameData.setUrl(newUrl);
      checkErrorList(gameData);
    }

It's not as fancy as it would be with a build-in function, but at least it get's the job done.

And yes, I'm new to (JAVA/)JAXB.

The approach could be simplified by leaving out the ErrorList class and use a one-dimensional ArrayList inside of the game class.

like image 37
Felix Lehmann Avatar answered Oct 16 '22 20:10

Felix Lehmann