Spring.boot: binding request parameter to data object with nested Optional - spring-boot

Working with a vanilla #RestController in Spring.Boot, how can you use the default databinding and properly bind to nested Optional attributes in a data object?
In terms of code, this is the data class (using lombok to reduce the boilerplate)
#Data
public class SomeData {
Optional<String> name;
}
which is used as a request parameter in a GET route, like this
#GetMapping("/something")
public void getSomething(SomeData data) {
...
}
The problem: if no corresponding parameter is given in the request, then data.name is null instead of Optional.empty.
What I found so far:
* Optional is handled properly when you just use it directly, like getSomething(Optional<String> name)
* Additional annotations like #RequestParam or #Valid don't affect this behavior

Related

Providing default values for validation in SpringBoot

I want SpringBoot to be able to provide default values for fields that the user must enter. For example, I have something like this:
*Controller class*
#PostMapping("/test")
public ResponseEntity<> myMethod(#RequestBody #Valid MyContract contract) {}
*MyContract class*
#Valid
DataObject dataObject;
*DataObject class*
#Component
public class DataObject {
private #Value("${field1.default}") String field1Default;
private String field1
public String getField1() {
return (field1 == null ? field1Default : field1);
}
}
The DataObject class needs to be created on a per request basis. There are also other places in the code where it needs to be created on demand. So I imagine it needs to be a Prototype object. But I can't figure out how to get Spring to created it properly when it creates it for the request.
Update
I have read more about #RequstBody, e.g., https://www.javadevjournal.com/spring/spring-request-response-body/ and Should spring #RequestBody class be singleton or prototype?, which explains that the object is not a Component, but a simple POJO that gets the values from the Json request. So it seems that there is no way to inject #Values from the Spring application.properties file. Is there any other way around this? Or another suggested implementation?

Binding request parameters (GET query) starting with underscore "_" to bean attributes

I need to handle requests like:
http://host/path?_param1=abc&_param2=xxx...
and bind them to bean, like:
#RestController
public class Controller {
#GetMapping("/path")
public String endpoint(#Valid Data data) {
...;
}
static public class Data {
private int _param1;
private String _param2;
...
public int get_param1() {
return _param1;
}
public void set_param1(int _param1) {
this._param1 = _param1;
}
...
}
}
The problem is that Spring ignores properties starting with underscore "_" or is unable to bind them to bean properly. I am just getting empty properties in data bean. Other properties are bound as expected.
Is there a way to handle that? I cannot change the URL and param names...
It costed me some time but I figured out how to solve it. Spring binding has by default turned on mechanism to handle missing attribute values and to distinguish them from just not used attributes (i.e. http checkbox when is not checked does not send any param, but yet it was in form and this case should be treated as "false"/"null" as opposite to case when there was no such checkbox in form element). To do that every such attribute has redundant attribute prefixed with underscore ("checkboxField" has "_checkboxField" companion that is a hidden field and is always sent).
But processing such "companions" looks for field without underscore prefix and creates one with null value when it is not found.
To turn off the mechanism one must use #InitBinder method:
#RestController
public class MyController {
#InitBinder
public void customizeBinding(WebDataBinder binder) {
binder.setFieldMarkerPrefix(null); //required to handle underscore prefixed fields ("_field")
}
#GetMapping(path = "/items")
String endpoint( #RequestParam("_param") String param ) {
... // param is populated with query string "_param"
}
}

Ajax POST Array of Objects to Spring MVC Controller - Multiple Errors

I need to submit a JS-formed Array of Objects to a Spring MVC Controller. All the property names match.
#PostMapping("/addAdmin")
public void addAdmin(#RequestParam List<UserRolesGUIBean> userRolesGUIBeans)
{
// ...
}
JS:
var entries = [];
//...
// entries is an array of objects of the form {id: "..", role: ".."}
// verified to be correct before submission
$.ajax({
type : "post",
dataType : "json",
url : 'addAdmin',
data : JSON.stringify(entries)
})
Bean
public class UserRolesGUIBean implements Serializable {
private String id;
private String role;
// + Constructors (Empty + Full), Getters and setters
}
Error:
Required List parameter 'userRolesGUIBeans' is not present]
Also tried this with ModelAttribute and an ArrayList,
PostMapping("/addAdmin")
public void addAdmin(#ModelAttribute ArrayList<UserRolesGUIBean> userRolesGUIBeans) {
Now there are no errors, but the list is empty, no data was received.
Tried everything -- arrays vs. lists, JSON.stringify(data) or a data object with data {"entries" : entries}, RequestBody doesn't work and gives UTF Errors; and RequestParam as above doesn't work either.
This is way too complicated for a simple task.
You are trying to send a JSON object by using a post. You should use #RequestBody annotation.
Try to change your method in this way:
#PostMapping("/addAdmin")
public void addAdmin(#RequestBody List<UserRolesGUIBean> userRolesGUIBeans)
{
// ...
}
In this way Spring will intercept the Json and transform it in List of wished objects
SOLUTION:
1) In theory, if I was doing Form Submission (like $('#myForm').submit()), I could use #ModelAttribute to automatically bind my form to the bean. That's what #ModelAttribute does -- it's used for Form Submission. I don't have a real form; only my own custom values.
I could still "fake" a Form Submit by creating a Dynamic Form "on the fly," but I couldn't get the Arrayed-Field Form Submission (e.. obj[] with [] notation in the HTML Name) to map to a #ModelAttribute List<Bean>, so I disregarded this unusual Form Submit approach.
2) The real approach that worked is to just submit a custom JSON string which is my own. Not related to any form submission, so can't use #ModelAttribute. Instead, this is the #RequestBody approach. Then I have to parse the JSON RequestBody String myself -- and here we have to use Jackson, Java JSON, or GSON to parse the JSON Array.
In my case,
JS:
$.ajax({
type : "post",
dataType : 'json',
url : 'addAdmin',
data : JSON.stringify(entries)
})
Controller (note it takes a custom String only). Then it uses Jackson to parse the string manually, because Spring won't do it in this case. (Spring will only auto-parse if you're using #ModelAttribute form binding.)
#PostMapping("/addAdmin")
public boolean addAdmin(#RequestBody String json) throws Exception {
String decodedJson = java.net.URLDecoder.decode(json, "UTF-8");
ObjectMapper jacksonObjectMapper = new ObjectMapper(); // This is Jackson
List<UserRolesGUIBean> userRolesGUIBeans = jacksonObjectMapper.readValue(
decodedJson, new TypeReference<List<UserRolesGUIBean>>(){});
// Now I have my list of beans populated.
}
As promised, please go to https://start.spring.io/ and create a new project with a single depdendency for spring-boot-starter-web.
After that, you can create the following bean in your project.
import java.util.List;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class DemoController {
#PostMapping("/request-body")
public void getRequestBody(#RequestBody List<Person> list) {
for (Person person : list) {
System.out.println(person.name);
}
}
public static class Person {
private String name;
private String phoneNo;
/**
* #return the name
*/
public String getName() {
return name;
}
/**
* #param name the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* #return the phoneNo
*/
public String getPhoneNo() {
return phoneNo;
}
/**
* #param phoneNo the phoneNo to set
*/
public void setPhoneNo(String phoneNo) {
this.phoneNo = phoneNo;
}
}
}
Nothing special, just take in a list of Person and print out the names. You can right click and run the project directly from the IDE.
You can open Postman and make a POST request as following.
This is what gets printed in the console.
If it works with Postman, you can make it work in JS. You just haven't figured out how. Instead of settling with that "workaround" you found, I think you should find out the proper way to submit a request in JS. In addition, some understanding of the Spring framework would help too. Otherwise, you will just keep randomly trying stuff like #ModelAttribute without getting anywhere.

Sprinng 4.X passing variables with #Pathvariable annotation

I want to pass some variables to my server. I did it this way, like shown in the example:
#Controller
#RequestMapping("/owners/{ownerId}")
public class RelativePathUriTemplateController {
#RequestMapping("/pets/{petId}")
public void findPet(#PathVariable String ownerId, #PathVariable String petId, Model model) {
// implementation omitted
}
}
This works totally fine when I send a request like this:
domain/owners/123/pets/123
But what I want to do is getting all pets of one owner. This means I dont need/want to pass a pet-ID:
domain/owners/123/pets/
But then I get an Excpetion that there is no Handler for this request. Is it possible to send a request like this or is it limited by Spring?
You have to add a second method:
#RequestMapping("/pets/")
public void findPetByOwner(#PathVariable String ownerId Model model) {
// implementation omitted
}

Spring MVC : Common param in all requests

I have many controllers in my Spring MVC web application and there is a param mandatoryParam let's say which has to be present in all the requests to the web application.
Now I want to make that param-value available to all the methods in my web-layer and service-layer. How can I handle this scenario effectively?
Currently I am handling it in this way:
... controllerMethod(#RequestParam String mandatoryParam, ...)
and, then passing this param to service layer by calling it's method
#ControllerAdvice("net.myproject.mypackage")
public class MyControllerAdvice {
#ModelAttribute
public void myMethod(#RequestParam String mandatoryParam) {
// Use your mandatoryParam
}
}
myMethod() will be called for every request to any controller in the net.myproject.mypackage package. (Before Spring 4.0, you could not define a package. #ControllerAdvice applied to all controllers).
See the Spring Reference for more details on #ModelAttribute methods.
Thanks Alexey for leading the way.
His solution is:
Add a #ControllerAdvice triggering for all controllers, or selected ones
This #ControllerAdvice has a #PathVariable (for a "/path/{variable}" URL) or a #RequestParam (for a "?variable=..." in URL) to get the ID from the request (worth mentioning both annotations to avoid blind-"copy/past bug", true story ;-) )
This #ControllerAdvice then populates a model attribute with the data fetched from database (for instance)
The controllers with uses #ModelAttribute as method parameters to retrieve the data from the current request's model
I'd like to add a warning and a more complete example:
Warning: see JavaDoc for ModelAttribute.name() if no name is provided to the #ModelAttribute annotation (better to not clutter the code):
The default model attribute name is inferred from the declared
attribute type (i.e. the method parameter type or method return type),
based on the non-qualified class name:
e.g. "orderAddress" for class "mypackage.OrderAddress",
or "orderAddressList" for "List<mypackage.OrderAddress>".
The complete example:
#ControllerAdvice
public class ParentInjector {
#ModelAttribute
public void injectParent(#PathVariable long parentId, Model model) {
model.addAttribute("parentDTO", new ParentDTO(parentId, "A faked parent"));
}
}
#RestController
#RequestMapping("/api/parents/{parentId:[0-9]+}/childs")
public class ChildResource {
#GetMapping("/{childId:[0-9]+}")
public ChildDTO getOne(#ModelAttribute ParentDTO parent, long childId) {
return new ChildDTO(parent, childId, "A faked child");
}
}
To continue about the warning, requests are declaring the parameter "#ModelAttribute ParentDTO parent": the name of the model attribute is not the variable name ("parent"), nor the original "parentId", but the classname with first letter lowerified: "parentDTO", so we have to be careful to use model.addAttribute("parentDTO"...)
Edit: a simpler, less-error-prone, and more complete example:
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
#Documented
#RestController
public #interface ProjectDependantRestController {
/**
* The value may indicate a suggestion for a logical component name,
* to be turned into a Spring bean in case of an autodetected component.
*
* #return the suggested component name, if any
*/
String value() default "";
}
#ControllerAdvice(annotations = ParentDependantRestController.class)
public class ParentInjector {
#ModelAttribute
public ParentDTO injectParent(#PathVariable long parentId) {
return new ParentDTO(parentId, "A faked parent");
}
}
#ParentDependantRestController
#RequestMapping("/api/parents/{parentId:[0-9]+}/childs")
public class ChildResource {
#GetMapping("/{childId:[0-9]+}")
public ChildDTO getOne(#ModelAttribute ParentDTO parent, long childId) {
return new ChildDTO(parent, childId, "A faked child");
}
}

Resources