Spring mvc arraylist auto binding converts into string array - java

I am using auto-binding feature for field skills (Array list) in my View:
...
<p>
Student's Skills <select name="skills" multiple>
<option value="Java Core"> Java Core </option>
<option value="Spring Core"> Spring Core </option>
<option value="Spring MVC"> Spring MVC </option>
</select>
</p>
(Action is for ` "/MySpringMVCProject3/submitAddmission.html" method="post" `)
...
And this is my model class:
public class Student {
...//name, age fields
private ArrayList<String> skills;
public ArrayList<String> getSkills() {
return skills;
}
public void setSkills(ArrayList<String> skils) {
this.skills = skils;
}
//other getter/setters
}
This is my controller:
#Controller
public class AdmissionController {
#RequestMapping(value = "/submitAddmission.html", method = RequestMethod.POST)
public ModelAndView submitAdmissionForm(#ModelAttribute("st1") Student student1, BindingResult result) {
if (result.hasErrors()) {
ModelAndView model = new ModelAndView("AdmissionForm");
return model;
}
ModelAndView model2 = new ModelAndView("AdmissionSuccess");
return model2;
}
}
But when i clicked to submit button, this binding result error appears:
Failed to convert property value of type java.lang.String[] to required type java.util.ArrayList for property skills; nested exception is java.lang.IllegalStateException: Cannot convert value of type [java.lang.String[]] to required type [java.util.ArrayList] for property skills: no matching editors or conversion strategy found
Why Spring expected an array of String instead of String arraylist while skills type is an String arraylist?

When you post a form with a multiple select option, Spring parses the parameters in an array of Strings.
Let's take a closer look at your error message.
Line 1:
Failed to convert property value of type java.lang.String[] to required type java.util.ArrayList for property skills;
Spring parses the String[] from the URL parameters and doing:
String[] input = { "foo", "bar" };
ArrayList<String> skills = (ArrayList<String>) input;
is obviously going to fail, since Java doesn't automatically know how to typecast it. However, there are a few simple conversions built in, like String[] into List<String>, as shown here.
Line 2:
nested exception is java.lang.IllegalStateException: Cannot convert value of type [java.lang.String[]] to required type [java.util.ArrayList] for property skills: no matching editors or conversion strategy found
You can teach Spring to convert basically anything into anything, if you define a proper conversion strategy. This works by building a Converter class to automatically convert A into B and then teaching Spring to use it. Here's another answer, that outlines how to do that.

add mvc:annotation-driven namespace in xxxx-dispatcher-servlet.xml

Related

Using Immutables with Thymeleaf multiple select for an Enum List

I've started using Thymeleaf recently, and generally been getting on pretty well. However I'm trying to use a multiple select to populate a list of Enums that are wrapped up in an immutable.
I refactored it and it works using a POJO:
public enum Place {
FIRST("First"),
SECOND("Second"),
THIRD("Third"),
FOURTH("Fourth"),
FIFTH("Fifth");
private final String formatted;
Place(String s) {
formatted = s;
}
public String getFormatted() {
return formatted;
}
}
public class Places {
private List<Place> places;
// get/set/constructors omitted
}
#GetMapping("/complex")
public ModelAndView complex() {
Places places = new Places(Place.THIRD);
ModelAndView model = new ModelAndView("complex");
model.addObject("filter", places);
return model;
}
#RequestMapping(value = "/complex",
method = RequestMethod.POST)
public ModelAndView multiPost(#ModelAttribute Places places, ModelAndView model) {
model.addObject("filter", places);
model.setViewName("complex");
System.out.println("post " + places.getPlaces());
return model;
}
<form id="main_form" class="mainform"
method="post" th:action="#{/complex}"
th:object="${filter}">
<div class="col-md-3">
<label id="include">Included</label>
<select class="form-select" aria-label=".form-select-sm example" multiple="multiple"
th:field="*{{places}}">
<option th:each="state : ${T(uk.co.enums.Place).values()}"
th:value="${state}"
th:text="${state.getFormatted()}">
</option>
</select>
</div>
<input type="submit" value="Create">
</form>
However, I'd like to be able to use an object created through the immutables library to replace the Places class.
#Value.Immutable
public interface Places {
List<Place> places();
}
However this produces:
Caused by: org.springframework.core.convert.ConversionFailedException:
Failed to convert from type [java.lang.String] to type [com.google.common.collect.ImmutableList<uk.co.enums.Place>] for value 'FIRST';
nested exception is java.lang.IllegalArgumentException:
Could not instantiate Collection type: com.google.common.collect.ImmutableList
I created the converters (StringToListPlaceConverter and StringArrToPlacesConverter) and added them to the formatter registry which works, but ends up being similar in length to having a POJO, but with extra classes dotted around. These also seemed to require explicit use of the ImmutableList, which feels wrong to me:
public class StringToListPlaceConverter implements Converter<String, ImmutableList<Place>> {
#Override
public ImmutableList<Place> convert(String from) {
System.out.println("from = " + from);
return ImmutableList.of(Place.valueOf(from));
}
}
Am I missing something annotation-wise with the immutables? Or is this something that needs to stay as plain as possible while interfacing between the web and java-side?

Should I instantiate a field when I use lombok

I use lombok to omit the getter and setter of a java bean.
Here is a example from the book "Spring in Action 5 edition"
A java bean:
#Data
public class Taco {
#Size(min=1, message="You must choose at least 1 ingredient")
private List<String> ingredients;
}
Controller:
#PostMapping
public String processDesign(#Valid #ModelAttribute("design") Taco design, Errors errors, Model model) {
if (errors.hasErrors()) {
return "design";
}
System.out.println(design.getIngredients());
return "redirect:/orders/current";
}
The rendered view:
<form method="POST">
<input name="ingredients" type="checkbox" value="FLTO">
<span>Flour Tortilla</span><br>
<input name="ingredients" type="checkbox" value="GRBF">
<span>Ground Beef</span><br>
</form>
When I submit the form and no checkbox is checked,the validation does not work,in the controller, errors.hasErrors() is false,and design.getIngredients() is null
Then I change the code in the java bean:
private List<String> ingredients=new ArrayList<>();
validation works,user will get the message:"You must choose at least 1 ingredient"
But my question is : should I instantiate a field even I already used lombok,especially for a reference field?Is there a way use an annotation to do it?
You can define the nullary constructor and initialize the List inside it. Lomboks constructor will be overridden.
#Data
public class Taco {
#Size(min=1, message="You must choose at least 1 ingredient")
private List<String> ingredients;
public Taco() {
ingredients = new ArrayList<>()
}
}

failed to convert from String to type Long when field is LOB

in the spring MVC application, I have Question entity class
#Entity
public class Question {
#Lob
#Column(name="QUESTION_TITLE")
private String question;
...
}
I use Thymeleaf. for this field my view is bellow
<input type="text" class="form-control" id="question"
th:field="*{question}" th:value="${question}" placeholder="">
my controller save method is
#PostMapping("/save")
public String saveQuestion(Question question, BindingResult bindingResult){
questionService.save(question);
return "redirect:/admin/questions/all/";
}
but when I submit, I have got error
Failed to bind request element:
org.springframework.beans.TypeMismatchException:Failed to convert value of type 'java.lang.String' to required type com.sendit.security.model.Question';
nested exception is
org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.lang.Long] for value 'what';
nested exception is java.lang.NumberFormatException:
For input string: "what"
when I add #Convert(converter = QuestionConverter.class) attribute to the question field and implemented QuestionConverter method like bellow.
#Converter
public static class QuestionConverter implements AttributeConverter<String, Integer> {
#Override
public Integer convertToDatabaseColumn(String attribute) {
return attribute.length();
}
#Override
public String convertToEntityAttribute(Integer dbData) {
return "";
}
}
I got the error again.
I have found the solution. simply have changed "question" field name to another field name (like "title") and it works. it was caused because when in the View I want to send "question" attribute, it known like object and sent object hash value.

Spring MVC Rest webservice: using HashMap in request bean

I have the following Spring MVC Rest controller:
#ResponseStatus(HttpStatus.OK)
#RequestMapping(value = "/zoek", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public #ResponseBody
JsonExcelLijstList zoek(ZoekExcelLijstParameters parameters) {
// arbitraty code
}
The ZoekExcelLijstParameters object looks like this:
public class ZoekExcelLijstParameters extends NgTableParams {
private String typeSleutel;
private String status;
private Date datumVan;
private Date datumTot;
// getters and setters
}
and the NgTableParams looks like this
public class NgTableParams {
private int page = 1;
private int count = 20 ;
private HashMap<String, String> sorting; // HashMap because Jackson doesn't like interfaces (?)
// getters and setters
}
I've already configured the Jackson Message Converter:
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter" />
</mvc:message-converters>
</mvc:annotation-driven>
The data that I'm sending using Angular.js looks like this (as represented by Chrome dev tools, so this is not JSON)
count:10
dateTot:
dateVan:
page:1
sorting:{"uploadDate":"asc", "uploader": "asc"}
status:
type:
When I do this, I get the following error:
BindException:org.springframework.validation.BeanPropertyBindingResult:
1 errors↵Field error in object 'zoekExcelLijstParameters' on field
'sorting': rejected value [{"uploadDate":"asc"}]; codes
[typeMismatch.zoekExcelLijstParameters.sorting,typeMismatch.sorting,typeMismatch.java.util.HashMap,typeMismatch];
arguments
[org.springframework.context.support.DefaultMessageSourceResolvable:
codes [zoekExcelLijstParameters.sorting,sorting]; arguments [];
default message [sorting]]; default message [Failed to convert
property value of type 'java.lang.String' to required type
'java.util.HashMap' for property 'sorting'; nested exception is
java.lang.IllegalStateException: Cannot convert value of type
[java.lang.String] to required type [java.util.HashMap] for property
'sorting': no matching editors or conversion strategy found]
I have no idea what I'm doing wrong. I've also tried using a List with key-value objects instead of a HashMap.
I've seen many solutions where they suggest to work with POST data and a request body, or just plain HashMap's, but here I need (want) it to work with a GET (since it's a 'get' operation and not a 'create' operation).
Sorry I didn't have the time to rewrite your code but here is a solution i used for a different project: Just put them in a map object
#RestController
public class NewServiceController
{
#Autowired
EsbcoreServiceRepository serviceRepo;
#Autowired
EsbcoreRuleRepository ruleRepo;
#Autowired
EsbcoreRuleConditionRepository ruleConditionRepo;
#Autowired
EsbcoreRuleDestinationRepository ruleDestinationRepo;
#Autowired
EsbcoreServiceDestinationRepository serviceDestinationRepo;
#GetMapping(value="/api/allservices")
public Map<String, Object> getAllServices()
{
Map<String, Object> servicesMap=new HashMap<String, Object>();
servicesMap.put("services", serviceRepo.findAll());
servicesMap.put("rules", ruleRepo.findAll());
servicesMap.put("ruleConditions", ruleConditionRepo.findAll());
servicesMap.put("ruleDestinations", ruleDestinationRepo.findAll());
servicesMap.put("serviceDestinations", serviceDestinationRepo.findAll());
return servicesMap;
}

Spring Formatter for input field

I have a question about the formatter in Spring.
I have a Formatter for my select boxes, for example:
public class SportTypeFormatter implements Formatter<SportType> {
#Autowired
private SportTypeRepository sportTypeRepository;
#Override
public String print(SportType sportType, Locale locale) {
return String.valueOf(sportType.getTypeId());
}
#Override
public SportType parse(String sportTypeId, Locale locale) throws ParseException {
return sportTypeRepository.findSportTypeByTypeId(Long.valueOf(sportTypeId));
}
}
in thymeleaf something like this:
<select class="form-control" name="sportTypeId" th:field="*{person.sport.sportType}">
<option th:each="spoType : ${allSportTypes}" th:value="${spoType.typeId}" th:selected="${spoType.typeId == person.sport.sportType}" th:text="#{${'login.sport.sportType.' + spo.typeId}}" >Sporttype</option>
</select>
Thats easy, because i only need one value (the id) and i'm going with select-box.
But what is if i need two values?
Suggest i have Email, there i need the id and the value (mail-address). I can build an Formatter for email but i have no chance to transfer the email and id at the same time.
In the print method i can something like that:
#Override
public String print(Email email, Locale locale) {
return email.getId() + email.getEmail();
}
in thymeleaf:
<input id="email" type="text" class="form-control" th:field="*{{person.email}}" th:placeholder="#{login.email}" />
But with this the user can see the id.
If i do the binding the "standard" Spring way i get the following exception (thats the reason why i use the formatter):
{timestamp=Wed Dec 10 11:14:47 CET 2014, status=400, error=Bad Request, exception=org.springframework.validation.BindException,
errors=[Field error in object 'person' on field 'email': rejected value [com.sample.persistence.user.model.Email#0];
codes [typeMismatch.person.email,typeMismatch.person.institutionEmployees.email,
typeMismatch.email,typeMismatch.email,
typeMismatch.email,typeMismatch.com.sample.persistence.user.model.Email,typeMismatch];
arguments [org.springframework.context.support.DefaultMessageSourceResolvable:
codes [person.email,email];
arguments [];
default message [email]];
default message [Failed to convert property value of type 'com.sample.persistence.user.model.Email'
to required type 'com.sample.persistence.user.model.Email'
for property 'email';
nested exception is org.springframework.core.convert.ConversionFailedException:
Failed to convert from type com.sample.persistence.user.model.Email
to type #javax.persistence.OneToOne #javax.persistence.JoinColumn #com.google.gson.annotations.Expose
com.sample.persistence.user.model.Email for value
'com.sample.persistence.user.model.Email#0';
nested exception is org.springframework.dao.InvalidDataAccessApiUsageException:
Provided id of the wrong type for class com.sample.persistence.user.model.Email.
Expected: class java.lang.Long, got class java.lang.String;
nested exception is java.lang.IllegalArgumentException:
Provided id of the wrong type for class com.sample.persistence.user.model.Email.
Expected: class java.lang.Long, got class java.lang.String]],
message=Validation failed for object='person'. Error count: 1, path=/manageUsers/Ab-Soul/edit}
Any suggestion are welcome.
Thanks in advance.
1. EDIT:
The Controller-method
#RequestMapping(value = "/{login}/edit", method = RequestMethod.GET)
public ModelAndView editUserByLogin(#PathVariable("login") final String login) {
final User currentUser = UserRepository.findPersonByLogin(login);
ModelAndView mav = new ModelAndView(URL_EDIT_USER);
mav.addObject(MODEL_USER, currentUser);
return mav;
}
the scenario, the 'admin' get a list of all current user, if he clicked on the table the requestmapping-method would be called with the name of the user he has clicked.
the email class:
#Entity(name="Email")
public class Email implements Serializable
{
private static final long serialVersionUID = 6891079722082340011L;
#Id()
#GeneratedValue(strategy=GenerationType.AUTO)
#Expose
protected Long emailId;
#Expose
protected String value;
//getter/setter
#Override
public boolean equals(Object obj)
{
if(obj instanceof Email){
return value.equals(((Email) obj).getValue());
}
return false;
}
#Override
public int hashCode()
{
return value.hashCode();
}
}
2. EDIT:
Now i have change the email field of child to emailChild
org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'person' on field 'child[0].emailChild':
rejected value [com.sample.persistence.user.model.Email#0];
codes [typeMismatch.person.child[0].emailChild,
typeMismatch.person.child.emailChild,
typeMismatch.child[0].emailChild,
typeMismatch.child.emailChild,
typeMismatch.emailChild,
typeMismatch.com.sample.persistence.user.model.Email,typeMismatch];
arguments [org.springframework.context.support.DefaultMessageSourceResolvable:
codes [person.child[0].emailChild,child[0].emailChild];
arguments [];
default message [child[0].emailChild]];
default message [Failed to convert property value of type 'com.sample.persistence.user.model.Email' to required type 'com.sample.persistence.user.model.Email'
for property 'child[0].emailChild';
nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type com.sample.persistence.user.model.Email
to type #javax.persistence.OneToOne #javax.persistence.JoinColumn #com.google.gson.annotations.Expose com.sample.persistence.user.model.Email
for value 'com.sample.persistence.user.model.Email#0';
nested exception is org.springframework.dao.InvalidDataAccessApiUsageException: Provided id of the wrong type for class com.sample.persistence.user.model.Email. Expected: class java.lang.Long, got class java.lang.String;
nested exception is java.lang.IllegalArgumentException: Provided id of the wrong type for class com.sample.persistence.user.model.Email. Expected: class java.lang.Long, got class java.lang.String]
3. EDIT:
Adding the controller method for the post:
#RequestMapping(value = "/{login}/edit", method = RequestMethod.POST)
public ModelAndView updateUser(#PathVariable("login") final String login, #ModelAttribute(MODEL_USER) final Person person, BindingResult bindingResult, final Model model) {
Person repositoryPerson = personRepository.findPersonByLogin(login);
repositoryPerson = repositoryPerson.updateWith(person);
manageUserService.updatePerson(repositoryPerson);
model.asMap().clear();
return new ModelAndView("redirect:" + URL_USERS_OVERVIEW, MODEL, model);
}
I have answered my own question.
We should remember that
the registration process works fine, it would bind fine
Only the Problem occurs by the edit process, so i have a valid Email-Object in the db, bind with a valid Child-Object and this is bind with a valid User-Object.
I have decided to build a EmailFormatter with the following implemenatation:
public class EmailFormatter implements Formatter<Email> {
#Override
public String print(Email email, Locale locale) {
return email.getValue();
}
#Override
public Email parse(String mailAddress, Locale locale) throws ParseException {
return new Email(mailAddress);
}
}
with this, i get in my Child-Instance a new Email-Object (without the id). Now i have a valid Email-Object in a valid Child-Object. Take a look at the post-Method:
#RequestMapping(value = "/{login}/edit", method = RequestMethod.POST)
public ModelAndView updateUser(#PathVariable("login") final String login, #ModelAttribute(MODEL_USER) final Person person, BindingResult bindingResult, final Model model) {
Person repositoryPerson = personRepository.findPersonByLogin(login);
repositoryPerson = repositoryPerson.updateWith(person);
manageUserService.updatePerson(repositoryPerson);
model.asMap().clear();
return new ModelAndView("redirect:" + URL_USERS_OVERVIEW, MODEL, model);
}
At this time i have the login name from the user, i get the "old" (valid) user-Object from the db. I have wrote a "merge" method, i give the "old" user object the modified user object.
Here i can get the old Email Object with id and set the email-adress just like this:
this.emailChild.setValue(modifiedChild.getemailChild().getValue());
After this i look at this solution, i notice thats really simple.
But if the binding problem occurs by a "complexer" (more then one relevant field) object then, i think, you go better white the solution that PatrickLC suggest:
#InitBinder
public void setAllowedFields(WebDataBinder dataBinder) {
dataBinder.setDisallowedFields("emailId");
}

Categories