How can I use Spring REST with MappingJackson2XmlHttpMessageConverter and XSD validation - java

I'm creating a Spring 4 REST application, using MappingJackson2XmlHttpMessageConverter to convert incoming XML requests to domain objects. Is there any way to apply XSD validation in that process? If not, I think my fallback is to just make the #RequestBody a String, parse and validate it, and then convert it to the domain object. Is there a better approach?

One approach to this may be to write a custom HttpMessageConverter<T> that checks XSD validation (look here for a way to validate XML with XSD) before returning the object.
Suppose that you have the following method in your Controller class:
#RequestMapping(value = "/")
public CustomObject getCustomObject(#RequestParam(value = "id") String id){
return new CustomObject();
}
Then your converter may look like this:
public class CustomObjectConverter implements HttpMessageConverter<CustomObject> {
// a real message converter that will respond to ancillary methods and do the actual work
protected HttpMessageConverter<Object> delegateConverter;
public CustomObjectConverter (HttpMessageConverter<Object> delegate) {
super(delegate, personService);
super.delegateConverter = delegate;
this.employeePhotoBaseUrl = employeePhotoBaseUrl;
}
#Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return delegateConverter.canRead(clazz, mediaType) && CustomObject.class.equals(clazz);
}
#Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return delegateConverter.canWrite(clazz, mediaType) && CustomObject.class.equals(clazz);
}
#Override
public List<MediaType> getSupportedMediaTypes() {
return delegateConverter.getSupportedMediaTypes();
}
#Override
public CustomObject read(Class<? extends CustomObject> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return (CustomObject) delegateConverter.read(clazz, inputMessage);
}
#Override
public void write(CustomObject t, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
if(validationOK)
delegateConverter.write(t, contentType, outputMessage);
else
// You may implement a custom exception handler to return a proper HTTP error code
throw new YourCustomException();
}
}
Remember to configure your new converter. I do this in my configuration class:
#Configuration
#EnableWebMvc
public class RestConfig extends WebMvcConfigurerAdapter {
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// initialize your MappingJackson2XmlHttpMessageConverter
MappingJackson2XmlHttpMessageConverter xmlMessageConverter = xmlMessageConverter();
//Here we add our custom-configured HttpMessageConverters
converters.add(new CustomObjectConverter(xmlMessageConverter));
super.configureMessageConverters(converters);
}
}

Related

Parse RequestBody as two different objects in Spring boot

In my Spring Boot application (2.5.5) I get a large JSON body in the POST request to a specific endpoint. On that request I need to get both the parsed object and that whole object as a string to do some validation. The JSON object contains a lot of information that I don't need so that is not included in the Object so I can't convert it to a string.
#RestController
#RequestMapping("/example")
public class ExampleController {
#PostMapping("")
public void example(
#RequestBody String stringBody,
#RequestBody ExampleRequest exampleRequest
) {
// Validate request with 'stringBody'
// Do things with 'exampleRequest'
}
}
The best idea I had so far is to just use #RequestBody String stringBody and then convert that string to a JSON object but that is really not the ideal solution.
I know that you can't have two #RequestBody but I really need to somehow have both.
I believe that a custom HandlerMethodArgumentResolver is your best option.
For that I suggest you create a custom annotation as follows:
#Target({ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
public #interface ValidJsonSignature { }
Now you need to implement the custom HandlerMethodArgumentResolver:
public class JsonSignatureValidationArgumentResolver implements HandlerMethodArgumentResolver {
private final ObjectMapper objectMapper;
public JsonSignatureValidationArgumentResolver(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
#Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterAnnotation(ValidJsonSignature.class) != null;
}
#Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
HttpServletRequest httpServletRequest = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
String jsonPayload = StreamUtils.copyToString(httpServletRequest.getInputStream(), StandardCharsets.UTF_8);
// Do actual validation here
if (// Valid) {
// If valid, then convert String to method parameter type and return it
return objectMapper.treeToValue(objectMapper.readTree(jsonPayload), methodParameter.getParameterType());
} else {
// Throw exception if validation failed
}
}
}
Next, you need to register JsonSignatureValidationArgumentResolver as an argument resolver:
#Configuration
public class JsonSignatureValidationConfiguraion implements WebMvcConfigurer {
#Autowired
private ObjectMapper objectMapper;
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new JsonSignatureValidationArgumentResolver(objectMapper));
}
}
Last but not the least, you need to annotate the Controller attribute with #ValidJsonSignature as follows:
#RestController
#RequestMapping("/example")
public class ExampleController {
#PostMapping("")
public void example(#ValidJsonSignature ExampleRequest exampleRequest) {
}
}

MessageBodyProviderNotFoundException is thrown in JerseyTest when using GSON

I use Jersey and decided to go with GSON instead of Moxy for JSON handling (didn't like the fact that Moxy requires setters).
Everything works fine until now, except one very annoying issue in my JerseyTest subclasses: custom GsonProvider is not being recognized unless explicitly registered for each call. It is, however, being recognized if I deploy the application to Tomcat.
My ResourceConfig:
#ApplicationPath("")
public class MyResourceConfig extends ResourceConfig {
public MyResourceConfig() {
register(GsonProvider.class);
register(SomeResource.class);
}
}
Implementation of GsonProvider (though I don't think it is related to the issue I experience):
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
public class GsonProvider<T> implements MessageBodyReader<T>, MessageBodyWriter<T> {
private final Gson mGson;
public GsonProvider() {
mGson = new GsonBuilder().create();
}
#Override
public boolean isReadable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return true;
}
#Override
public T readFrom(Class<T> type, Type genericType, Annotation[] annotations,
MediaType mediaType, MultivaluedMap<String, String> httpHeaders,
InputStream entityStream) throws IOException, WebApplicationException {
InputStreamReader reader = new InputStreamReader(entityStream, "UTF-8");
try {
return mGson.fromJson(reader, type);
} finally {
reader.close();
}
}
#Override
public boolean isWriteable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return true;
}
#Override
public long getSize(T t, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return -1;
}
#Override
public void writeTo(T t, Class<?> type, Type genericType, Annotation[] annotations,
MediaType mediaType, MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream) throws IOException, WebApplicationException {
PrintWriter printWriter = new PrintWriter(entityStream);
try {
String json = mGson.toJson(t);
printWriter.write(json);
printWriter.flush();
} finally {
printWriter.close();
}
}
}
This test results in MessageBodyProviderNotFoundException:
public class SomeResourceTest extends JerseyTest {
#Override
public Application configure() {
return new MyResourceConfig();
}
#Test
public void someApi_200Returned() throws Exception {
// Arrange
// Act
SomeResponse response =
target("/somepath")
.request()
.post(Entity.json(""), SomeResponse.class);
// Assert
assertThat(response.getStatus(), is(200));
}
}
In order to resolve this issue I register GsonProvider for request. The following change makes the test pass:
public class SomeResourceTest extends JerseyTest {
#Override
public Application configure() {
return new MyResourceConfig();
}
#Test
public void someApi_200Returned() throws Exception {
// Arrange
// Act
SomeResponse response =
target("/somepath")
.register(GsonProvider.class)
.request()
.post(Entity.json(""), SomeResponse.class);
// Assert
assertThat(response.getStatus(), is(200));
}
}
So, registration of GsonProvider in MyResourceConfig is good for deployment, but JerseyTest requires additional registration per request.
While I can live with that, it is annoying, time consuming and will be hard to communicate to other team members. Any solution for this issue?
You haven't shown the stacktrace, but I'm pretty sure that if you look closely at it, it will show that it is actually a client side error. What you need to do is register the gson provider with the client also since you are trying to deserialize the response JSON to a POJO
#Override
public void configureClient(ClientConfig config) {
config.register(GsonProvider.class)
}
The configureClient method is a method in the JerseyTest that you can override.

Intercept null ResponseBody before marshalling response

I've got multiple controllers for RESTful endpoints which currently return null if there's no resource at the endpoint. For instance,
#RequestMapping(method = ReqeustMethod.GET, value = "{id}")
#ResponseBody
public MyResource get(#PathVariable final Long id) {
return this.myService.get(id); // returns null if bad id
}
I want to return a specific, different resource to the client (ErrorResource) when there's no MyResource with the given id. I know I can do that with a separate method with #ExceptionHandler, such as:
#RequestMapping(method = RequestMethod.GET, value = "{id}")
#ResponseBody
public MyResource get(#PathVariable final Long id) {
final MyResource myResource = this.myService.get(id);
if (myResource == null) {
throw new NotFoundException();
}
return myResource;
}
#ExceptionHandler(NotFoundException.class)
#ResponseStatus(value = HttpStatus.NOT_FOUND)
#ResponseBody
public ErrorResource notFoundException(
final HttpServletRequest request,
final NotFoundException exception) {
final ErrorResource errorResource = new ErrorResource();
errorResource.setStatus(HttpStatus.NOT_FOUND.value());
errorResource.setDeveloperMessage("No resource found at " + request.getRequestURL());
return errorResource;
}
And that's nice. But what I'd really like to be able to do is have some kind of interceptor that figures out for me that whenever an API method is returning a null #ResponseBody, it should instead run the logic in my notFoundException() method. That will make all my controller methods a little cleaner. Is there any way to do that?
It sounds like a job for Spring's HttpMessageConverter.
You can write your own converter by implementing HttpMessageConverter<T> interface.
In your case I would implement a converter of HttpMessageConverter<MyResource> with a null check on the MyResource instance in the write method. If the MyResource instance is null, then build and write your ErrorResource instance.
Here is an example:
import java.io.IOException;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
public class MyResourceConverter implements HttpMessageConverter<MyResource> {
// a real message converter that will respond to ancillary methods and do the actual work
private HttpMessageConverter<Object> delegateConverter;
public MyResourceConverter(HttpMessageConverter<Object> delegateConverter){
this.delegateConverter = delegateConverter;
}
#Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return delegateConverter.canRead(clazz, mediaType) && MyResource.class.equals(clazz);
}
#Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return delegateConverter.canWrite(clazz, mediaType) && MyResource.class.equals(clazz);
}
#Override
public MyResource read(Class<? extends MyResource> clazz,
HttpInputMessage inputMessage) throws IOException,
HttpMessageNotReadableException {
return (MyResource) delegateConverter.read(clazz, inputMessage);
}
#Override
public void write(MyResource t, MediaType contentType,
HttpOutputMessage outputMessage) throws IOException,
HttpMessageNotWritableException {
Object result = null;
if(t == null){
result = // build your ErrorResource here
}else{
result = t;
}
delegateConverter.write(result, contentType, outputMessage);
}
}
Note that this converter needs to be registered in your Spring configuration.
The configuration class must extend WebMvcConfigurerAdapter and override the configureMessageConverters method, like:
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// Here we add our custom-configured HttpMessageConverters.
// yourDelegateConverter may be a MappingJackson2HttpMessageConverter instance for example
converters.add(new EmployeeConverter(yourDelegateConverter));
super.configureMessageConverters(converters);
}
References (from official Spring documentation):
HTTP Message conversion
HttpMessageConverter
WebMvcConfigurerAdapter

Moxy ignore invalid fields in json

When I send this request:
{"invalidField": "value", "date": "value"}
to my rest service:
#PUT
#Consumes("application/json")
public void putJson(Test content) {
System.out.println(content.toString());
}
I expected to get an exception because:
There is no invalidField in my domain model.
Date format is not valid.
But really I get test object with null values. My dmain model is:
public class Test {
private String name;
private Date date;
//getters and setters here
}
I think this is not a valid behavior. How can I fix that?
Thanks for help.
Solution:
As Blaise Doughan said, it is required to extend MOXy's MOXyJsonProvider and override the preReadFrom method to set custom javax.xml.bind.ValidationEventHandler. But the problem is that Jersey's ConfigurableMoxyJsonProvider will always be picked first, unless you write a MessageBodyWriter/MessageBodyReader that is parameterized with a more specific type than Object. To solve this problem it is necessary to disable MOXy and then enable CustomMoxyJsonProvider.
Example:
Create your own feature that extends javax.ws.rs.core.Feature:
#Provider
public class CustomMoxyFeature implements Feature {
#Override
public boolean configure(final FeatureContext context) {
final String disableMoxy = CommonProperties.MOXY_JSON_FEATURE_DISABLE + '.' + context.getConfiguration().getRuntimeType().name().toLowerCase();
context.property(disableMoxy, true);
return true;
}
}
Create your own provider that extends MOXyJsonProvider:
#Provider
public class CustomMoxyJsonProvider extends MOXyJsonProvider {
#Override
protected void preReadFrom(Class<Object> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, String> httpHeaders, Unmarshaller unmarshaller) throws JAXBException {
unmarshaller.setEventHandler(new ValidationEventHandler() {
#Override
public boolean handleEvent(ValidationEvent event) {
return false;
}
});
}
}
Add this resources in Application config:
package com.vinichenkosa.moxyproblem;
import java.util.Set;
import javax.ws.rs.core.Application;
#javax.ws.rs.ApplicationPath("webresources")
public class ApplicationConfig extends Application {
#Override
public Set<Class<?>> getClasses() {
Set<Class<?>> resources = new java.util.HashSet<>();
addRestResourceClasses(resources);
return resources;
}
private void addRestResourceClasses(Set<Class<?>> resources) {
resources.add(com.vinichenkosa.moxyproblem.TestResource.class);
resources.add(com.vinichenkosa.moxyproblem.custom_provider.CustomMoxyFeature.class);
resources.add(com.vinichenkosa.moxyproblem.custom_provider.CustomMoxyJsonProvider.class);
}
}
MOXy will report information about invalid property values, but by default it does not fail on them. The reporting is done to an instance of javax.xml.bind.ValidationEventHandler. You can override the ValidationEventHandler set on the Unmarshaller to do this.
You can create your own MesageBodyReader/MessageBodyWriter that extends MOXy's MOXyJsonProvider and override the preReadFrom method to do this.
#Override
protected void preReadFrom(Class<Object> type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, String> httpHeaders,
Unmarshaller unmarshaller) throws JAXBException {
unmarshaller.setEventHandler(yourValidationEventHandler);
}

jersey/jackson - filter properties based on query parameter

Filtering out properties with Jackson is pretty simple:
final FilterProvider filters = new SimpleFilterProvider().
addFilter(... the name of the filter ...,
SimpleBeanPropertyFilter.filterOutAllExcept(... enumeration of properties ...));
<object mapper>.writer(filters).writeValueAsString(... the bean ...);
I am trying to integrate this in my Jersey REST application. The API user has the possibility to filter the properties by providing a query string:
https://the-api/persons?fields=name,age,location,gender
What is the most elegant way to do this in Jersey? I could easily execute the above in my resource methods, but this somehow kills the elegance of Jersey. Also, I believe that creating a new ObjectMapper for every request will have performance penalties.
I could write a MessageBodyWriter which fetches the fields query parameter from the UriInfo context and serializes the entity to json while applying a filter based on the fields query parameter. Is this the best way to do this? Like this:
#Provider
#Produces(MediaType.APPLICATION_JSON)
public class JustTesting implements MessageBodyWriter<Object> {
#Context
UriInfo uriInfo;
#Context
JacksonJsonProvider jsonProvider;
public boolean isWriteable(Class<?> aClass, Type type, Annotation[] annotations, MediaType mediaType) {
return MediaType.APPLICATION_JSON_TYPE.equals(mediaType);
}
public long getSize(Object object, Class<?> aClass, Type type, Annotation[] annotations, MediaType mediaType) {
return -1;
}
public void writeTo(Object object, Class<?> aClass, Type type, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> stringObjectMultivaluedMap, OutputStream outputStream) throws IOException, WebApplicationException {
final FilterProvider filters = new SimpleFilterProvider().addFilter("MyFilter", SimpleBeanPropertyFilter.filterOutAllExcept(uriInfo.getQueryParameters().getFirst("fields")));
jsonProvider.locateMapper(aClass, mediaType).writer(filters).writeValue(outputStream, object);
}
}
It seems to work, but I am unsure if it is smart to do it like this. I am new to the Jersey library.
My current solution to a similar problem is to register the following servlet filter:
#Singleton
public class ViewFilter implements Filter {
private #Inject Provider<ViewBeanPropertyFilter> filter;
#Override
public void init(FilterConfig filterConfig) throws ServletException { }
#Override
public void destroy() { }
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
ObjectWriterInjector.set(new ObjectWriterModifier() {
#Override
public ObjectWriter modify(EndpointConfigBase<?> endpoint, MultivaluedMap<String, Object> responseHeaders, Object valueToWrite, ObjectWriter w, JsonGenerator g) throws IOException {
return w.with(new FilterProvider() {
#Override
public BeanPropertyFilter findFilter(Object filterId) {
if(filterId.equals(ViewFilterJacksonModule.FILTER_NAME)) {
return filter.get();
}
return null;
}
});
}
});
chain.doFilter(request, response);
}
public static class ViewBeanPropertyFilter extends SimpleBeanPropertyFilter {
private #Inject ViewManager manager;
#Override
protected boolean include(BeanPropertyWriter writer) {
Class<?> cls = writer.getMember().getDeclaringClass();
return manager.isFieldInView(cls, writer.getMember().getName());
}
#Override
protected boolean include(PropertyWriter writer) {
return true;
}
}
}
It's a little more gnarly than the solution you provided, but leaves the standard JacksonJsonProvider serialization in place.
It can be improved by pulling the (possibly) existing FilterProvider via m.getConfig().getFilterProvider() and delegating to it before / after your filter.

Categories