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<>()
}
}
Related
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?
I am trying to bind my form to a data transfer object. The form is a FreeMarker template. They are as follows:
The Data object:
#Data
public class TransferObject {
private List<Subclass> subclassInstances;
public TransferObject(Data data) {
// this takes the data and populates the object, also works
// we end up with a list of subclasses.
}
#Data //lombok -> generates getters and setters
#AllArgsConstructor
private static class Subclass {
private String id;
private String code;
}
}
The Controller:
#GetMapping({"/endpoint", "/endpoint"})
public String endpoint(Model model, #RequestParam(value="code", required=false, defaultValue="") String code) {
// this retrieves the data, but that works so it's irrelevant here
Data data = this.dataService.findByCode(code).orElse(null);
if(data != null) {
TransferObject transferObject = new TransferObject(data);
model.addAttribute("data", transferObject);
} else {
log.warn("no data found");
}
return "endpoint";
}
The Freemarker template:
<form:form action="/endpoint" method="post" modelAttribute="data">
<#if data??>
<#list data.subclasses as subclass>
${subclass} <!-- this shows an object with 2 fields that are filled -->
<#spring.bind "data.subclasses[${subclass?index}].id"/>
<input type="text" value="${subclass.id}"/> <!-- This line fails -->
<#spring.bind "data.subclasses[${subclass?index}].code"/>
<input type="text" value="${subclass.code}"/>
</#list>
</#if>
</form:form>
There is an error in the template that states:
[The following has evaluated to null or missing:
==> sublcass.id] I don't get that because I print the subclass just above that and it is there..
I also tried changing
<input type="text" value="${subclass.id}"/>
to
<input type="text" value="${data.subclasses[subclass?index].id}"/>
But then it tells me that 'data' is null or missing. What am I doing wrong?
I found the issue after all:
The problem was in the TranferObject. The Sublclass class has private access. so any getters or setters aren't found. That's why the FreeMarker template could not find the .id property.
When I tried to access the getter in normal Java code I got a compilation error:
Error:(65, 77) java: getId() in Data.Subclass is defined in an inaccessible class or interface
Which in my opinion is better than exclaiming it's null or missing.
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
I have the class Lesson, which holds the reference to Course object, like so:
public class Lesson {
...
private Course course;
...
public Course getCourse() {
return course;
}
public void setCourse(Course course) {
this.course = course;
}
...
}
And I want to set the Course property on the Lesson object through the select form:
<form:form method="post" action="addLesson" modelAttribute="lesson">
<form:select path="course">
<form:options items="${courses}"/>
</form:select>
<input type="submit" name="addLesson" value="Add lesson">
</form:form>
In my controller I have the following:
#Controller
public class LessonController {
#Autowired
private LessonRepository lessonRepository;
#Autowired
private CourseRepository courseRepository;
// form setup
#RequestMapping(value = "/", method = RequestMethod.GET)
public String showSchedule(ModelMap model) {
...
model.addAttribute("lesson", new Lesson());
model.addAttribute("courses", courseRepository.findAll());
...
}
#RequestMapping(value = "/addLesson", method = RequestMethod.POST)
public String addLesson(#ModelAttribute("lesson") Lesson lesson, BindingResult result) {
lessonRepository.save(lesson);
return "redirect:/";
}
...
}
The problem is that it passes the String representation of the Course object (defined by toString()) to the course setter of the Lesson object.
How do I properly set the Course property of the Lesson object using my select form?
Usually for UI rendering Formatter<T> is used with ConversionService. But prior to Spring 3, PropertyEditors were used.
I've shared sample github project for your case https://github.com/jama707/SpringSelectBoxSample
#Component("courseFormatter")
public class CourseFormatter implements Formatter<Course> {
private CourseRepository courseRepository=new CourseRepository();
#Override
public String print(Course course, Locale arg1) {
return course.getName();
}
#Override
public Course parse(String actorId, Locale arg1) {
return courseRepository.getCourse(actorId);
}
}
According to spring documentation you need to set itemValue and itemLabelon the form:options tag, otherwise ,as you already mentioned, the value will be the toString() of the Object, itemValue and itemLable should refer to properties from your Course bean.
Assuming that your Course class has a property name, then your form should look like this:
<form:form method="post" action="addLesson" modelAttribute="lesson">
<form:select path="course">
<form:options items="${courses}" itemValue="name" itemLabel="name"/>
</form:select>
<input type="submit" name="addLesson" value="Add lesson">
</form:form>
You can bind the course object directly instead of some course object property by using Spring Converters.
Implement the Converter interface which in your case may convert selected courseName to Course:
public class CourseConverter implements Converter<String, Course> {
public Course convert(String source) {
List<Course> courseList = //Populate courseList the way you did in Lesson
Course course = //Get course object based on selected courseName from courseList;
return course;
}
}
Now register the converter:
<mvc:annotation-driven conversion-service="conversionService"/>
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean" >
<property name="converters">
<set>
<bean class="your.package.CourseConverter"/>
</set>
</property>
</bean>
and change your form:options as:
<form:options items="${courses}" itemValue="courseName" itemLabel="courseName"/>
I am trying to get a list of selected candidates to my controller using #modelAttribute with their respective id and blurb. I am able to bring one Candidate correctly but i don't know how to bring a list of candidates thru... I tried to add List<> as i have shown below, but i get
ERROR -
SEVERE: Servlet.service() for servlet [dispatcher] in context with path [/panel-requests] threw exception [Request processing failed; nested exception is org.springframework.beans.BeanInstantiationException: Could not instantiate bean class [java.util.List]: Specified class is an interface] with root cause
org.springframework.beans.BeanInstantiationException: Could not instantiate bean class [java.util.List]: Specified class is an interface
JSP -
<form:form modelAttribute="candidateAddAttribute"
action="/panel-requests/requests/${panelRequestForId.id}/empl" method="post">
<c:forEach items="${candidates}" var="candidates">
<select name="employee" id="employee" disabled="disabled">
<option value="default" selected="selected">${candidates.candidateName}</option>
</select>
<textarea rows="3" cols="40" id="candidateBlurb" name="candidateBlurb"
disabled="disabled">${candidates.candidateBlurb}</textarea>
<textarea rows="2" cols="20" id="candidateCV" name="candidateCV"
disabled="disabled">${candidates.candidateCV}</textarea>
</c:forEach>
<div id="candidateDiv" id="candidateDiv">
<select name="employee" id="employee">
<option value="default" selected="selected">Select Employee</option>
<c:forEach items="${employees}" var="employee">
<option value="${employee.id}" id="${employee.id}">
${employee.employeeName}- ${employee.employeeCV}<
/option>
</c:forEach>
</select>
<textarea rows="3" cols="40" id="candidateBlurb"
name="candidateBlurb">BLURB</textarea>
<div id="employeeCv"></div>
<input type="submit" value="Add Candidate" />
</div>
</form:form>
The above form at first displays list of employee and when user selects employee, enters blurb and hits add candidate button, i take data to controller.
Controller:
#RequestMapping(value = "{id}/empl", method = RequestMethod.POST)
public String getEmployeeDetails(
#ModelAttribute("candidateAddAttribute") #Valid List<Candidate> candidates,
BindingResult result, #PathVariable("id") int requestId, Model model) {
//implementation goes here
}
How do I implement List in this case? Thanks in advance.
EDITED PART
I tried sending 2 candidate's details, firebug sends it correctly like -
Parameters
candidateBlurb BLURB sar
candidateBlurb BLURB dann
employee 1
employee 2
so it can be a problem in initBinder that i ma using,
binder.registerCustomEditor(Employee.class,
new PropertyEditorSupport() {
public String getAsText() {
return Long.toString(((Employee) getValue()).getId());
}
public void setAsText(final String text) {
Employee employee = Employee.findById(Integer
.parseInt(text));
setValue(employee);
}
});
which only takes 1 employee detail at a time. Is that a problem here???
Create a POJO class which contains the List as a property, such as
public class EmployeeForm {
private List<Candidate> candidates;
public List<Candidate> getCandidates() { ... }
public void setCandidates(List<Candidates>) { ... }
}
and use this in your #RequestMapping method's signature rather than a List<Candidate> directly.
The error message you get is Spring complaining that it doesn't know how to instantiate a List interface, in order for Spring to bind the request parameters in the form to it. You want to provide a simple form/command class to data-bind the form parameters to, Spring knows how to handle a list from there.
you can try this :
List<Candidate> candidates = ListUtils.lazyList(new ArrayList<Candidate>(),FactoryUtils.instantiateFactory(Candidate.class));
I've run into this same issue, half of the fields are binding correctly and half aren't. I noticed that the half that are not binding correctly (thus giving me NULL in the controller) were wrapped in a DIV.
When I moved these fields outside the DIV, bingo, they were bound properly in the controller.
I see that you have some fields wrapped in a DIV too, I'd recommend moving them out and seeing if they become visible to your controller.