Thymeleaf checkbox list passing values, but not displaying existing values - spring-boot

I have an edit form for a Project object. Each project has a list of roles associated with them out of the complete list of all available roles. I need a checkbox list to select the roles. I implemented it like in this code sample I found thanks to StackOverflow, using a Formatter: https://github.com/jmiguelsamper/thymeleafexamples-selectmultiple
The issue: The form I created allows me to select the roles and save them successfully. But when I display an existing project object to edit it, the roles already associated with that project are not checked in the list. All the checkboxes are clear.
The code sample above was with a String id. I use a Long id. I think that's the reason for the issue, but I don't know how to solve it. Should I drop the Formatter approach entirely? Is there a way to make this work?
This is my code so far:
Project class:
#Entity
public class Project
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#ManyToMany
private List<Role> rolesNeeded;
public Project()
{
rolesNeeded = new ArrayList<>();
}
//getters and setters omitted for brevity
}
Role class:
#Entity
public class Role
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#NotNull
#Column
private String name;
public Role() {}
//getters and setters omitted for brevity
}
Controller:
#Controller
public class ProjectController
{
#Autowired
private ProjectService projectService;
#Autowired
private RoleService roleService;
#RequestMapping(value = "/projects/save", method = RequestMethod.POST)
public String addProject(#Valid Project project)
{
projectService.save(project);
return "redirect:/";
}
#RequestMapping("/projects/{id}/edit")
public String editForm(#PathVariable Long id, Model model)
{
Project project = projectService.findById(id);
model.addAttribute("project", project);
model.addAttribute("allRoles", roleService.findAll());
return "project/form";
}
}
The RoleFormatter:
#Component
public class RoleFormatter implements Formatter<Role>
{
#Override
public Role parse(String id, Locale locale) throws ParseException
{
Role role = new Role();
role.setId(Long.parseLong(id));
return role;
}
#Override
public String print(Role role, Locale locale)
{
String id = role.getId() + "";
return id;
}
}
And finally the Thymeleaf form:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<body>
<section>
<div class="container wrapper">
<form th:action="#{/projects/save}" method="post" th:object="${project}">
<input type="hidden" th:field="*{id}"/>
<div>
<label for="project_name"> Project Name:</label>
<input type="text" id="project_name" th:field="*{name}"/>
</div>
<div>
<label>Project Roles:</label>
<ul class="checkbox-list">
<li th:each="role : ${allRoles}">
<input type="checkbox" th:id="${{role}}" th:value="${{role}}" th:field="*{rolesNeeded}" />
<span class="primary" th:text="${role.name}"></span>
</li>
</ul>
</div>
<div class="actions">
<button type="submit" value="Save" class="button">Save</button>
<a th:href="#{/}" class="button button-secondary">Cancel</a>
</div>
</form>
</div>
</section>
</body>
</html>
UPDATE
As discussed in the comments: when I do not use the Formatter like above, I get a 400 Bad Request error. This is the header data of the POST request. In this case I tried selecting two roles (id 1 and 3 as you can see below)
Request URL:http://localhost:8080/projects/save
Request Method:POST
Status Code:400 Bad Request
Remote Address:[::1]:8080
Referrer Policy:no-referrer-when-downgrade
Response Headers
Connection:close
Content-Language:en-GB
Content-Length:350
Content-Type:text/html;charset=ISO-8859-1
Date:Tue, 31 Oct 2017 20:10:09 GMT
Server:Apache-Coyote/1.1
Request Headers
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding:gzip, deflate, br Accept-Language:en-GB,en;q=0.9,en-US;q=0.8,fr;q=0.7
Cache-Control:max-age=0
Connection:keep-alive
Content-Length:161
Content-Type:application/x-www-form-urlencoded
Host:localhost:8080
Origin:http://localhost:8080
Referer:http://localhost:8080/projects/add
Upgrade-Insecure-Requests:1
User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64)
AppleWebKit/537.36 (KHTML, like Gecko)
Chrome/62.0.3202.75
Safari/537.36
Form Data
id:
name:Implement recipe site
description:description
status:RUNNING
rolesNeeded:1
_rolesNeeded:on
_rolesNeeded:on
rolesNeeded:3
_rolesNeeded:on
_rolesNeeded:on

Compleately remove formatter as you dont need it in your case and do the checkbox like that
<input type="checkbox" th:value="${role.id}" th:field="*{rolesNeeded}" th:text="${role.name}"/>
this should work. Id from checkbox will be autointerpreted as existing entities id and will be fetched from the database.
Formatters are meant to generate localized presentation of some objects not to be converters between web forms and backing beans. Yes i am aware that some tutorials are teaching pplmto do that but please dont. Maybe someday in the past, in older versions of spring or thymeleaf it was the correct solution,but right now it is more like a hack, not a how-o-do-thigs-right pattern.
PS: this is a part of working application
Controller method declaration:
public String addPlacePost(#Valid final Place place, BindingResult placeValidation, Model model) {
Checkbox markup:
<fieldset th:object="${place}" th:classappend="${#fields.hasErrors('services')} ? 'has-error' : _ ">
<legend>Select services</legend>
<div class="checkbox" th:each="service : ${allServices}">
<label> <input th:value="${service.id}" th:field="*{services}" type="checkbox"/> <span
th:text="${service.name}" th:remove="tag"> </span>
</label>
</div>
<span class="help-block" th:each="msg : ${#fields.errors('services')}" th:text="${msg}">Some error message for this field</span>
</fieldset>
And the Place entity part that contains Services
#ManyToMany
#JoinTable(joinColumns = #JoinColumn(name = "place_id"), inverseJoinColumns = #JoinColumn(name = "service_id"))
#NotEmpty
private Set<Service> services;
Works like charm both for adding new places as well as editing existing ones.

Running ahead - a complete Github example with explanations is available here -> https://stackoverflow.com/a/46926492/6332774
Now comments specific to your case:
If you did not get it resolved please check this response from Thymeleaf team:
http://forum.thymeleaf.org/The-checked-attribute-of-the-checkbox-is-not-set-in-th-each-td3043675.html
In your case your getRolesNeeded() method needs to return an Array where size of the array needs to equal to number of checkboxes.
For this you can use AttributeConverter as documented here https://stackoverflow.com/a/34061723/6332774
Add StringListConverter class (as in link above) and change your model class as:
#Convert(converter = StringListConverter.class)
private List<String> rolesNeeded = new ArrayList<>();
...
public List<String> getRolesNeeded() {
return rolesNeeded;
}
public void setRolesNeeded(List<String> rolesNeeded) {
this.rolesNeeded = rolesNeeded;
}
Then in your html checkbox input remove Id as suggested by #Antoniossss . Change it to something like this:
<div th:each="roles : ${allRoles_CanAddRolesArrayInController}">
<input type="checkbox" th:field="*{rolesNeeded}" th:value="${role.id}"/><label th:text="${role.name}">Role1</label>
</div>
Hope it helps.

Related

Problems with mapping a controller

I am completely stuck on mapping a controller to a URL. I have searched quite a bit, but cannot figure out what I'm doing wrong, except from the fact that the mapping is just not happening. The parts included are currently the only ones not working.
Controller Code (Part of a bigger controller file with another controller method with value "/course_users")
#RequestMapping(value = "/new_users", method = RequestMethod.GET)
public String addStudent(
Model model,
Principal principal,
HttpServletRequest requestID, HttpServletRequest requestName)
{
Long courseId = Long.parseLong(requestID.getParameter("id"));
User currentUser = userService.findByEmail(principal.getName());
Course currentCourse = courseService.findCourseById(courseId);
model.addAttribute("user", currentUser);
model.addAttribute("courseId", courseId);
try{
if(!currentUser.getRoles().contains(Role.ADMIN) && !currentUser.getRoles().contains(Role.LECTURER)){
String errorMsg = "Nie masz wystarczających uprawnień, aby dodać kursanta";
model.addAttribute("errorMsg", errorMsg);
return "error";
}
List<User> users = userService.findAll();
model.addAttribute("users", users);
String userName = requestName.getParameter("userName");
User newStudent = userService.findByEmail(userName);
courseService.addStudentToCourse(currentCourse, newStudent);
}
catch (IllegalArgumentException e){
String errorMsg = "Podano nieprawidłowe argumenty podczas tworzenia kursu.";
model.addAttribute("errorMsg", errorMsg);
return "error";
}
return "course_users";
}
HTML file code (the file is called "course_users.html")
<div class="container pb-3">
<form th:action="#{/new_users}">
<div class="form-group">
<label for="userName">Dodawany Student:</label>
<select class="form-control" id="userName" th:name="userName">
<option th:each="student : ${users}" th:value="${student.getEmail()}" th:text="${student.getEmail()}"></option>
</select>
</div>
<div class="pt-3">
<button type="submit" class="btn btn-primary">Dodaj studenta</button>
</div>
</form>
EDIT
Relevant part of UserService (Course Service is annotated the same way)
#Service
public class UserService {
private final UserRepository userRepository;
#Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
//rest of service code
}
The error is located in the header, and is attempting to call getFirstName() on user, which it doesn't seem to be getting (and why I'm assuming the controller is not getting mapped).
I had an identical problem (and error) with the first method, where I swapped #PathVriable for HttpServletRequest which fixed the problem, sadly no such luck here.
Method call: Attempted to call method getFirstName() on null context object
I feel like this is most likely caused by something very minor that I just keep missing.
EDIT2
I decided to see what happens if I (once again) try
#RequestParam(value = "userName") String userName and #PathVariable("courseId") Long courseId with value = "/new_users/{courseId}"
That went about the same until I swapped
<option th:each="student : ${users}" th:value="${student.getEmail()}" th:text="${student.getEmail()}"></option>
with
<option th:each="student : ${course.getEnrolledStudents()}" th:value="${student.getEmail()}" th:text="${student.getEmail()}"></option>
which showed me an entry in the selection list and gave me an expected error page on clicking the button! (since I was trying to add the same person a second time)
Could be that I'm just mucking something up in HTML syntax, or using/adding the model variable users wrong?
Your controller looks fine can you check you annotated your courseService and userService with #Autowired annotation or not.
#Autowired
private CourseService courseService
Share your error message if it doesn't solve your problem.
I understand that this way you would access the user's values added to model, having always created the corresponding getters and setters of each attribute:
<select class="form-control" id="userName" th:name="userName">
<option th:each="student : ${users}" th:value="${student.email}"
th:text="${student.email}"></option>
</select>
Finally found and answer!
Instead of trying to add a model attribute through model.addAttribute, just add it like this:
#ModelAttribute("users")
public List<User> users() {
return userService.findAll();
}
I still couldn't tell you why the other method didn't work, but at least here's one that works.

Spring - Failed to convert property value of type java.lang.String to required type java.util.Date

I have read some topics about this, but none of them helped me.
My problem is that the conversion is working well, but today I received a user's complaint using ios 12 and Safari. Immediately I tested on Iphone with ios 12 and I couldn't reproduce the error.
Could it be some device specific configuration or regional settings?
Please help!
Thanks.
Controller
#Controller
public class SaleController {
#InitBinder
public void setPropertyBinder(WebDataBinder dataBinder) {
//The date format to parse or output your dates
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
//Create a new CustomDateEditor
CustomDateEditor editor = new CustomDateEditor(dateFormat, true);
//Register it as custom editor for the Date type
dataBinder.registerCustomEditor(Date.class, editor);
}
#RequestMapping(value="/venda3", method=RequestMethod.POST)
public String venda3(#Valid #ModelAttribute("anuncioImovel") AnuncioImovel anuncioImovel, BindingResult resultImovel, Model model, Principal principal, HttpServletRequest request) {
}
}
Model
#Entity
public class AnuncioImovel {
#Column(nullable=true)
#Temporal(TemporalType.DATE)
#DateTimeFormat(pattern="yyyy-MM-dd")
#Past
private Date inicioLocacao;
#Column(nullable=true)
#Temporal(TemporalType.DATE)
#DateTimeFormat(pattern="yyyy-MM-dd")
#Future
private Date terminoLocacao;
public void setInicioLocacao(Date inicioLocacao) {
this.inicioLocacao = inicioLocacao;
}
public void setTerminoLocacao(Date terminoLocacao) {
this.terminoLocacao = terminoLocacao;
}
}
Thymeleaf Template
<!-- Início Locação -->
<div class="form-group">
<label for="frmInicioLocacao" class="form-control-label text-primary"><strong>Início Locação</strong></label>
<input type="date" class="form-control" id="frmInicioLocacao" name="inicioLocacao"
th:value="${anuncioImovel.inicioLocacao} ? ${#dates.format(anuncioImovel.inicioLocacao, 'yyyy-MM-dd')}"
th:classappend="${#fields.hasErrors('anuncioImovel.inicioLocacao')} ? 'is-invalid' : ''"
oninput="javascript: this.value = this.value.slice(0, 10);"/>
<div class="invalid-feedback">
<span th:errors="*{anuncioImovel.inicioLocacao}"></span>
</div>
</div>
<!-- Término Locação -->
<div class="form-group">
<label for="frmTerminoLocacao" class="form-control-label text-primary"><strong>Término Locação</strong></label>
<input type="date" class="form-control" id="frmTerminoLocacao" name="terminoLocacao"
th:value="${anuncioImovel.terminoLocacao} ? ${#dates.format(anuncioImovel.terminoLocacao, 'yyyy-MM-dd')}"
th:classappend="${#fields.hasErrors('anuncioImovel.terminoLocacao')} ? 'is-invalid' : ''"
oninput="javascript: this.value = this.value.slice(0, 10);"/>
<div class="invalid-feedback">
<span th:errors="*{anuncioImovel.terminoLocacao}"></span>
</div>
</div>
Print error sent by the user
click here
My test
In my test on Iphone the dates are shown this way, and the conversion works (I don't know how the date is shown on user's device):
Click here
Your error states that it can't parse the date format dd/MM/yyyy.
In your code, you are using the format yyyy-MM-dd.
Are you sure your are sending the right date format?

How to transfer the list of strings in DTO using Thymeleaf

I'm developing Spring + Thymeleaf application. I'm implementing search with multiple params. I have a form with the corresponding DTO. Here is the code of the DTO:
public class ClassSearchDto {
private String searchParam;
private Long programId;
private List<String> teacherNames;
//getters, setters and constructor are omitted
}
As you see, I have a list of strings in my DTO called teacherNames. Here is the way I'm displaying my form:
<form th:action="#{/classes/search}" method="get" th:object="${classSearchDto}">
<div class="form-group">
<input type="hidden" class="form-control"
th:value="${classSearchDto.programId}" th:field="*{programId}"/>
<label for="searchParam">Search</label>
<input type="text" class="form-control" id="searchParam" placeholder="keyword"
th:value="${classSearchDto.searchParam}" th:field="*{searchParam}"/>
<div>
<th:block th:each="name, iter ${classSearchDto.teacherNames}">
<input th:value="${name}" th:field="*{teacherNames[__${iter.index}__]}/>
</th:block>
</div>
</div>
<button class="btn btn-default" type="submit">Find</button>
</form>
I want to implement my search with help of #RequestParam annotation on the back-end. This is my controller:
#RequestMapping(value = "/search")
public String findClassByName(#RequestParam("searchParam") final String searchParam,
#RequestParam("programId") final Long programId,
#RequestParam("teacherNames") final List<String> teacherNames,
final Model model) {
...
}
The problem is that I can't get the list of teacher names in this way. I get this exception:
org.springframework.web.bind.MissingServletRequestParameterException:Required List parameter 'teacherNames' is not present
Could you please help me to transfer the list of elements in DTO to my back-end with this approach? Maybe you know how to do it correctly in another way. Thank you in advance.
I can suggest you one thing, I don't know whether it works or not. Try changing
public String findClassByName(#RequestParam("searchParam") final String searchParam,#RequestParam("programId") final Long programId,#RequestParam("teacherNames") final List<String> teacherNames,final Model model)
to
public String findClassByName(#ModelAttribute("classSearchDto") ClassSearchDto classSearchDto,#RequestParam("searchParam") String searchParam,#RequestParam("programId") Long programId,#RequestParam("teacherNames") List<String> teacherNames,Model model)

How to proceed with complex object in POST request

Hello SO I had 2 entities
Main entity
#Entity
#Table(name = "Events")
public class Event {
//Some fields ...
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(name = "event_eventtypes" ,
joinColumns = #JoinColumn(name = "event_id"),
inverseJoinColumns = #JoinColumn(name = "event_type_id"))
private Set<EventType> eventTypes;
//Getters and setters...
Now I have a form that is created using spring form-taglib
<form:form modelAttribute="event" method="post" action="/event/registerEvent" commandName="event">
<form:label path="name">Event display name</form:label>
<form:input path="name" type="text" cssClass="form-control" placeholder="Display name"/>
<form:label path="description" >Description</form:label>
<form:textarea path="description" cssClass="form-control"/>
<form:label path="priv" cssClass="">Make private? <span style="font-family:'Open Sans', sans-serif; font-size:11px; color: dodgerblue;">(It will be seen by your friends and people who you send invitation.)</span></form:label>
<form:checkbox path="priv" cssClass="form-check-input"/>
<form:label path="age">Age limit</form:label>
<form:select path="age">
<form:options items="${age}"/>
</form:select>
<form:hidden path="lng" id="formLang" />
<form:hidden path="lat" id="formLat"/>
<%--Question appear here--%>
<form:select path="eventTypes" items="${eventTypes}" multiple="true"/>
<input type="submit" value="Submit">
Controller
#Controller
public class EventController {
private static final Logger logger = LoggerFactory.getLogger(EventController.class);
#Autowired
private EventService eventService;
#Autowired
private UserService userService;
#Autowired
private EventTypeService eventTypeService;
#RequestMapping(path = "/event/create", method = RequestMethod.GET)
public String init(ModelMap modelMap) {
List<String> tags = eventTypeService.listEventTypes().stream().map(EventType::getName).collect(Collectors.toList());
ArrayList<Integer> ageArr = new ArrayList();
ageArr.add(0);
ageArr.add(6);
ageArr.add(12);
ageArr.add(16);
ageArr.add(18);
modelMap.addAttribute("event", new Event());
modelMap.addAttribute("age", ageArr);
modelMap.addAttribute("eventTypes", tags);
return "/event/create";
}
#RequestMapping(path = "/event/registerEvent", method = RequestMethod.POST)
public String createEvent(#ModelAttribute("event") Event event, #ModelAttribute("eventTypes1") List<String> eventTypes){
event.setDate(new Date());
event.setUser(userService.getUserByUsername(
AuthenticationService.getLoggedInUser())
);
eventService.addEvent(event);
return "redirect:/";
}
When complete form with values, click submit, get error
400 The request sent by the client was syntactically incorrect.
It's because eventTypes property is of String type. How can I send that list as another parameter in controller or what should I do it?
Maybe you need a DTO to include event and eventType list both.
When I have done this I send a DTO with a list of IDs/Strings for event types to the form and then create a new event from the DTO.
For the select statements loop round each option returned in the DTO and use a service findById to retrieve each eventType and then add this to the event model.
for (String tag : eventDTO.getEventTags()) {
EventType eventTag = eventTypeService.findById(Long.valueOf(tag));
if (eventTag != null) {
event.getEventTypes().add(eventTag );
}
}
Found soulution.
Just implement jsp-based form as :
<form:form modelAttribute="event" method="post" action="/event/registerEvent" commandName="event">
...
<%--There I'm not using more jsp tabs.--%>
<select multiple name="et" id="tags">
<c:forEach items="${eventTypes}" var="e">
<option value=${e}>${e}</option>
</c:forEach>
</select>
<input type="submit" value="Submit">
</form:form>
In Controller I get parameters as request.getParameterValues("et")) - that return String[] from HttpServletRequest

Annotated Spring MVC #ModelAttribute automapping not working with associated objects

I am using Spring MVC with Annotations. Here's a quick outline of my problem.
My Domain:
public class Restaurant {
private String name;
private Address address = new Address();
//Get and set....
}
public class Address{
private String street;
//Get and set....
}
My Controller:
//Configure and show restaurant form.
public ModelAndView showAction() {
ModelAndView mav = new ModelAndView("/restaurant/showRestaurant");
restaurant = new Restaurant();
mav.addObject("restaurant", restaurant);
return mav;
}
//Save restaurant
public ModelAndView saveAction(#ModelAttribute(value="restaurant") Restaurant restaurant,BindingResult result) {
restaurant.getName();//<- Not is null
restaurant.getAddress().getStreet(); //<- is null
}
My View:
<form>
<span class="full addr1">
<label for="Nome">Name<span class="req">*</span></label>
<h:inputText class="field text large" value="#{restaurant.name}"
id="name" forceId="true" styleClass="field text addr"/>
</span>
<span class="full addr1">
<label for="Nome">Street <span class="req">*</span></label>
<h:inputText class="field text large" value="#{restaurant.address.street}"
id="street" forceId="true" styleClass="field text addr"/>
</span>
</form>
My problem is, when I fill the name and the street to call the method "saveAction" when I try to get the restaurant filled happens that the name comes from the street but did not.
I'm not all that familliar with jsf, but for binding in spring you generally need the full path, i.e. name="address.street", in order to get the street name bound properly
Try binding using the spring form tags http://static.springsource.org/spring/docs/2.0.x/reference/spring-form.tld.html. Its pretty easy.

Resources