Spring MVC ModelAttribute as Interface - java

Using a Spring MVC #Controller, how do I have a #RequestMapping endpoint have a #ModelAttribute declared as an interface?
I want to have three different forms being posted to the mapping that the underlying classes are all of an interface type.
So for example, I can have three different form objects with the action to the following:
#RequestMapping(path="/doSomething", method=RequestMethod.POST)
public String doSomething(ObjectInterface formInfo) {
...
}
(Where ObjectInterface is an interface, not a concrete class.)

It can be done using request level model attributes as follows:
Suppose There is ObjectInterace is an interface with three implementation classes as ObjectA, ObjectB and ObjectC. then Controller method declaration is:
#RequestMapping(path="/doSomething", method=RequestMethod.POST)
public String doSomething(#ModelAttribute("object") ObjectInterface formInfo) {
...
}
Add Method to populate modelattribute in the controller class:
#ModelAttribute("object")
public ObjectInterface getModelObject(HttpServletRequest request) {
ObjectInterface object = null;
String type = request.getParameter("type");
if (StringUtils.equals("A", type)) {
object= new objectA();
} else if (StringUtils.equals("B", type)){
object= new objectB();
}else if (StringUtils.equals("C", type)){
object= new objectC();
}else{
//object=any default object.
}
return object ;
}
the value returned by getModelObject is added to the Model and it will be populated with the values from the view to the controller method.
Before invoking the handler method, Spring invokes all the methods that have #ModelAttribute annotation. It adds the data returned by these methods to a temporary Map object. The data from this Map would be added to the final Model after the execution of the handler method.

Figured it out. It is to write and register a custom HandlerMethodArgumentResolver. Below is the core code. You just need to figure out which concrete bean class to pass into the webDataBinderFactory. Your controller can then be written to accept an interface and you will be provided the concrete implementing bean behind the interface.
public class MessageResolverTest implements HandlerMethodArgumentResolver {
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterType().equals(<Interface>.class);
}
public Object resolveArgument(MethodParameter methodParameter,
ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest,
WebDataBinderFactory webDataBinderFactory) throws Exception {
String name = ModelFactory.getNameForParameter(methodParameter);
WebDataBinder webDataBinder = webDataBinderFactory.createBinder(nativeWebRequest, new <ConcreteBean>(), name);
MutablePropertyValues mutablePropertyValues = new MutablePropertyValues(nativeWebRequest.getParameterMap());
webDataBinder.bind(mutablePropertyValues);
return webDataBinder.getBindingResult().getTarget();
}
}

Related

Spring Boot: Optional mapping of request parameters to POJO

I'm trying to map request parameters of a controller method into a POJO object, but only if any of its fields are present. However, I can't seem to find a way to achieve this. I have the following POJO:
public class TimeWindowModel {
#NotNull
public Date from;
#NotNull
public Date to;
}
If none of the fields are specified, I'd like to get an empty Optional, otherwise I'd get an Optional with a validated instance of the POJO. Spring supports mapping request parameter into POJO objects by leaving them unannotated in the handler:
#GetMapping("/shop/{shopId}/slot")
public Slice<Slot> getSlots(#RequestAttribute("staff") Staff staff,
#PathVariable("shopId") Long shopId, #Valid TimeWindowModel timeWindow) {
// controller code
}
With this, Spring will map request parameters "from" and "to" to an instance of TimeWindowModel. However, I want to make this mapping optional. For POST requests you can use #RequestBody #Valid Optional<T>, which will give you an Optional<T> containing an instance of T, but only if a request body was provided, otherwise it will be empty. This makes #Valid work as expected.
When not annotated, Optional<T> doesn't appear to do anything. You always get an Optional<T> with an instance of the POJO. This is problematic when combined with #Valid because it will complain that "from" and "to" are not set.
The goal is to get either (a) an instance of the POJO where both "from" and "to" are not null or (b) nothing at all. If only one of them is specified, then #Valid should fail and report that the other is missing.
I came up with a solution with a custom HandlerMethodArgumentResolver, Jackson and Jackson Databind.
The annotation:
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
public #interface RequestParamBind {
}
The resolver:
public class RequestParamBindResolver implements HandlerMethodArgumentResolver {
private final ObjectMapper mapper;
public RequestParamBindResolver(ObjectMapper mapper) {
this.mapper = mapper.copy();
this.mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterAnnotation(RequestParamBind.class) != null;
}
#Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mav, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
// take the first instance of each request parameter
Map<String, String> requestParameters = webRequest.getParameterMap()
.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue()[0]));
// perform the actual resolution
Object resolved = doResolveArgument(parameter, requestParameters);
// *sigh*
// see: https://stackoverflow.com/questions/18091936/spring-mvc-valid-validation-with-custom-handlermethodargumentresolver
if (parameter.hasParameterAnnotation(Valid.class)) {
String parameterName = Conventions.getVariableNameForParameter(parameter);
WebDataBinder binder = binderFactory.createBinder(webRequest, resolved, parameterName);
// DataBinder constructor unwraps Optional, so the target could be null
if (binder.getTarget() != null) {
binder.validate();
BindingResult bindingResult = binder.getBindingResult();
if (bindingResult.getErrorCount() > 0)
throw new MethodArgumentNotValidException(parameter, bindingResult);
}
}
return resolved;
}
private Object doResolveArgument(MethodParameter parameter, Map<String, String> requestParameters) {
Class<?> clazz = parameter.getParameterType();
if (clazz != Optional.class)
return mapper.convertValue(requestParameters, clazz);
// special case for Optional<T>
Type type = parameter.getGenericParameterType();
Class<?> optionalType = (Class<?>)((ParameterizedType)type).getActualTypeArguments()[0];
Object obj = mapper.convertValue(requestParameters, optionalType);
// convert back to a map to find if any fields were set
// TODO: how can we tell null from not set?
if (mapper.convertValue(obj, new TypeReference<Map<String, String>>() {})
.values().stream().anyMatch(Objects::nonNull))
return Optional.of(obj);
return Optional.empty();
}
}
Then, we register it:
#Configuration
public class WebConfig implements WebMvcConfigurer {
//...
#Override
public void addArgumentResolvers(
List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new RequestParamBindResolver(new ObjectMapper()));
}
}
Finally, we can use it like so:
#GetMapping("/shop/{shopId}/slot")
public Slice<Slot> getSlots(#RequestAttribute("staff") Staff staff,
#PathVariable("shopId") Long shopId,
#RequestParamBind #Valid Optional<TimeWindowModel> timeWindow) {
// controller code
}
Which works exactly as you'd expect.
I'm sure it's possible to accomplish this by using Spring's own DataBind classes in the resolver. However, Jackson Databind seemed like the most straight-forward solution. That said, it's not able to distinguish between fields that are set to null and fields that just not set. This is not really an issue for my use-case, but it's something that should be noted.
To implement logic (a) both not null or (b) both are nulls you need to implement custom validation.
Examples are here:
https://blog.clairvoyantsoft.com/spring-boot-creating-a-custom-annotation-for-validation-edafbf9a97a4
https://www.baeldung.com/spring-mvc-custom-validator
Generally, you create a new annotation, it's just a stub, and then you create a validator which implements ConstraintValidator where you provide your logic and then you put your new annotation to your POJO.

Varargs in thymeleaf #ModelAttribute?

I want to pass multiple variable parameters (including none) for a #ModelAttribute evaluation from html thymeleaf page to a method:
//provide 0 or X params
#ModelAttribute("url")
public String url(String... params) {
return "generatedurl";
}
The following thymeleaf statements should be possible:
th:href="#{${url()}"
th:href="#{${url('page')}}"
th:href="#{${url('page', 'sort')}}"
But it does not work. Why?
#ModelAttribute is used to bind common objects to the model.
You are returning a String generatedurl every time from your method url() annotated with #ModelAttribute. So in your Thymleaf view every time yo do ${url} you get generatedurl.
One workaround for your problem could be this
#ModelAttribute("url")
public void url(Model model) {
model.addAttribute("url","YOUR_URL");
model.addAttribute("sort","age");
}
As a workaround: add the enclosing class as a model parameter, and call the method on this param from thymeleaf:
#Controller
public class PageController {
#GetMapping
public String persons(Model model) {
model.addAttribute("util", this);
return "persons";
}
public String url(String... params) {
//evaluate...
return "generatedurl";
}
}
It's then possible to access the methods as normal:
th:href="#{${url()}"
th:href="#{${url('page')}}"
th:href="#{${url('page', 'sort')}}"
Only drawback is of course having to add the class to the model explicit.
Another choice would be to initialize the model with the parameter permutations that I need. But that's less flexible:
#ModelAttribute
public void init(Model model) {
model.addAttribute("plainUrl", ...);
model.addAttribute("pageUrl", ...);
model.addAttribute("pageSizeUrl", ...);
}

Does Spring MVC 4 consider Jackson annotation of parameters?

There is a class as follow:
class A {
#JsonProperty("first_name")
String firstName;
}
And a MVC controller:
public A createNewA(A a){
...
}
I expect the following REST create an instance of A with first name:
POST: /path/to/resouce
Form data
first_name: Ali
But the first name is null. By the way, the following request works fine:
POST: /path/to/resouce
Form data
firstName: Ali
As I debug, ServletModelAttributeMethodProcessor is used to resolve parameter and where objects are Considered as bean.
Is there any parameter resolver to check jackson annotation?
Jackson is used to parse and serialize JSON. You're sending x-www-form-urlencoded data. So Jackson is irrelevant.
If you send a JSON request body, and thus annotate the a parameter with #RequestBody, then Jackson will be used, and will honor the annotation.
Default resolver of spring-mvc to convert method arguments to bean objects uses exactly name of fields in the bean to search in key-values sent in the request. If you want another behavior you should implement yours resolver. To do that you should implement HandlerMethodArgumentResolver which has two main method.
In your case which you want resolver behaves similar to jackson deserializer you could implement such resolver as follow. This resolver create an map of key-values from parameters of sent request then uses jackson mapper to fill a bean object by given key-values:
public class BeanObjectParameterResolver implements HandlerMethodArgumentResolver {
#Inject
ObjectMapper mapper;
#Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
Map<String, Object> params = new HashMap<>();
Iterator<String> enumName = webRequest.getParameterNames();
while(enumName.hasNext()){
String name = enumName.next();
params.put(name, webRequest.getParameter(name));
}
// jackson 1.9 and before
// mapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// or jackson 2.0
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
Object obj = mapper.convertValue(params, parameter.getParameterType());
return obj;
}
#Override
public boolean supportsParameter(MethodParameter parameter) {
return !BeanUtils.isSimpleProperty(parameter.getParameterType());
}
}

It is possible to send more than one type of object from JSP to Spring Controller?

I'm coding a JSP view that has to send multiple objects to a Spring #Controller once submitted.
The controller's handler has the following signature:
public ModelAndView handlerX(#ModelAttribute ModelMap model){
I've tried something like this in my JSP:
<form method="post" action="action">
<spring:bind path="objectX.name">
<input type="text" name="${status.expression}" value="${status.value}" readonly="readonly" />
</spring:bind>
But when the debugger reaches the controller, the model object doesn't contain any of the form's values.
Can anybody please give some advice on how to design the form? I think I can not wrap the two different objects in a Command-type-object, as the controller's handler accepts only a ModelMap. Thank you very much!
You could solve this problem by tailoring your own HandlerMethodArgumentResolver to bind request paramaters sent from your form to your model argument. For example:
public class RequestToModelBindingArgumentResolver implements HandlerMethodArgumentResolver, Ordered {
#Override
public boolean supportsParameter(final MethodParameter parameter) {
return parameter.hasParameterAnnotation(ModelAttribute.class) &&
parameter.getParameterType() == ModelMap.class;
}
#Override
public Object resolveArgument(final MethodParameter parameter, final ModelAndViewContainer mavContainer, final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory) throws Exception {
ModelMap model = mavContainer.getModel();
Map<String, String[]> requestParameters = webRequest.getParameterMap();
// Bind all request parameters to the model
for (String param : requestParameters.keySet()) {
String[] values = requestParameters.get(param);
if (values.length == 1) {
model.addAttribute(param, values[0]);
} else {
model.addAttribute(param, values);
}
}
return model;
}
#Override
public int getOrder() {
return HIGHEST_PRECEDENCE;
}
}
An instance of this class can be added to the argument resolver list your application configuration.
#Override
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new RequestToModelBindingArgumentResolver());
}
There is a problem though. Arguments annotated with #ModelAttribute will be resolved by ModelAttributeMethodProcessor and since no ordering of HandlerMethodArgumentResolvers takes place it will always be the first to resolve the value. This means that if you would add a custom argument resolver it will never be reached. This means that we need to find a way to sort the collection of resolvers (That is the reason the resolver implements Ordered).
One easy way to sort the resolver collection is to inject the RequestmappingHandlerAdapter into the configuration.
#Autowired
private RequestMappingHandlerAdapter adapter;
Now we need a method to be invoked after the configuration is constructed so we have a chance to sort the collection of resolvers.
#PostConstruct
public void orderArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(adapter.getArgumentResolvers());
Collections.sort(resolvers, new OrderComparator());
adapter.setArgumentResolvers(resolvers);
}
Since adapter.getArgumentResolvers() will return a unmodifiable list we need to jump a little hoop here before the sorting can commence. After sorting, the RequestToModelBindingArgumentResolver instance will be on top of the list and the first to respond to the support() call.
But hey! I think it's much easier to just alter the handlers signature :)
You can create a form having fields matching the form fields and then get that form like this:
#RequestMapping(method= RequestMethod.POST)
public Response add(#RequestBody Form form, HttpServletRequest request){
//The form element's fields must match with fields in your form. Especially the names and types.
}
Apart from these 2 parameters that are passed in add(), you can pass many more

Could Spring MVC call #ModelAttribute after #RequestMapping?

I have a controller like this:
#Controller
public class HomeController {
#RequestMapping(value = "/update", method = RequestMethod.POST)
public String update(#RequestParam("user") User user, ModelMap model){
SaveUserToDatabase(user);
return "index";
}
#ModelAttribute("user")
String getUser() {
return LoadCurrentUserFromDataBase();
}
}
In short, my views would render user in almost every actions in HomeController,
but I don't want to code:
model.addAttribute("user", LoadCurrentUserFromDataBase())
in every actions, instead I'm seeking a way like #ModelAttribute to expose user to all my views.
However, according to the docs, #ModelAttribute methods in a controller are invoked before #RequestMapping methods, within the same controller.
As to my code, getUser is called before update, but i'd like to get the updated user.
Is there a way to expose the user attribute after actions without explicitly call model.addAttribute in every actions?
Each time you do a POST, make a redirection. That way, your POST method will only be responsible for updating the data, and the updated data will be loaded by the target controller.
So in this case, the update() method would redirect to another controller which would call the getUser() method before its GET method.
The Post / redirect / GET solution is valid if it works for you.
However, I had a similar situation where I had model attributes that needed to be written by all my controllers, after request processing (tracking information mostly). What I did was to register a custom interface (e.g. AdditionalModelDataSupplier), which I apply to all controllers that need to provide additional data. The interface would have a method like this:
void provideAdditionalData(Model model, HttpServletRequest request);
Now, I wrote an interceptor that in the postHandle method checks the Controller bean for this interface and calls this method:
#Override
public void postHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler,
final ModelAndView modelAndView) throws Exception {
AdditionalModelDataSupplier modelDataSupplier = findAdditionalModelDataSupplier(handler);
if (modelDataSupplier != null) {
final ModelMap modelMap = modelAndView.getModelMap();
final Model targetModel;
if (modelMap instanceof Model) {
targetModel = (Model) modelMap;
} else {
// the modelmap doesn't implement model, so we need to provide a wrapper view
targetModel = new ForwardingModel(modelMap);
}
modelDataSupplier.provideAdditionalData(targetModel, request);
}
}
#Nullable
private static AdditionalModelDataSupplier findAdditionalModelDataSupplier(final Object handler) {
if (handler instanceof AdditionalModelDataSupplier) {
return (AdditionalModelDataSupplier) handler;
}
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Object bean = handlerMethod.getBean();
if (bean instanceof AdditionalModelDataSupplier) {
return (AdditionalModelDataSupplier) bean;
}
}
return null;
}
(the ForwardingModel class mentioned above is trivial to create, it just delegates everything to the ModelMap)

Categories

Resources