0

I am currently running Glassfish 4.1 on JDK 1.8.0-40. I am using javaee-web-api-7.0 and jersey-media-moxy-2.22. I am marshalling/unmarshalling JSON and XML from/to JAXB-annotated java objects.

I have a ContextResolver<Unmarshaller> set up to provide an Unmarshaller with a custom ValidationEventHandler which will collect exceptions from the property setters and throw a BadRequestException with the aggregate validation errors. This part is working.

However, I also have beforeMarshal and afterUnmarshal methods on the objects that check for unset properties (the rules for which properties must be set vary on the values of the properties, leading me to rule out validation against a schema). If an exception is thrown from the afterUnmarshal method, it is not seen by the ValidationEventHandler and instead bubbles up to the ExceptionMapper.

Is there any way to catch the exceptions from the beforeMarshal and afterUnmarshal methods on the individual objects and get them to the ValidationEventHandler?

I think it would be possible to implement a MessageBodyReader to catch the exceptions, use Unmarshaller.getEventHandler, manually call ValidationEventHandler.handleEvent, and throw a BadRequestException if handleEvent returns false [edit: if an exception is thrown from Unmarshaller.unmarshal, it wouldn't be possible to continue unmarshalling, so the only possible recourse is to terminate processing]. But this would be missing the event location information, and I don't particularly fancy implementing my own MessageBodyReader. I am hoping there is a easier built-in way to do this that I have not been able to discover.

Thanks in advance for any help.

David Kleszyk
  • 642
  • 3
  • 10

1 Answers1

2

After a bunch of digging and headaches, I ended up developing a solution.

Step 1 (optional)

EDIT: You don't have to patch Jersey to achieve this behavior. I cannot find it documented anywhere, but the org.glassfish.jersey.internal.inject.Custom annotation marks a provider as custom (whereas @Provider alone is insufficient). You also have to disable MOXy by setting CommonProperties.MOXY_JSON_FEATURE_DISABLE to true if your provider is for application/json. Thus you only have to do:

@Custom
@Provider
public class MyCustomMessageBodyReader...
[...]

This is my least favorite part of my solution, but also saved me from a bunch of code duplication. Jersey's sorting algorithm for selecting MessageBodyReader/Writers has no way to prioritize application providers (that I could find). I wanted to extend AbstractRootElementJaxbProvider to re-use its functionality, but that meant I couldn't make it more specific than the Jersey-provided XmlRootElementJaxbProvider. Since by default Jersey only sorts on media type distance, object type distance, and whether a provider is registered as a custom provider (providers detected via the @Provider annotation aren't registered as custom providers), the Jersey implementation would always be selected instead of my MessageBodyReader/Writer.

I checked out the Jersey 2.10.4 source from Github and patched MessageBodyFactory to utilize @Priority annotations as part of the selection algorithm for MessageBodyReader/Writers.

diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/MessageBodyFactory.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/MessageBodyFactory.java
index 3845b0c..110f18c 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/MessageBodyFactory.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/MessageBodyFactory.java
@@ -72,6 +72,7 @@ import javax.ws.rs.ext.MessageBodyWriter;
 import javax.ws.rs.ext.ReaderInterceptor;
 import javax.ws.rs.ext.WriterInterceptor;

+import javax.annotation.Priority;
 import javax.inject.Inject;
 import javax.inject.Singleton;
 import javax.xml.transform.Source;
@@ -107,6 +108,8 @@ import jersey.repackaged.com.google.common.primitives.Primitives;
  */
 public class MessageBodyFactory implements MessageBodyWorkers {

+    private static final int DEFAULT_WORKER_PRIORITY = 1000;
+
     private static final Logger LOGGER = Logger.getLogger(MessageBodyFactory.class.getName());

     /**
@@ -218,13 +221,15 @@ public class MessageBodyFactory implements MessageBodyWorkers {
         public final T provider;
         public final List<MediaType> types;
         public final Boolean custom;
+        public final int priority;
         public final Class<?> providerClassParam;

         protected WorkerModel(
-                final T provider, final List<MediaType> types, final Boolean custom, Class<T> providerType) {
+                final T provider, final List<MediaType> types, final Boolean custom, final int priority, Class<T> providerType) {
             this.provider = provider;
             this.types = types;
             this.custom = custom;
+            this.priority = priority;
             this.providerClassParam = getProviderClassParam(provider, providerType);
         }

@@ -239,8 +244,8 @@ public class MessageBodyFactory implements MessageBodyWorkers {

     private static class MbrModel extends WorkerModel<MessageBodyReader> {

-        public MbrModel(MessageBodyReader provider, List<MediaType> types, Boolean custom) {
-            super(provider, types, custom, MessageBodyReader.class);
+        public MbrModel(MessageBodyReader provider, List<MediaType> types, Boolean custom, int priority) {
+            super(provider, types, custom, priority, MessageBodyReader.class);
         }

         public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
@@ -263,8 +268,8 @@ public class MessageBodyFactory implements MessageBodyWorkers {

     private static class MbwModel extends WorkerModel<MessageBodyWriter> {

-        public MbwModel(MessageBodyWriter provider, List<MediaType> types, Boolean custom) {
-            super(provider, types, custom, MessageBodyWriter.class);
+        public MbwModel(MessageBodyWriter provider, List<MediaType> types, Boolean custom, int priority) {
+            super(provider, types, custom, priority, MessageBodyWriter.class);
         }

         public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
@@ -437,6 +442,10 @@ public class MessageBodyFactory implements MessageBodyWorkers {
             if (modelA.custom ^ modelB.custom) {
                 return (modelA.custom) ? -1 : 1;
             }
+
+            if(modelA.priority != modelB.priority) {
+                return modelA.priority - modelB.priority;
+            }
             return 0;
         }

@@ -578,17 +587,27 @@ public class MessageBodyFactory implements MessageBodyWorkers {
         }
     }

+    private static int getPriority(Priority annotation) {
+        if (annotation == null) {
+            return DEFAULT_WORKER_PRIORITY;
+        }
+
+        return annotation.value();
+    }
+
     private static void addReaders(List<MbrModel> models, Set<MessageBodyReader> readers, boolean custom) {
         for (MessageBodyReader provider : readers) {
+            int priority = getPriority(provider.getClass().getAnnotation(Priority.class));
             List<MediaType> values = MediaTypes.createFrom(provider.getClass().getAnnotation(Consumes.class));
-            models.add(new MbrModel(provider, values, custom));
+            models.add(new MbrModel(provider, values, custom, priority));
         }
     }

     private static void addWriters(List<MbwModel> models, Set<MessageBodyWriter> writers, boolean custom) {
         for (MessageBodyWriter provider : writers) {
+            int priority = getPriority(provider.getClass().getAnnotation(Priority.class));
             List<MediaType> values = MediaTypes.createFrom(provider.getClass().getAnnotation(Produces.class));
-            models.add(new MbwModel(provider, values, custom));
+            models.add(new MbwModel(provider, values, custom, priority));
         }
     }

After building Jersey, I replaced the jersey-common jar in the Glassfish modules directory with my patched version. This let me annotate my MessageBodyReader/Writers with @Priority(500) and have them be selected by Jersey.

I felt that this was the cleanest way to let me prioritize my MessageBodyReader/Writers without affecting anything else in Glassfish that relies on Jersey.

Step 2

Inspired by this post I decided that using an Unmarshaller.Listener would be cleaner than my original path of implementing afterUnmarshal on each of my JAXB classes. I made an interface (CanBeValidated) and extended Unmarshaller.Listener as follows.

public final class ValidatingUnmarshallerListener
        extends Unmarshaller.Listener
{
    private final ValidationEventHandler validationEventHandler;

    public ValidatingUnmarshallerListener(
            ValidationEventHandler validationEventHandler)
    {
        this.validationEventHandler = validationEventHandler;
    }

    @Override
    public void afterUnmarshal(Object target, Object parent)
    {
        if (target == null
                || !(target instanceof CanBeValidated))
        {
            return;
        }

        CanBeValidated v = (CanBeValidated) target;
        Collection<Throwable> validationErrors = v.validate();

        for (Throwable t : validationErrors)
        {
            ValidationEvent event = new ValidationEventImpl(
                    ValidationEvent.ERROR,
                    t.getLocalizedMessage(),
                    null,
                    t);

            this.validationEventHandler.handleEvent(event);
        }
    }
}

Step 3

Finally, I extended org.glassfish.jersey.message.internal.AbstractRootElementJaxbProvider to override the readFrom method.

@Override
protected Object readFrom(
        Class<Object> type,
        MediaType mediaType,
        Unmarshaller u,
        InputStream entityStream)
        throws JAXBException
{
    final SAXSource source = getSAXSource(spf.provide(), entityStream);
    ValidationEventCollector eventCollector = new ValidationEventCollector();
    ValidatingUnmarshallerListener listener = new ValidatingUnmarshallerListener(eventCollector);
    u.setEventHandler(eventCollector);
    u.setListener(listener);

    final Object result;
    if (type.isAnnotationPresent(XmlRootElement.class))
    {
        result = u.unmarshal(source);
    }
    else
    {
        result = u.unmarshal(source, type).getValue();
    }

    if (eventCollector.hasEvents())
    {
        HttpError error = new HttpError(Response.Status.BAD_REQUEST);

        for (ValidationEvent event : eventCollector.getEvents())
        {
            error.addMessage(ValidationUtil.toString(event));
        }

        throw new WebApplicationException(error.toResponse());
    }

    return result;
}
David Kleszyk
  • 642
  • 3
  • 10
  • Would you please show the complete code of your subclass of `org.glassfish.jersey.message.internal.AbstractRootElementJaxbProvider`? I can't get mine to work; I tried to inject `Providers` with `@Context` in my constructor, which I have to get hold of to pass to the supertype constructor, but I keep getting this exception: `org.jboss.weld.exceptions.CreationException: WELD-001530: Cannot produce an instance of class [...].` – Hein Blöd Jun 06 '16 at 15:05
  • I ran into the same issue. I worked around it by lazily-loading the class. I wrote a wrapper `public abstract class LazyJaxbProvider implements MessageBodyReader, MessageBodyWriter` and injected the providers into the wrapper `@Context private Providers ps`. Then I wrote a method `getMessageBodyReaderWriter` that returns a new instance of my subclass of `AbstractRootElementJaxbProvider`, providing `this.ps` as a constructor argument. – David Kleszyk Jun 08 '16 at 16:58
  • Depending on what you're doing, you might also want to look at the [bean validation API](https://jersey.java.net/documentation/latest/bean-validation.html). I haven't tried using it at all, so I'm not even sure if the Jersey version packaged with Glassfish 4.1 supports bean validation. I felt that it made more sense to have objects perform their own validation rather than having to write a bunch of boilerplate custom annotations that are going to be tightly coupled to the target class anyway. – David Kleszyk Jun 08 '16 at 17:07
  • Thanks for your replies, also to my question! I'm currently away on a trip and can't try it out right away, but will do so when I return. I've been using the bean validation API before, but my use case for your technique is a little different: I have various `XmlJavaTypeAdapter`s that perform conversions that may throw exceptions like `NumberFormatException`. The problem is that by default in Jersey 2.x, such exceptions are swallowed and the corresponding fields are simply set to `null`. I want to be able to use an exception mapper and return HTTP 400 instead. – Hein Blöd Jun 09 '16 at 19:32
  • I think I completely understood your solution now. I presume you're doing completely customized validation in `validate()` in each of your classes that implement `CanBeValidated`? That must be why you suggested the bean validation API as an alternative approach. Since my validation error occurs in an `XmlJavaTypeAdapater` and is already indicated by an exception, I "just" need to prevent Jersey/GlassFish from swallowing that exception silently (as is explained [here](http://stackoverflow.com/questions/11114428/jaxb-xmladapter-method-does-not-throws-exception/11114563#11114563)) ... – Hein Blöd Jun 13 '16 at 14:52
  • ... and I need to wrap that exception in a HTTP 400. – Hein Blöd Jun 13 '16 at 14:53
  • I'm glad I was able to help a bit. I've done some more digging (since upgrading to Payara broke the Jersey patch I originally wrote) and found a better way to register custom providers that doesn't require patching. Personally, I dislike using annotations for validation. It makes the validation behavior completely dependent on the application environment instead of being internal to the class where (IMO) it belongs. – David Kleszyk Jun 16 '16 at 15:26
  • That solution with `@Custom` looks promising, even though it's a pity that the annotation resides in a package named "internal". Thanks for sharing that! I have one question: you say that for JSON one has to disable the MOXy JSON feature. Does that mean that for XML, such a `@Custom` provider automatically gets selected instead (respectively ahead) of the `XmlRootElementJaxbProvider` subclasses (`App`, `General`, `Text`) included in GlassFish 4.1/Jersey 2.10.4? – Hein Blöd Jun 17 '16 at 07:02
  • 1
    It should. At least on a vanilla install of Glassfish, the only "custom" message body providers are `ConfigurableMoxyJsonProvider` and `JsonStructureBodyReader` (how these classes are registered as custom is still beyond me, since they aren't explicitly annotated with `@Custom`). But for any other media type the `@Custom` annotation should be sufficient (as long as there aren't any other custom message body providers coming in from elsewhere). – David Kleszyk Jun 17 '16 at 15:34