Spring Boot REST custom request parameters mapping for complex abstract entity - java

I'm working on REST API for the application using SpringBoot 2 and I want to get a specific object as my request parameter. Assuming I have my endpoint declared like this:
#RestController("TestEndpoint")
#RequestMapping(path = "/", produces = MediaType.APPLICATION_JSON_VALUE)
public class TestEndpoint {
#RequestMapping(method = RequestMethod.GET, value = "/test")
String getTest(MyWeirdRequest myRequest) {
return myRequest.toString();
}
}
where MyWeirdRequest looks like this:
public class MyWeirdRequest {
private Map<String, String> startWithOne;
private Map<String, String> anythingElse;
// setters and getters here
}
and my GET request would look like this example:
http://localost:8088/test?first=aaa&second=1bbb&third=1ccc&fourth=2ddd
List of parameters is not defined anywhere and so they can contain any keys.
I want my endpoint to get MyWeirdRequest object where all the params with value starting with 1 to be in startWithOne map and the rest of params to be in anythingElse where key is request parameter name. So in case of request above I want the result where my endpoint receives a MyWeirdRequest containing
startWithOne=[second:1bbb, third:1ccc]
anythingElse=[first:aaa, fourth:2ddd]
I know I could use a Map as a getTest param and then do all the mapping inside this method, but MyWeirdRequest will be used as a param for multiple endpoints and I want to avoid working with Maps directly everywhere.
I tried to create a custom PropertyEditor and register it in WebDataBinder, but it is only used if there is a #Requestparam annotation, but if I add it to getTest method - a parameter named myRequest becomes mandatory.
How do I handle the request like that?

So I figured out that in case you need to parse request parameters as I needed - you should implement HandlerMethodArgumentResolver. It provides the ability to access a lot of different request data. In my case it could look like this (initially I've forgot that request parameters may contain arrays of values, so MyWeirdRequest field types were changed to Map<String, String[]>):
public class TestArgumentResolver implements HandlerMethodArgumentResolver {
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(MyWeirdRequest.class);
}
#Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory)
throws Exception {
MyWeirdRequest result = new MyWeirdRequest();
Map<String, String[]> startsWithOne = new HashMap<>();
Map<String, String[]> anythingElse = new HashMap<>();
for (Map.Entry<String, String[]> paramEntry : webRequest.getParameterMap().entrySet()) {
String[] swoValues = Arrays.stream(paramEntry.getValue()).filter(v -> v.startsWith("1"))
.collect(Collectors.toList()).toArray(new String[0]);
if (swoValues.length > 0) {
startsWithOne.put(paramEntry.getKey(), swoValues);
}
String[] aeValues = Arrays.stream(paramEntry.getValue()).filter(v -> !v.startsWith("1"))
.collect(Collectors.toList()).toArray(new String[0]);
if (aeValues.length > 0) {
anythingElse.put(paramEntry.getKey(), aeValues);
}
}
result.setStartWithOne(startsWithOne);
result.setAnythingElse(anythingElse);
return result;
}
}
After that, I created a configuration to register my request resolver:
#Configuration
public class TestRequestConfiguration implements WebMvcConfigurer {
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new TestArgumentResolver());
}
}
And that's pretty much it!

Related

How can I instantiate a specific sub-type for a #RequestBody parameter based on the requested URI for a Spring MVC controller method?

Given the following basic domain model:
abstract class BaseData { ... }
class DataA extends BaseData { ... }
class DataB extends BaseData { ... }
I want to write a Spring MVC controller endpoint thus ...
#PostMapping(path="/{typeOfData}", ...)
ResponseEntity<Void> postData(#RequestBody BaseData baseData) { ... }
The required concrete type of baseData can be inferred from the typeOfData in the path.
This allows me to have a single method that can handle multiple URLs with different body payloads. I would have a concrete type for each payload but I don't want to have to create multiple controller methods that all do the same thing (albeit each would do very little).
The challenge that I am facing is how to "inform" the deserialization process so that the correct concrete type is instantiated.
I can think of two ways to do this.
First use a custom HttpMessageConverter ...
#Bean
HttpMessageConverter httpMessageConverter() {
return new MappingJackson2HttpMessageConverter() {
#Override
public Object read(final Type type, final Class<?> contextClass, final HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
// TODO How can I set this dynamically ?
final Type subType = DataA.class;
return super.read(subType, contextClass, inputMessage);
}
};
}
... which gives me the challenge to determine the subType based on the HttpInputMessage. Possibly I could use a Filter to set a custom header earlier when the URL is available to me, or I could use a ThreadLocal also set via a Filter. Neither sounds ideal to me.
My second approach would be to again use a Filter and this time wrap the incoming payload in an outer object which would then provide the type in a way that enables Jackson to do the work via #JsonTypeInfo. At the moment this is probably my preferred approach.
I have investigated HandlerMethodArgumentResolver but if I try to register a custom one it is registered AFTER the RequestResponseBodyMethodProcessor and that class takes priority.
Hmm, so after typing all of that out I had a quick check of something in the RequestResponseBodyMethodProcessor before posting the question and found another avenue to explore, which worked neatly.
Excuse the #Configuration / #RestController / WebMvcConfigurer mash-up and public fields, all for brevity. Here's what worked for me and achieved exactly what I wanted:
#Configuration
#RestController
#RequestMapping("/dummy")
public class DummyController implements WebMvcConfigurer {
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
#Documented
#interface BaseData {}
public static class AbstractBaseData {}
public static class DataA extends AbstractBaseData {
public String a;
}
public static class DataB extends AbstractBaseData {
public String b;
}
private final MappingJackson2HttpMessageConverter converter;
DummyController(final MappingJackson2HttpMessageConverter converter) {
this.converter = converter;
}
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(
new RequestResponseBodyMethodProcessor(Collections.singletonList(converter)) {
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(BaseData.class)
&& parameter.getParameterType() == AbstractBaseData.class;
}
#Override
protected <T> Object readWithMessageConverters(
NativeWebRequest webRequest, MethodParameter parameter, Type paramType)
throws IOException, HttpMediaTypeNotSupportedException,
HttpMessageNotReadableException {
final String uri =
webRequest.getNativeRequest(HttpServletRequest.class).getRequestURI();
return super.readWithMessageConverters(
webRequest, parameter, determineActualType(webRequest, uri));
}
private Type determineActualType(NativeWebRequest webRequest, String uri) {
if (uri.endsWith("data-a")) {
return DataA.class;
} else if (uri.endsWith("data-b")) {
return DataB.class;
}
throw new HttpMessageNotReadableException(
"Unable to determine actual type for request URI",
new ServletServerHttpRequest(
webRequest.getNativeRequest(HttpServletRequest.class)));
}
});
}
#PostMapping(
path = "/{type}",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<? extends AbstractBaseData> post(#BaseData AbstractBaseData baseData) {
return ResponseEntity.ok(baseData);
}
}
The key to this is that I stopped using #RequestBody because that is what was preventing me overriding the built-in behaviour. By using #BaseData instead I get a HandlerMethodArgumentResolver that uniquely supports the parameter.
Other than that it was a case of assembling the two objects that already did what I needed, so autowire a MappingJackson2HttpMessageConverter and instantiate a RequestResponseBodyMethodProcessor with that one converter. Then pick the right method to override so that I could control what parameter type was used at a point that I had access to the URI.
Quick test. Given the following payload for both requests ...
{
"a": "A",
"b": "B"
}
POST http://localhost:8081/dummy/data-a
... gives a response of ...
{
"a": "A"
}
POST http://localhost:8081/dummy/data-b
... gives a response of ...
{
"b": "B"
}
In our real-world example this means that we will be able to write one method each that supports the POST / PUT. We need to build the objects and configure the validation possibly - or alternatively if we use OpenAPI 3.0 which we are investigating we could generate the model and validate without writing any further code ... but that's a separate task ;)

Spring: HandlerMethodArgumentResolved not working with custom annotation parameter

I've been searching for hours on the internet, as well as attempting the solutions i've found in order to work with a custom parameter annotation on a controller method.
The idea behind this is that i want to practice how to map requests, responses and all sort of things with custom annotations when working with spring.
So what i want is to create an annotation parameter which should create a Map instance, my interface is coded this way:
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface SearchCriteria {
String value() default "";
}
The resolver:
public class SearchCriteriaResolver implements HandlerMethodArgumentResolver {
private Logger log = LoggerFactory.getLogger(SearchCriteriaResolver.class);
private Map<String, Object> parameters = new HashMap<>() {{
put("name", "");
put("limit", 10);
}};
#Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.hasParameterAnnotation(SearchCriteria.class);
}
#Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
HttpServletRequest request = (HttpServletRequest) nativeWebRequest.getNativeRequest();
log.info("Parameter test: " + request.getParameter("test"));
return this.parameters;
}
}
And the configurer:
#Configuration
#EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new SearchCriteriaResolver());
}
}
I've found on the internet multiple times that handlers are created this way. So in the controller, i am making use of the annotation like this:
#RestController
#RequestMapping("/series")
public class SeriesController {
private static final Logger log = LoggerFactory.getLogger(SeriesController.class);
#Autowired
SeriesService seriesService;
#GetMapping
public ResponseEntity<List<SeriesBatch>> getSeriesList(#SearchCriteria Map<String, Object> parameters) {
log.info("GET /series/ -> getSeriesList");
log.info(parameters.toString());
List<SeriesBatch> seriesList = this.seriesService.findAll();
return new ResponseEntity<>(seriesList, HttpStatus.OK);
}
...
}
So i've been checking in the logs everytime this endpoint is triggered, but the log on the resolver is not triggered, and the log in the controller method only shows an empty object. I've debugged the application start to see if the resolvers.add is being invoked, and it is, but for some reason i don't know, the logic for this annotation is not being executed.
NOTE: I am learning spring as well as taking back JAVA after a long time, so i would appreciate if an explanation on why it has to be that way on the answer is given.
Refactor your code in a way that the data you stored in a java.util.Map instance before will now be stored in some other object, e.g. an instance of a custom class SearchParams. You could even wrap your map as a member in that class to keep things simple for now:
class SearchParams {
private Map<String, Object> values = new HashMap<>();
public void setValue(String key, Object value) {
this.values.put(key, value);
}
public Object getValue(String key) {
this.values.get(key);
}
public Map<String, Object> list() {
return new HashMap<>(this.values);
}
}
Now change your controller method to accept a SearchParams object instead of Map<String, Object>:
public ResponseEntity<List<SeriesBatch>> getSeriesList(#SearchCriteria SearchParams parameters) { ... }
Last but not least you gotta change your #SearchCriteria annotation implementation, e.g. as follows:
public class SearchCriteriaResolver implements HandlerMethodArgumentResolver {
#Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.hasParameterAnnotation(SearchCriteria.class);
}
#Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
// somehow determine search params
// setup + return your new SearchParams object to encapsulate your determined search params
SearchParams searchParams = new SearchParams();
searchParams.add("somekey", "somevalue");
return searchParams;
}
}
Now, plz try this out and let me know if it worked out well ;-)
Detailed explanation:
Have a look at Spring's org.springframework.web.method.support.HandlerMethodArgumentResolverComposite class, especially it's getArgumentResolver(MethodParameter parameter) method. Under the hood, Spring maintains a list of different HandlerMethodArgumentResolver instances and loops over them to find one that matches a given method argument (and later on map the method argument's value to some other object!). One of the registered HandlerMethodArgumentResolvers is of type org.springframework.web.method.annotation.MapMethodProcessor. MapMethodProcessor also matches if the parameter if of type java.util.Map and matches first (see attached screenshot below). That's why your custom HandlerMethodArgumentResolver never got called...
[[https://reversecoding.net/spring-mvc-requestparam-binding-request-parameters/]] shows an example where a java.util.Map is used as a controller method's argument -> search for '7. #RequestParam with Map'
Possible way to configure order / change priority of custom any HandlerMethodArgumentResolver in WebConfig configuration class:
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
// add custom resolver to beginning of resolver list
resolvers.add(0, new SearchCriteriaResolver());
}

How to allow POST both list or map as JSON in spring controller?

I have a #PostMapping that allows the user to send a plain json map, like:
{
"firstname": "john",
"lastname": "doh"
}
Servlet:
#RestController
public class PersonController {
#PostMapping("/generic")
public void post(Map<String, String> params) {
}
}
This works fine. But now I want to accept also a list of objects on the same endpoint. But I cannot just add another method that takes a different parameter. Because spring the complains about ambiguous mapping:
#PostMapping("/generic")
public void post2(List<Map<String, String>> params) {
}
Question: how can I accept json data that can be both a Map and a List? I could lateron continue in the business code conditionally if the input is either map/list. But how can I accept them at all in a spring controller side by side?
#PostMapping
public void post(JsonNode json) {
if (json.isObject()) {
Map<String, String> map = mapper.convertValue(json, Map.class);
} else if (json.isArray()) {
List<Map<String, String>> list = mapper.convertValue(json, List.class);
}
}

Spring MVC / Rest - return standard response

In Spring MVC, I am writing something using a controller, essentially performing REST plus a few additions.
Most of the functionality is called from ExtJS to spring, and to adhere to their conventions, the return is always a json object in the format:
{
success: true,
data: { ... }
}
or in the case of an error:
{
success: false,
message: { ... }
}
For the exceptions it is easy - Spring provides #ControllerAdvice and #ExceptionHandler etc so that errors can be trapped in one place and the standard response works.
However is there an easy way to force all of the valid responses to be passed through in the same way, so that the success element could be sent every time with the data sent back in the data object. in other words, currently for every call I have to do something like
#RequestMapping(method = RequestMethod.POST)
#ResponseBody
public HashMap<String, Object> add(#RequestBody ManagedView targetTest) {
ManagedView result = service.add(targetTest);
HashMap<String, Object> ret = new HashMap<String, Object>();
ret.put("success", new Boolean(true));
ret.put("data", result);
return ret;
}
whereas I would like to do is have something like
#RequestMapping(method = RequestMethod.POST)
public ManagedView add(#RequestBody ManagedView targetTest) {
return service.add(targetTest);
}
// and each response intercepted by something like this
public HashMap<String, Object> addWrapper(Object result) {
HashMap<String, Object> ret = new HashMap<String, Object>();
ret.put("success", new Boolean(true));
ret.put("data", result);
return ret;
}
I appreciate I could just create a standard utility type function to do this but is there something that does this out of the box without me having to add the same code to all of the methods - preferably set globally.
Thanks in advance
I think you can use org.springframework.web.servlet.HandlerInterceptor.
It contains preHandle(),postHandle() and afterCompletion() methods. In your case you can use postHandle() method to manipulate the ModelAndView object before render it to view page,and it will be common point for all the controllers.
This is a solution I just used, I went through several approaches until I found this one, which is the simplest one I think:
#ControllerAdvice
public class StandardResponseBodyWrapAdvice implements ResponseBodyAdvice<Object> {
#Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return (AnnotationUtils.findAnnotation(returnType.getContainingClass(), ResponseBody.class) != null ||
returnType.getMethodAnnotation(ResponseBody.class) != null);
}
#Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
ApiResponseDTO<Object> resp = new ApiResponseDTO<>();
resp.setVersion("1.0");
resp.setSuccess(true);
resp.setData(body);
return resp;
}
}

Spring programmatically register RequestMapping

I have a working url as this: localhost/info
#Controller
#RequestMapping("/info")
public class VersionController {
#RequestMapping(value = "", method = RequestMethod.GET)
public #ResponseBody
Map get() {
loadProperties();
Map<String, String> m = new HashMap<String, String>();
m.put("buildTimestamp", properties.getProperty("Application-Build-Timestamp"));
m.put("version", properties.getProperty("Application-Version"));
return m;
}
}
and I would to register some other mappings at initializing of my application as this:
localhost/xxxx/info
localhost/yyyy/info
localhost/zzzz/info
All these urls will return same response as localhost/info
The xxxx, yyyy part of the application is changeable. I have to register custom mappings as
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("???").setViewName("???");
}
Bu this is only working for views.
Any idea for dynamic registration?
You can register a new HandlerMapping where you can add the handlers for your URL paths; the most convenient implementation would be SimpleUrlHandlerMapping.
If you want those handlers to be bean methods (like those annotated with #RequestMapping) you should define them as HandlerMethod wrappers so that the already registered RequestMappingHandlerAdapter will invoke them.
As of Spring 5.0.M2, Spring provides a functional web framework that allows you to create "controller"-like constructs programmatically.
What you would need to do in your case is create a proper RouterFunction for the URLs you need to handle, and then simply handle the request with the appropriate HandlerFunction.
Keep in mind however that these constructs are not part of Spring MVC, but part of Spring Reactive.
Check out this blog post for more details
I believe that simple example is worth more than 1000 words :) In SpringBoot it will look like...
Define your controller (example health endpoint):
public class HealthController extends AbstractController {
#Override
protected ModelAndView handleRequestInternal(#NotNull HttpServletRequest request, #NotNull HttpServletResponse response) {
return new ModelAndView(new MappingJacksonSingleView(), "health", Health.up().build());
}
}
And create your configuration:
#Configuration
public class CustomHealthConfig {
#Bean
public HealthController healthController() {
return new HealthController();
}
#Bean
public SimpleUrlHandlerMapping simpleUrlHandlerMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setOrder(Integer.MAX_VALUE - 2);
mapping.setUrlMap(ImmutableMap.of("/health", healthController()));
return mapping;
}
}
Regarding the handler order - take a look about the reason here: Java configuration of SimpleUrlHandlerMapping (Spring boot)
The MappingJacksonSingleView is auxiliary class in order to return single json value for model and view:
public class MappingJacksonSingleView extends MappingJackson2JsonView {
#Override
#SuppressWarnings("unchecked")
protected Object filterModel(Map<String, Object> model) {
Object result = super.filterModel(model);
if (!(result instanceof Map)) {
return result;
}
Map map = (Map) result;
if (map.size() == 1) {
return map.values().toArray()[0];
}
return map;
}
}
Jackson single view source - this blog post: https://www.pascaldimassimo.com/2010/04/13/how-to-return-a-single-json-list-out-of-mappingjacksonjsonview/
Hope it helps!
It is possible (now) to register request mapping by RequestMappingHandlerMapping.registerMapping() method.
Example:
#Autowired
RequestMappingHandlerMapping requestMappingHandlerMapping;
public void register(MyController myController) throws Exception {
RequestMappingInfo mappingInfo = RequestMappingInfo.paths("xxxx/info").methods(RequestMethod.GET).build();
Method method = myController.getClass().getMethod("info");
requestMappingHandlerMapping.registerMapping(mappingInfo, myController, method);
}
You could register your own HandlerMapping by extending the RequestMappingHandlerMapping, e.g. override the registerHandlerMethod.
It's not quite clear what you're trying to achieve, but maybe you can use #PathVariable in your #RequestMapping, something like:
#RequestMapping("/affId/{id}")
public void myMethod(#PathVariable("id") String id) {}
Edit: Original example has changed it appears, but you might be able to use PathVariable anyway.

Categories