How to write a custom convertor for recieving data annotated with #RequestBody - spring-boot

I'm trying to write a custom convertor for receiving data POSTed to a REST application. The object I want to populate already has its own builder that accepts a string JSON so I have to use that instead of the Jackson deserializer Spring would normally use.
I've tried a number of different things but I keep getting the following exception:
org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class xxx.yyy.zzz.MyType]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `xxx.yyy.zzz.MyType` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
at [Source: (PushbackInputStream); line: 1, column: 1]
at at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:281) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:250) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
My convertor looks like:
public class MyConverter extends AbstractHttpMessageConverter<MyType> {
public MyConverter() {
super(/*MediaType.TEXT_PLAIN*/ MediaType.APPLICATION_JSON);
}
#Override
protected boolean supports(Class<?> type) {
return MyType.class.isAssignableFrom(type);
}
#Override
protected MyType readInternal(Class<? extends MyType> type, HttpInputMessage inputMessage) throws IOException {
String str = ..... read data from inputMessage
return MyType.build(str);
}
#Override
protected void writeInternal(MyType s, HttpOutputMessage outputMessage) {
}
}
and the controller is:
#RequestMapping(method = RequestMethod.POST)
public void add(#RequestBody MyType data) {
System.out.println("add:" + data.toString());
}
Even if change the MediaType in the constructor for MyConverter to 'MediaType.ALL' it will still fail. Curiously if I change it to TEXT_PLAIN and POST with Content-Type set to 'text/plain' it works!

Answering my own question. The problem turned out to be the order the HttpMessageConverter objects are processed. The inbuilt converters were being processed, and failing, before my converter had chance.
This isn't the best code, but it works:
#Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void extendMessageConverters(#Nonnull List<HttpMessageConverter<?>> converters) {
List<HttpMessageConverter<?>> temp = new ArrayList<>();
temp.add(new MyConverter());
temp.addAll(converters);
converters.clear();
converters.addAll(temp);
}
}
I can't quite believe this is the best answer for what, I think, should be a fairly standard problem. If somebody can suggest a better answer I'll happily accept that instead

Related

How to link a Vaadin Grid with the result of Spring Mono WebClient data

This seems to be a missing part in the documentation of Vaadin...
I call an API to get data in my UI like this:
#Override
public URI getUri(String url, PageRequest page) {
return UriComponentsBuilder.fromUriString(url)
.queryParam("page", page.getPageNumber())
.queryParam("size", page.getPageSize())
.queryParam("sort", (page.getSort().isSorted() ? page.getSort() : ""))
.build()
.toUri();
}
#Override
public Mono<Page<SomeDto>> getDataByPage(PageRequest pageRequest) {
return webClient.get()
.uri(getUri(URL_API + "/page", pageRequest))
.retrieve()
.bodyToMono(new ParameterizedTypeReference<>() {
});
}
In the Vaadin documentation (https://vaadin.com/docs/v10/flow/binding-data/tutorial-flow-data-provider), I found an example with DataProvider.fromCallbacks but this expects streams and that doesn't feel like the correct approach as I need to block on the requests to get the streams...
DataProvider<SomeDto, Void> lazyProvider = DataProvider.fromCallbacks(
q -> service.getData(PageRequest.of(q.getOffset(), q.getLimit())).block().stream(),
q -> service.getDataCount().block().intValue()
);
When trying this implementation, I get the following error:
org.springframework.core.codec.CodecException: Type definition error: [simple type, class org.springframework.data.domain.Page]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.springframework.data.domain.Page` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
at [Source: (io.netty.buffer.ByteBufInputStream); line: 1, column: 1]
grid.setItems(lazyProvider);
I don't have experience with vaadin, so i'll talk about the deserialization problem.
Jackson needs a Creator when deserializing. That's either:
the default no-arg constructor
another constructor annotated with #JsonCreator
static factory method annotated with #JsonCreator
If we take a look at spring's implementations of Page - PageImpl and GeoPage, they have neither of those. So you have two options:
Write your custom deserializer and register it with the ObjectMapper instance
The deserializer:
public class PageDeserializer<T> extends StdDeserializer<Page<T>> {
public PageDeserializer() {
super(Page.class);
}
#Override
public Page<T> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
//TODO implement for your case
return null;
}
}
And registration:
SimpleModule module = new SimpleModule();
module.addDeserializer(Page.class, new PageDeserializer<>());
objectMapper.registerModule(module);
Make your own classes extending PageImpl, PageRequest, etc. and annotate their constructors with #JsonCreator and arguments with #JsonProperty.
Your page:
public class MyPage<T> extends PageImpl<T> {
#JsonCreator
public MyPage(#JsonProperty("content_prop_from_json") List<T> content, #JsonProperty("pageable_obj_from_json") MyPageable pageable, #JsonProperty("total_from_json") long total) {
super(content, pageable, total);
}
}
Your pageable:
public class MyPageable extends PageRequest {
#JsonCreator
public MyPageable(#JsonProperty("page_from_json") int page, #JsonProperty("size_from_json") int size, #JsonProperty("sort_object_from_json") Sort sort) {
super(page, size, sort);
}
}
Depending on your needs for Sort object, you might need to create MySort as well, or you can remove it from constructor and supply unsorted sort, for example, to the super constructor. If you are deserializing from input manually you need to provide type parameters like this:
JavaType javaType = TypeFactory.defaultInstance().constructParametricType(MyPage.class, MyModel.class);
Page<MyModel> deserialized = objectMapper.readValue(pageString, javaType);
If the input is from request body, for example, just declaring the generic type in the variable is enough for object mapper to pick it up.
#PostMapping("/deserialize")
public ResponseEntity<String> deserialize(#RequestBody MyPage<MyModel> page) {
return ResponseEntity.ok("OK");
}
Personally i would go for the second option, even though you have to create more classes, it spares the tediousness of extracting properties and creating instances manually when writing deserializers.
There are two parts to this question.
The first one is about asynchronously loading data for a DataProvider in Vaadin. This isn't supported since Vaadin has prioritized the typical case with fetching data straight through JDBC. This means that you end up blocking a thread while the data is loading. Vaadin 23 will add support for doing that blocking on a separate thread instead of keeping the UI thread blocked, but it will still be blocking.
The other half of your problem doesn't seem to be directly related to Vaadin. The exception message says that the Jackson instance used by the REST client isn't configured to support creating instances of org.springframework.data.domain.Page. I don't have direct experience with this part of the problem, so I cannot give any advice on exactly how to fix it.

FormUrlEncoded POST request, I need to convert snake case values into camelCase with SpringBoot and Jackson

I am integrating with a third-party's vendor API.
I have a SpringBoot and Jackson setup
They are sending me a POST request that is of type formUrlEncoded and with the params in snake_case
(over 10 params in total and no body)
e.g.
POST www.example.com?player_id=somePlayerId&product_id=someProductId&total_amount=totalAmount...
There are many out of the box helpers for JSON but I cannot find any for formUrlEncoded (I hope I am missing something obvious).
I have tried #ModelAttribute and #RequestParam but had no luck.
I am trying to avoid the #RequestParam MultiValueMap<String, String> params + custom mapper option
#RequestParam is the simplest way which allows you to define the exact name of the query parameter something like:
#PostMapping
public String foo(#RequestParam("player_id") String playerId){
}
If you want to bind all the query parameters to an object , you have to use #ModelAttribute. It is based on the DataBinder and is nothing to do with Jackson. By default it only supports binding the query parameter to an object which fields have the same name as the query parameter. So you can consider to bind the query paramater to the following object :
public class Request {
private String player_id;
private String product_id;
private Long total_amount;
}
If you really want to bind to the object that follow traditional java naming convention (i.e lower camel case) from the query parameter that has snake case values , you have to cusomtize WebDataBinder.
The idea is to override its addBindValues() and check if the query parameter name is in snake case format , convert it the lower camel case format and also add it as the bind values for the request. Something like :
public class MyServletRequestDataBinder extends ExtendedServletRequestDataBinder {
private static Converter<String, String> snakeCaseToLowerCamelConverter = CaseFormat.LOWER_UNDERSCORE
.converterTo(CaseFormat.LOWER_CAMEL);
public MyServletRequestDataBinder(Object target) {
super(target);
}
public MyServletRequestDataBinder(Object target, String objectName) {
super(target, objectName);
}
#Override
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
super.addBindValues(mpvs, request);
Enumeration<String> paramNames = request.getParameterNames();
while (paramNames != null && paramNames.hasMoreElements()) {
String paramName = paramNames.nextElement();
if(paramName.contains("_")) {
String[] values = request.getParameterValues(paramName);
if (values == null || values.length == 0) {
// Do nothing, no values found at all.
} else if (values.length > 1) {
mpvs.addPropertyValue(snakeCaseToLowerCamelConverter.convert(paramName), values);
} else {
mpvs.addPropertyValue(snakeCaseToLowerCamelConverter.convert(paramName), values[0]);
}
}
}
}
}
P.S I am using Guava for helping me to convert snake case to lowerCamelCase.
But in order to use the customized WebDataBinder , you have to in turn customize WebDataBinderFactory and RequestMappingHandlerAdapter because :
customize WebDataBinderFactory in order to create the customised WebDataBinder
customize RequestMappingHandlerAdapter in order to create the WebDataBinderFactory
Something like:
public class MyServletRequestDataBinderFactory extends ServletRequestDataBinderFactory {
public MyServletRequestDataBinderFactory(List<InvocableHandlerMethod> binderMethods,
WebBindingInitializer initializer) {
super(binderMethods, initializer);
}
#Override
protected ServletRequestDataBinder createBinderInstance(Object target, String objectName,
NativeWebRequest request) throws Exception {
return new MyServletRequestDataBinder(target, objectName);
}
}
and
public class MyRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {
#Override
protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> binderMethods)
throws Exception {
return new MyServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer());
}
}
And finally register to use the customised RequestMappingHandlerAdapter in your configuration :
#Configuration
public class Config extends DelegatingWebMvcConfiguration {
#Override
protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() {
return new MyRequestMappingHandlerAdapter();
}
}
I don't think you are missing anything. Looking at the RequestParamMethodArgumentResolver#resolveName source I do no see a way to customize how a request parameter is matched. So it looks either you have to implement your own resolver or just annotate each parameter with #RequestParam and provide the name, e.g. #RequestParam("product_id") String productId
EDIT:
As for ModelAttribute, ModelAttributeMethodProcessor uses WebDataBinder. Again you can customize it with your custom DataBinder but I didn't found any that out of the box supports aliases as Jackson does.

Spring MVC RestController allow params with different names in methods

I am writing an API using Spring MVC and I am coming up with a problem allowing apps written in different languages to consume my API.
It turns out that the "Ruby users" like to have their params named in snake_case and our "Java users" like to have their param names in camel_case.
Is it possible to create my methods that allow param names to be named multiple ways, but mapped to the same method variable?
For instance... If I have a method that accepts a number of variables, of them there is mapped to a postal code. Could I write my method with a #RequestParam that accepts BOTH "postal_code" and "postalCode" and maps it to the same variable?
Neither JAX-RS #QueryParam nor Spring #RequestParam support your requirement i.e., mapping multiple request parameter names to the same variable.
I recommend not to do this as it will be very hard to support because of the confusion like which parameter is coming from which client.
But if you really wanted to handle this ((because you can't change the URL coming from 3rd parties, agreed long back), then the alternative is to make use of HandlerMethodArgumentResolver which helps in passing our own request argument (like #MyRequestParam) to the controller method like as shown in the below code:
Controller class:
#Controller
public class MyController {
#RequestMapping(value="/xyz")
public void train1(#MyRequestParam String postcode) {//custom method argument injected
//Add your code here
}
}
MyRequestParam :
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.PARAMETER)
public #interface MyRequestParam {
}
HandlerMethodArgumentResolver Impl class:
public class MyRequestParamWebArgumentResolver implements HandlerMethodArgumentResolver {
#Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {
MyRequestParam myRequestParam =
parameter.getParameterAnnotation(MyRequestParam.class);
if(myRequestParam != null) {
HttpServletRequest request =
(HttpServletRequest) webRequest.getNativeRequest();
String myParamValueToBeSentToController = "";
//set the value from request.getParameter("postal_code")
//or request.getParameter("postalCode")
return myParamValueToBeSentToController;
}
return null;
}
#Override
public boolean supportsParameter(MethodParameter parameter) {
return (parameter.getParameterAnnotation(MyRequestParam.class) != null);
}
}
WebMvcConfigurerAdapter class:
#Configuration
class WebMvcContext extends WebMvcConfigurerAdapter {
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new MyRequestParamWebArgumentResolver());
}
}
I think what you want to do is not allowed by Spring framework with the annotation RequestParam.
But if you can change the code or say to your third party to modify the calls i would suggest you 2 options
Option 1:
Use the #PathVariable property
#RequestMapping(value = "/postalcode/{postalCode}", method = RequestMethod.GET)
public ModelAndView yourMethod(#PathVariable("postalCode") String postalCode) {
//...your code
Here does not matter if the are calling your URL as:
http://domain/app/postalcode/E1-2ES
http://domain/app/postalcode/23580
Option 2:
Create 2 methods in your controller and use the same service
#RequestMapping(value = "/postalcode", method = RequestMethod.GET, params={"postalCode"})
public ModelAndView yourMethod(#RequestParam("postalCode") String postalCode) {
//...call the service
#RequestMapping(value = "/postalcode", method = RequestMethod.GET, params={"postal_code"})
public ModelAndView yourMethodClient2(#RequestParam("postal_code") String postalCode) {
//...call the service
If is possible, I would suggest you option 1 is much more scalable

Using MVC type conversion for path variable and returning 404 on null parameter

My controller. Note the custom #Exists annotation:
#RestController
public class ClientApiController {
#RequestMapping(path = "/{client}/someaction", method = RequestMethod.GET)
String handleRequest(#Exists Client client) {
// ...
}
}
The Exists annotation:
/**
* Indicates that a controller request mapping method parametet should not be
* null. This is meant to be used on model types to indicate a required entity.
*/
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface Exists {}
The converter which converts the String from the path variable into a Client instance:
#Component
public class StringToClient implements Converter<String, Client> {
#Autowired
private ClientDAO clientDAO;
#Override
public Client convert(String source) {
return clientDAO.getClientById(source);
}
}
The ResourceNotFoundException exception used to trigger a 404
#ResponseStatus(value = HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
}
My controller method receives the converted Client as desired. If the client id used in the URL matches a client, everything works fine. If the id doesn't match, the client parameter is null empty (uses default constructor) in the handle() controller method.
What I can't get to work now is declarative checking that the Client is not null (i.e. that the id refers to an existing client). If it's null, a ResourceNotFoundException should be thrown. Checking whether the argument is null in the method body and throwing my custom ResourceNotFoundException is easy to do, but repetitive (like this one does). Also, this declarative approach should work for all model classes implementing the interface ModelWithId so it can be used for multiple model types.
I've searched the Spring documentation and I haven't found how to achieve this. I need to insert some processing somewhere after type conversion and before the controller's handleRequest method.
I'm using Spring Boot 1.3.3
After type conversion and before the controller's method there is a validation. You can implement custom validator and raise exception in it. Add new validator to DataBinder, and mark method's parameter as #Validated:
#RestController
public class ClientApiController {
#InitBinder
public void initBinder(DataBinder binder){
binder.addValidators(new Validator() {
#Override
public boolean supports(Class<?> aClass) {
return aClass==Client.class;
}
#Override
public void validate(Object o, Errors errors) {
Client client = (Client)o;
if(client.getId()==null) throw new ResourceNotFoundException();
}
});
}
#RequestMapping(path = "/{client}/someaction", method = RequestMethod.GET)
String handleRequest(#Validated #Exists Client client) {
// ...
}
#RequestMapping(path = "/{client}/anotheraction", method = RequestMethod.GET)
String handleAnotherRequest(#Validated #Exists Client client) {
// ...
}
}
Of course, you can declare validator as separate class, and use it repeatedly in other controllers. Actually, you can raise exception right in your converter, but there is possibility, that you'll need the conversion without exception in other places of your application.

#Produces/provider media type matching

I am experimenting with api verioning and have a very peculiar requirement to work against. We are going to use content-negotiation i.e #Produces annotation for this and I want to a custom media type in a format like #Produces({"th/v1-v10+xml"}), where v1-v10 tells that this api will serve any request with Accept header of "th/v1+xml", "th/v2+xml" all the way to "th/v10+xml".
I know this is a bit strange, but the idea is that each drop we make in production will be a new version for the client, but not every service will be modified. So I want to annotate the service with a range so that I don’t have to duplicate it for every drop even if it’s not changed.
So what i want to find out is there any way I can intercept the login in Jersey while it matched the #Path and #Produces annotations? I know I can’t use regex to match media types.
.......
A bit more research tells me that the Jersey calls the MediaType.isCompatible(MediaType other) method to determine the compatibility between the requests accept header and the services provider media type.
Is may be able to leverage this a bit if I can create a custom MediaType and override the isCompatible method. Does Jersey allows such extension??
Any help is much appreciated.
You probabily should have to use a custom response mapper.
1.- Create a class implementing MessageBodyWriter in charge of writing the response
#Provider
public class MyResponseTypeMapper
implements MessageBodyWriter<MyResponseObjectType> {
#Override
public boolean isWriteable(final Class<?> type,final Type genericType,
final Annotation[] annotations,
final MediaType mediaType) {
... use one of the arguments (either the type, an annotation or the MediaType)
to guess if the object shoud be written with this class
}
#Override
public long getSize(final MyResponseObjectType myObjectTypeInstance,
final Class<?> type,final Type genericType,
final Annotation[] annotations,
final MediaType mediaType) {
// return the exact response length if you know it... -1 otherwise
return -1;
}
#Override
public void writeTo(final MyResponseObjectType myObjectTypeInstance,
final Class<?> type,final Type genericType,
final Annotation[] annotations,
final MediaType mediaType,
final MultivaluedMap<String,Object> httpHeaders,
final OutputStream entityStream) throws IOException, WebApplicationException {
... serialize / marshall the MyResponseObjectType instance using
whatever you like (jaxb, etC)
entityStream.write(serializedObj.getBytes());
}
}
2.- Register the Mappers in your app
public class MyRESTApp
extends Application {
#Override
public Set<Class<?>> getClasses() {
Set<Class<?>> s = new HashSet<Class<?>>();
s.add(MyResponseTypeMapper.class);
return s;
}
}
Jersey will scan all registered Mappers calling their isWriteable() method until one returns true... if so, this MessageBodyWriter instance will be used to serialize the content to the client

Resources