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?
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.
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.
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.
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.
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
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
.
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);
}
}
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With