Get Jersey to work with Optional parameters - java

I'm trying to get Jersey to work with Optional parameters. I have a very simple web service:
#Path("helloworld")
public static class HelloWorldResource {
public static final String CLICHED_MESSAGE = "Hello World!";
#GET
#Produces("text/plain")
public String getHello(#QueryParam("maybe") Optional<String> maybe) {
return CLICHED_MESSAGE;
}
}
And a simple harness:
public static void main(String[] arg) throws IOException {
ResourceConfig config = new ResourceConfig(HelloWorldResource.class);
String baseUri = "http://localhost:8080/api/";
HttpServer server = GrizzlyHttpServerFactory
.createHttpServer(URI.create(baseUri), config, false);
server.start();
}
However I get the following error:
Exception in thread "main" org.glassfish.jersey.server.model.ModelValidationException: Validation of the application resource model has failed during application initialization.
[[FATAL] No injection source found for a parameter of type public java.lang.String com.mercuria.odyssey.server.GrizllyOptional$HelloWorldResource.getHello(java.util.Optional) at index 0.; source='ResourceMethod{httpMethod=GET, consumedTypes=[], producedTypes=[text/plain], suspended=false, suspendTimeout=0, suspendTimeoutUnit=MILLISECONDS, invocable=Invocable{handler=ClassBasedMethodHandler{handlerClass=class com.mercuria.odyssey.server.GrizllyOptional$HelloWorldResource, handlerConstructors=[org.glassfish.jersey.server.model.HandlerConstructor#a3d9978]}, definitionMethod=public java.lang.String com.mercuria.odyssey.server.GrizllyOptional$HelloWorldResource.getHello(java.util.Optional), parameters=[Parameter [type=class java.util.Optional, source=maybe, defaultValue=null]], responseType=class java.lang.String}, nameBindings=[]}']
at org.glassfish.jersey.server.ApplicationHandler.initialize(ApplicationHandler.java:555)
at org.glassfish.jersey.server.ApplicationHandler.access$500(ApplicationHandler.java:184)
at org.glassfish.jersey.server.ApplicationHandler$3.call(ApplicationHandler.java:350)
at org.glassfish.jersey.server.ApplicationHandler$3.call(ApplicationHandler.java:347)
at org.glassfish.jersey.internal.Errors.process(Errors.java:315)
at org.glassfish.jersey.internal.Errors.process(Errors.java:297)
at org.glassfish.jersey.internal.Errors.processWithException(Errors.java:255)
at org.glassfish.jersey.server.ApplicationHandler.<init>(ApplicationHandler.java:347)
at org.glassfish.jersey.server.ApplicationHandler.<init>(ApplicationHandler.java:311)
at org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpContainer.<init>(GrizzlyHttpContainer.java:337)
at org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory.createHttpServer(GrizzlyHttpServerFactory.java:140)
at com.mercuria.odyssey.server.GrizllyOptional.main(GrizllyOptional.java:33)
I presume I need to do something about so that Jersey knows how to handle Optional parameters, but I've no idea what!

So parameter types that are allowed as a #xxxParam, you need to meet one of these requirements:
Be a primitive type
Have a constructor that accepts a single String argument
Have a static method named valueOf() or fromString() that accepts a single String argument (see, for example, Integer.valueOf(String))
Have a registered implementation of ParamConverterProvider JAX-RS extension SPI that returns a ParamConverter instance capable of a "from string" conversion for the type.
Be List<T>, Set<T> or SortedSet<T>, where T satisfies 2, 3 or 4 above. The resulting collection is read-only.
So in this case of Optional, going down the list; it's not a primitive; it doesn't have a String constructor; it doesn't have a static valueOf() or fromString()
So basically, the only option left is to implement a ParamConverter/ParamConverterProvider pair for it. Dropwizard (a framework built on top of Jersey) has a good implementation for it. I will post it here in case the link ever goes dead
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.jersey.internal.inject.Providers;
import org.glassfish.jersey.internal.util.ReflectionHelper;
import org.glassfish.jersey.internal.util.collection.ClassTypePair;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.ws.rs.ext.ParamConverter;
import javax.ws.rs.ext.ParamConverterProvider;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Optional;
import java.util.Set;
#Singleton
public class OptionalParamConverterProvider implements ParamConverterProvider {
private final ServiceLocator locator;
#Inject
public OptionalParamConverterProvider(final ServiceLocator locator) {
this.locator = locator;
}
/**
* {#inheritDoc}
*/
#Override
public <T> ParamConverter<T> getConverter(final Class<T> rawType, final Type genericType, final Annotation[] annotations) {
if (Optional.class.equals(rawType)) {
final List<ClassTypePair> ctps = ReflectionHelper.getTypeArgumentAndClass(genericType);
final ClassTypePair ctp = (ctps.size() == 1) ? ctps.get(0) : null;
if (ctp == null || ctp.rawClass() == String.class) {
return new ParamConverter<T>() {
#Override
public T fromString(final String value) {
return rawType.cast(Optional.ofNullable(value));
}
#Override
public String toString(final T value) {
return value.toString();
}
};
}
final Set<ParamConverterProvider> converterProviders = Providers.getProviders(locator, ParamConverterProvider.class);
for (ParamConverterProvider provider : converterProviders) {
final ParamConverter<?> converter = provider.getConverter(ctp.rawClass(), ctp.type(), annotations);
if (converter != null) {
return new ParamConverter<T>() {
#Override
public T fromString(final String value) {
return rawType.cast(Optional.ofNullable(value).map(s -> converter.fromString(value)));
}
#Override
public String toString(final T value) {
return value.toString();
}
};
}
}
}
return null;
}
}
Note, if you are using a Jersey version 2.26+, instead of injecting ServiceLocator you will use InjectionManager instead. Also the argument that accepts a locator, you will need to change the the manager.
With this class, you just need to register it with your Jersey application.

This is a partial solution, but it seems like DropWizard has a feature specifically to support this:
https://github.com/dropwizard/dropwizard/blob/master/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalParamBinder.java
So you can simply use their code:
import io.dropwizard.jersey.optional.*;
class DirtyBinder extends AbstractBinder {
#Override
protected void configure() {
bind(OptionalParamConverterProvider.class).to(ParamConverterProvider.class).in(Singleton.class);
bind(OptionalDoubleParamConverterProvider.class).to(ParamConverterProvider.class).in(Singleton.class);
bind(OptionalIntParamConverterProvider.class).to(ParamConverterProvider.class).in(Singleton.class);
bind(OptionalLongParamConverterProvider.class).to(ParamConverterProvider.class).in(Singleton.class);
}
}
then just add:
config.register(new DirtyBinder());

Related

In Quarkus/RESTEasy, how do I show helpful error messages for malformed query parameters?

I have a REST endpoint that looks like this:
#Path("/api/v1/users")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
public class UsersResource {
#GET
public Uni<ListUsersResponse> list(#BeanParam #Valid ListUsersRequest req) {
...
}
}
...
public class ListUsersRequest {
#NotNull
#NotEmpty
#QueryParam("status_in")
List<UserStatus> statusIn;
}
...
public enum UserStatus {
All,
Creating,
Deleting,
...
}
When I make an API call like this:
http://localhost:8080/api/v1/users?status_in=invalid_status
Quarkus/RESTEasy will return a 404, and nothing will even show up in the server logs, because it is a 404, because it is a "client error".
If I enable debug logging, the error looks like this:
java.lang.IllegalArgumentException: No enum constant com.myproject.UserStatus.something
I dislike this behaviour, because:
A 404 implies "not found", which makes users think that the path they used doesn't exist at all, which is confusing.
It's difficult for both users and developers to figure out what went wrong, as there are no error messages in the response or in the server logs without DEBUG enabled.
Apparently, this is on purpose, because it conforms to some spec:
3.2 Fields and Bean Properties
if the field or property is annotated with #MatrixParam, #QueryParam or #PathParam then an implementation MUST generate an instance of NotFoundException (404 status) that wraps the thrown exception and no entity
So my question is, how can I modify this to actually give helpful error messages and status code?
I have created a variety of exception handlers for just this reason. For example, I want to check if an incoming JSON body is malformed. For that I did:
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import org.jboss.resteasy.spi.ReaderException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
/**
* Maps a ReaderException to a response. A ReaderException is most often thrown
* when we get a bad JSON packet.
*
*/
#Provider
public class ReaderExceptionMapper implements ExceptionMapper<ReaderException> {
#Override
public Response toResponse(ReaderException exception) {
if( exception.getCause() instanceof JsonParseException ||
exception.getCause() instanceof JsonMappingException ) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("error parsing json body - " + exception.getMessage())
.type(MediaType.TEXT_PLAIN_TYPE)
.build();
}
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(exception.getMessage())
.type(MediaType.TEXT_PLAIN_TYPE)
.build();
}
}
In your case you should be able to create an ExceptionMapper for IllegalArgumentException and do what you'd like. I am running this in both Wildfly and Quarkus.
You could use a ParamConverter for generic enum types.
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ParamConverter;
import javax.ws.rs.ext.ParamConverterProvider;
import javax.ws.rs.ext.Provider;
#Provider
public class EnumParamConverterProvider implements ParamConverterProvider {
#Override
public <T> ParamConverter<T> getConverter(final Class<T> rawType, final Type genericType, final Annotation[] annotations) {
if (!rawType.isEnum()) {
return null;
}
final Enum<?>[] constants = (Enum<?>[]) rawType.getEnumConstants();
return new ParamConverter<T>() {
#Override
#SuppressWarnings("unchecked")
public T fromString(final String value) {
if (value == null || value.isEmpty()) {
return null;
}
for (Enum<?> e : constants) {
if (e.name().equalsIgnoreCase(value)) {
return (T) e;
}
}
// No match, check toString()
for (Enum<?> e : constants) {
if (e.toString().equalsIgnoreCase(value)) {
return (T) e;
}
}
throw new WebApplicationException(Response.serverError().entity(String.format("Failed to find constant '%s'", value)).build());
}
#Override
public String toString(final T value) {
return value != null ? value.toString() : null;
}
};
}
}
There is a similar question for which I gave an answer. Basically I suggested use of ResponseEntity class as a return value or creating Exception handler marked as #ControllerAdvice. Please look at the answer here: How to add message to response with exception

How to use instance PathParam to create an object

Suppose I have a JAX-RS web service like this:
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
#Path("/somePath/{id}")
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.APPLICATION_JSON)
public class SomeObject {
#PathParam("id")
private String id;
#GET
#Path("/something")
public String something() {
DbObject dbObject = new DbObject(id);
// return something
}
#POST
#Path("/somethingElse")
public void somethingElse(Arg1 arg1, Arg2 arg2) {
DbObject dbObject = new DbObject(id);
// do something else with it
}
...
}
The very first line in almost all my methods is creating my dbObject.
Is there a way to do that immediately after id is set?
Can I do that in the id setter? Will the setId method be called instead of populating the value of the id variable?
Or what other option do I have?
Quoting the #PathParam documentation:
The type of the annotated parameter, field or property must either:
Be PathSegment, the value will be the final segment of the matching part of the path. See UriInfo for a means of retrieving all request path segments.
Be List<javax.ws.rs.core.PathSegment>, the value will be a list of PathSegment corresponding to the path segment(s) that matched the named template parameter. See UriInfo for a means of retrieving all request path segments.
Be a primitive type.
Have a constructor that accepts a single String argument.
Have a static method named valueOf or fromString that accepts a single String argument (see, for example, Integer.valueOf(String)).
Have a registered implementation of ParamConverterProvider JAX-RS extension SPI that returns a ParamConverter instance capable of a "from string" conversion for the type.
If you meet one of the above criteria, you will be able to use:
#PathParam("id")
private DbObject dbObject;
Let's focus in the three last approaches. First, using a constructor with a single String argument:
public class DbObject {
private String id;
public DbObject(String id) {
this.id = id;
}
...
}
Alternatively you can use a valueOf(String) method:
public class DbObject {
private String id;
public DbObject(String id) {
this.id = id;
}
public static DbObject valueOf(String id) {
return new DbObject(id);
}
...
}
Or define a ParamConverterProvider:
#Provider
public class DbObjectParamConverterProvider implements ParamConverterProvider {
#Override
public <T> ParamConverter<T> getConverter(Class<T> rawType, Type genericType,
Annotation[] annotations) {
if (rawType.getName().equals(DbObject.class.getName())) {
return new ParamConverter<T>() {
#Override
public T fromString(String value) {
return rawType.cast(new DbObject(value));
}
#Override
public String toString(T value) {
return ((DbObject) value).getId();
}
};
}
return null;
}
}

How to forbid slash '/' usage in entity attribute using jaxb and jaxrs

I have the following entity:
#XmlAccessorType(XmlAccessType.FIELD)
#XmlRootElement(name = EntityConstants.PARTNER)
public class FilePartner
{
#XmlAttribute(name = EntityConstants.IDENTIFIER, required = true)
#XmlJavaTypeAdapter(RestResourceIdJaxbAdapter.class)
private String identifier;
...
}
Here is the jaxb adapter:
public class RestResourceIdJaxbAdapter extends XmlAdapter<String, String>
{
#Override
public String unmarshal(String v) throws Exception
{
if (v != null && v.contains("/"))
{
// throw new ApiException(Status.BAD_REQUEST, RestErrorMessages.BAD_REQUEST_SUFFIX, "Identifier must not contain slashes");
return v.replaceAll("/", "");
}
return v;
}
#Override
public String marshal(String v) throws Exception
{
return v;
}
}
I have a jaxrs service that accepts POST requests with body FilePartner:
#POST
#Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
#Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public Response createPartner(FilePartner partner,
#Context UriInfo uriInfo,
#HeaderParam(HttpHeaders.ACCEPT) String acceptHeader)
throws ApiException
{
...
}
What I want to achieve is to forbid the usage of slashes '/' in the identifier attribute of the FilePartner entity.
Today I am doing this using some jaxb adapter which simply strips all slashes from the id when unmarshalling.
Instead, what I would like is to return an appropriate BAD_REQUEST exception to the user.
I tried throwing exception in the unmarshal method of the jaxb adapter but seems that jaxrs is swallowing it and simply setting my identifier to null.
If we want to override this behavior I think I must create a new #Provider and register a special ValidationEventHandler in the unmarshaller that the javax.ws.rs.ext.MessageBodyReader creates.
Unfortunately, this is impossible unless I define an explicit dependency to a JAX-RS implementation which I want to avoid.
Are there any other options to restrict the usage of slashes in the identifier attribute, without defining an explicit dependency to jersey/resteasy and without handling the restriction in the #POST method of the service?
To your rescue comes ReaderInterceptor
Don't do any special handling using #XmlJavaTypeAdapter. Register a ReaderInterceptor with your Application class (if in jersey2) or in web.xml if earlier.
import java.io.IOException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
import javax.ws.rs.ext.ReaderInterceptor;
import javax.ws.rs.ext.ReaderInterceptorContext;
javax.ws.rs.ext.ReaderInterceptor
#Provider
public class ValidationInterceptor implements ReaderInterceptor {
public ValidationInterceptor() {
super();
}
#Override
public Object aroundReadFrom(ReaderInterceptorContext readerInterceptorContext) throws IOException,
WebApplicationException {
Object o = readerInterceptorContext.proceed();
if (o instanceof FilePartner&& ((FilePartner) o).getIndentifier().contains("/")) {
throw new WebApplicationException(Response.status(400)
.entity("Identifier must not contain a slash")
.build());
}
return o;
}
}
And register the interceptor to your Application in override of public Set<Class<?>> getClasses() method something like classes.add(ValidationInterceptor.class);
Hope that helps.

Catch exception from Object.afterUnmarshal in Glassfish 4.1

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.
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;
}

How to pass more parameters to spring validate method

I have extension of org.springframework.validation.Validator.
public class MyValidator implements Validator {
#Override
public void validate(Object target, Errors errors) {
...
}
}
My goal is to pass more than one target to method.
I don't like idea with overload validate method because it smells as bad code:
validate(Object target1, Object target1, Errors errors) or creating map with needed targets.
It will be good to know better approach regarding this case.
I did not try the following code, but it demonstrates a basic idea how one field of the bean could be verified against the other. Hopefully, it will help you
Let's say you have the following form bean
public class MyForm {
private String id;
private List<String> oldIds;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public List<String> getOldIds() {
return oldIds;
}
public void setOldIds(List<String> oldIds) {
this.oldIds = oldIds;
}
}
and the id property has to be validated against the oldIds object (if i did understand your requirements correctly). To achieve it your need to create a constraint and mark your bean. So, the first is the constraint interface
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.validation.Constraint;
import javax.validation.Payload;
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = MyConstraintValidator.class)
#Documented
public #interface MyConstraint {
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String[] value();
}
next, you need to implement the constraint validator class:
import java.util.List;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.apache.commons.beanutils.PropertyUtils;
public class MyConstraintValidator implements
ConstraintValidator<MyConstraint, Object> {
private String firstAttribute;
private String secondAttribute;
#Override
public void initialize(final MyConstraint constraintAnnotation) {
firstAttribute = constraintAnnotation.value()[0];
secondAttribute = constraintAnnotation.value()[1];
}
#Override
public boolean isValid(final Object object,
final ConstraintValidatorContext constraintContext) {
try {
final String id = (String) PropertyUtils.getProperty(object,
firstAttribute);
List<String> oldIds = (List<String>) PropertyUtils.getProperty(
object, secondAttribute);
// do your validation
return true;
} catch (final Exception e) {
throw new IllegalArgumentException(e);
}
}
}
finally, apply the created constraint to the form bean
#MyConstraint(value = { "id", "oldIds" })
public class MyForm {
// the code
}
For now, your mark your bean with the #Valid annotation from the javax.validation package or feed it to the validator object
We use a target bean which holds all the data which need to be validated. Something like
private static final class ParamsBean {
String id;
List<String> oldIds;
}
Then we simply cast the object. It's the cleanest possible solution imo, as it does not use generic Map or List of unknown objects (though the casting still is not nice).
i faced with a similar situation where i need to pass more arguments to the validate method so i came up with a idea of my own.in my case i wanted a String to be passed to this method
validate method implemented in the following classes CustomValidatorBean, LocalValidatorFactoryBean, OptionalValidatorFactoryBean, SpringValidatorAdapter
I extended the CustomValidatorBean and called the validate method in super class and it is working perfectly
import javax.validation.Validator;`
import org.apache.commons.lang3.StringUtils;`
import org.springframework.validation.Errors;`
importorg.springframework.validation.beanvalidation.CustomValidatorBean;`
public class MyValidator extends CustomValidatorBean {`
public void myvalidate(Object target,Errors errors,String flag,Profile profile)
{
super.validate(target,errors);
if(StringUtils.isEmpty(profile.name())){
errors.rejectValue("name", "NotBlank.profilereg.name", new Object[] { "name" }, "Missing Required Fields");
}
}
}

Categories

Resources