Spring Boot Rest Controller Array Handling - spring

Hello Spring Developers
I have a spring-boot related issue, which I don't know how to resolve properly.
This issue is a direct consequence of spring's behaviour with query parameters:
When a single query parameter contains the , character it will be split by that comma.
Example:
XXX?array=1,2,3 => ["1", "2", "3"]
When a query parameter - containing the , character - is defined multiple times, spring will not split every value by the comma.
Example:
XXX?array=1,2&array=3 => ["1,2", "3"]
Issue: Some of our query parameters may contain a , character.
Since spring does not behave like the RFC is stated I need a way to NOT split the query parameters by commas.
My Controller
#RestController
public class Controller {
#GetMapping(path = "arrays", produces = {MediaType.APPLICATION_JSON_VALUE})
#ResponseBody
public List<String> getArrayQueryParameters(#RequestParam("array") String[] array) {
return Arrays.asList(array);
}
}
My Testcase
#SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = Controller.class)
#AutoConfigureMockMvc
#EnableAutoConfiguration(exclude=SecurityAutoConfiguration.class)
public class ControllerMvcTest {
#Autowired
private MockMvc mvc;
// This Test fails
#Test
void oneParameter() throws Exception {
mvc.perform(get("/arrays?array=1,2,3,4,5")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(1)))
.andExpect(jsonPath("$[0]", is("1,2,3,4,5")));
}
#Test
void parameterTwoTimes() throws Exception {
mvc.perform(get("/arrays?array=1,2&array=3,4,5")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(2)))
.andExpect(jsonPath("$[0]", is("1,2")))
.andExpect(jsonPath("$[1]", is("3,4,5")));
}
#Test
void parameterThreeTimes() throws Exception {
mvc.perform(get("/arrays?array=1,2&array=3&array=4,5")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(3)))
.andExpect(jsonPath("$[0]", is("1,2")))
.andExpect(jsonPath("$[1]", is("3")))
.andExpect(jsonPath("$[2]", is("4,5")));
}
}
You can find an example github repo here: https://github.com/arolfes/RestArrayParametersSample

You can make query params not split by commas using #InitBinder and DataBinder.
Spring DataBinder uses StringToArrayConverter for converting String to String[], which behaves as you experienced. (Related source codes)
Classes annotated with #Controller or #ControllerAdvice can have methods with #InitBinder.
You can utilize registerCustomEditor() of DataBinder to applying PropertyEditor which not using the comma delimiter.
If you add this method for your ControllerServletRequest, the first test case will succeed. (I checked.)
#InitBinder
private void initBinder(DataBinder binder) {
binder.registerCustomEditor(String[].class, new StringArrayPropertyEditor(null));
}
As you can see, I intentionally give null for constructor of StringArrayPropertyEditor because the arg is delimiter, whose default value is ",". (Related source codes)

Related

How to disable spring boot parameter split

We have many #RestController receiving phrases in common language written by users. Phrases can be very long and contains punctuation, like periods and, of course, commas.
Simplified controller example:
#RequestMapping(value = "/countphrases", method = RequestMethod.PUT)
public String countPhrases(
#RequestParam(value = "phrase", required = false) String[] phrase) {
return "" + phrase.length;
}
Spring boot default behaviour is to split parameters values at comma, so the previous controller called with this url:
[...]/countphrases?phrase=john%20and%20me,%20you%and%her
Will return "2" istead of "1" like we want. In fact with the comma split the previous call is equivalent to:
[...]/countphrases?phrase=john%20and%20me&phrase=you%and%her
We work with natural language and we need to analyze phrases exactly how the users wrote them and to know exactly how many they wrote.
We tried this solution: https://stackoverflow.com/a/42134833/1085716 after adapting it to our spring boot version (2.0.5):
#Configuration
public class MvcConfig implements WebMvcConfigurer {
#Override
public void addFormatters(FormatterRegistry registry) {
// we hoped this code could remove the "split strings at comma"
registry.removeConvertible(String.class, Collection.class);
}
}
But it doesn't work.
Anyone know how to globally remove the "spring boot split string parameters at comma" behaviour in spring boot 2.0.5?
I find the solution.
To override a default conversion we must add a new one. If we remove the old one only it doesn't work.
The correct (example) code should be:
#Configuration
public class MvcConfig implements WebMvcConfigurer {
#Override
public void addFormatters(FormatterRegistry registry) {
registry.removeConvertible(String.class, String[].class);
registry.addConverter(String.class, String[].class, noCommaSplitStringToArrayConverter());
}
#Bean
public Converter<String, String[]> noCommaSplitStringToArrayConverter() {
return new Converter<String, String[]>() {
#Override
public String[] convert(String source) {
String[] arrayWithOneElement = {source};
return arrayWithOneElement;
}
};
}
}
This way any controller like the one in the main question will not split parameters values:
[...]/countphrases?phrase=a,b will return 1 (and fq=["a,b"])
[...]/countphrases?phrase=a,b&phrase=c,d will return 2 (and fq=["a,b", "c,d"])
Replacing your formatter registry with a completely new list could make you loose some needed default formatters that would come with the framework. This will also disable all String-To-Collections parsing for the entire application, on every endpoint, such that if you want to a request filter such as the following at another endpoint, it won't work:
identifiers = 12,34,45,56,67
Solution:
Just change your delimiter into something else... # or ; or $
identifiers = 12;23;34;45;56
This is what I have been doing, so I don't mess with all the goodies in the formatter registry.

Spring Validation Errors for RequestParam

I want to pass org.springframework.validation.Errors to CodeValidator class.
But, since I am not using RequestBody/RequestPart/ModelAttribute, I cannot put Errors in method param after variable.
I use #RequestParam for code variable, and I want to validate that using CodeValidator class that implement org.springframework.validation.Validator.
Here is my code
#RequestMapping(value = "/check-code", method = RequestMethod.POST)
public ResponseEntity<Object> checkCode(#RequestParam("code") String code, Errors errors) {
codeValidator.validate(code, errors);
if(errors.hasErrors()) {
return ResponseEntity.badRequest().body("Errors");
}
return ResponseEntity.ok("");
}
and here error result for my code:
An Errors/BindingResult argument is expected to be declared immediately after the model attribute, the #RequestBody or the #RequestPart arguments to which they apply: public org.springframework.http.ResponseEntity com.example.myapp.controller.CodeController.checkCode(java.lang.String,org.springframework.validation.BindingResult)
what should I do to be able using CodeValidator with #RequestParam?
Updated:
Code for CodeValidator
#Service
public class CodeValidator implements Validator {
#Override
public void validate(Object target, Errors errors) {
String code = ((String) target);
if(code == null || code.isEmpty()) {
errors.rejectValue("code", "", "Please fill in Code.");
}
}
}
Did you create an annotation with your validator?
Otherwise take a look at a small example/tutorial for custom validating with spring: https://www.baeldung.com/spring-mvc-custom-validator
(edit) if you are using spring boot you might need add a MethodValidationPostProcessor bean to your spring config to enable custom valdation for the #requesParam

Expose public field of POJO to FTL in Spring

I can't figure out how to send a POJO to my template in Spring Boot.
Here's my POJO and my controller:
class DebugTest {
public String field = "Wooowee";
public String toString() {
return "testie " + field;
}
}
#Controller
#RequestMapping("/debug")
public class WebDebugController {
#RequestMapping(value = "/ftl", method = RequestMethod.GET)
public ModelAndView ftlTestPage(Model model) {
DebugTest test = new DebugTest();
ModelAndView mnv = new ModelAndView("debug");
mnv.addObject("test", test);
return mnv;
}
}
Here's my template:
HERES THE TEST: ${test}$
HERES THE TEST FIELD: ${test.field}$
Here's the output (GET /debug/ftl):
HERES THE TEST: testie Wooowee$
HERES THE TEST FIELD: FreeMarker template error (DEBUG mode; use RETHROW in production!):
The following has evaluated to null or missing:
==> test.field [in template "debug.ftl" at line 3, column 25]
[Java stack trace]
The class itself (DebugTest) must be public too, as per the JavaBeans Specification. Also, fields by default aren't exposed. Defining getter methods is generally the best (with Lombok maybe), but if you want to go with fields, configure the ObjectWrapper as such. As you are using Spring Boot, I think that will be something like this in your application.properites:
spring.freemarker.settings.objectWrapper=DefaultObjectWrapper(2.3.28, exposeFields = true)

Junit passing multiple parameters to rest service

I have a rest controller like bellow :
#RequestMapping(value = "/create", method = RequestMethod.POST)
public
#ResponseBody
GlobalResponse createDeal(#RequestBody Deal deal,#RequestBody Owner owner) {
// code here
}
I use Junit and Mockito for my test :
#Test
public void createDeal() throws Exception{
this.mockMvc.perform(post("/v1/Deal/create").content("\"deal\":{\"dealNumber\":\"DA001\"},\"owner\":{\"id\":1}").contentType(MediaType.APPLICATION_JSON)).andDo(print());
}
I cant past multiple parameters to the controller service , how can I avoid this ?
You won't be able to pass multiple arguments annotated with #RequestBody annotation. The argument annotated with this annotation holds the whole request body and it can't be split into multiple.
What you can do is to have a wrapper to hold your Deal and Owner objects and you can pass that wrapper as a single request body argument.
For e.g.:
public class Wrapper {
private Deal deal;
private Owner owner;
//Getters and setters
}
And your controller's method:
#RequestMapping(value = "/create", method = RequestMethod.POST)
public
#ResponseBody
GlobalResponse createDeal(#RequestBody Wrapper wrapper) {
// code here
}
Hope this makes sense.

Spring MVC Binding: How to bind ArrayList<...>?

I've got a DTO (bean) with ArrayList field:
public MyDTO {
...
private List<MyThing> things;
...
... getters, setters and so on
}
In my initBinder I have:
#InitBinder
public void initBinder(WebDataBinder binder) {
...
binder.registerCustomEditor(List.class, "things", new PropertyEditorSupport() {
#Override
public void setAsText(String text) throws IllegalArgumentException {
List<MyThing> things = new ArrayList<MyThings>;
// fill things array with data from text
...
// On that stage things value is correct!
super.setValue(things);
}
});
}
And in my controller request method:
#RequestMapping({"save"})
public ModelAndView doSaveMyDTO(#ModelAttribute MyDTO myDTO) {
// very strange myDTO comes here=(
}
The problem is that while I'm in registerCustomEditor staff the things array is ok.
But when I get to the doSaveMyDTO method - MyDTO.things looks like Array of one element arrays of actual values:
Expected (things in initBinder):
[value1, value2, value3]
Get in doSaveMyDTO (myDTO.getThings()):
[[value1], [value2], [value3]]
Why? Please explain...
If the request is correctly formed (things=v1&things=v2&things=v3 or things=v1,v2,v3), spring's built-in converters should properly convert it to a List - no need to register your own.
If your input is JSON, then you'd need #RequestBody instead of #ModelAttribute

Resources