I have an object that can either have two or three repeating sets of fields. It seems like the easiest way to handle that is with a list and a form that will expand or contract with the size of the list.
Unfortunately, I keep getting an error suggesting that Thymeleaf doesn't recognize the variables I'm trying to bind to the main form object. This problem is solved in this article, but for some reason I am not able to get the same results.
package nathanLively.subAlignerjava.DTO;
import lombok.Builder;
import lombok.Data;
import nathanLively.subAlignerjava.Models.Enums.UnitsEnum;
import nathanLively.subAlignerjava.Models.PreAlignment;
import nathanLively.subAlignerjava.Models.Speaker;
import nathanLively.subAlignerjava.Models.UserAlignment;
import nathanLively.subAlignerjava.Models.UserAlignmentSource;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Data
@Builder
public class UserAlignmentDTO {
private UnitsEnum units;
private List<UserAlignmentSourceDTO2> userAlignmentSources;
public UserAlignment toUserAlignment(PreAlignment preAlignment) {
Set<UserAlignmentSource> userAlignmentSourceList = userAlignmentSources.stream()
.map(UserAlignmentSourceDTO2::toUserAlignmentSource)
.collect(Collectors.toSet());
UserAlignment userAlignment = UserAlignment.builder()
.preAlignment(preAlignment)
.userAlignmentSources(userAlignmentSourceList)
.distanceUnit(units)
.build();
return userAlignment;
}
}
package nathanLively.subAlignerjava.DTO;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import nathanLively.subAlignerjava.Models.Enums.UnitsEnum;
import nathanLively.subAlignerjava.Models.PreAlignment;
import nathanLively.subAlignerjava.Models.PreAlignmentSource;
import nathanLively.subAlignerjava.Models.Speaker;
import nathanLively.subAlignerjava.Models.UserAlignmentSource;
@Data
@Builder
public class UserAlignmentSourceDTO2 {
private String modelName;
private Integer count;
private Float distance;
public UserAlignmentSource toUserAlignmentSource() {
UserAlignmentSource userAlignmentSource = UserAlignmentSource.builder()
// .speaker(speaker)
.count(count)
.distance(distance)
.build();
return userAlignmentSource;
}
}
package nathanLively.subAlignerjava.Controllers;
import lombok.RequiredArgsConstructor;
import nathanLively.subAlignerjava.DTO.UserAlignmentDTO;
import nathanLively.subAlignerjava.DTO.UserAlignmentSourceDTO;
import nathanLively.subAlignerjava.DTO.UserAlignmentSourceDTO2;
import nathanLively.subAlignerjava.Models.*;
import nathanLively.subAlignerjava.Models.Enums.UnitsEnum;
import nathanLively.subAlignerjava.Services.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@Controller
@RequiredArgsConstructor
@RequestMapping("/align")
public class AlignController {
@Autowired
private final BrandService brandService;
@Autowired
private final SpeakerModelService speakerModelService;
@Autowired
private final DspPresetService dspPresetService;
@Autowired
private final UserAlignmentService userAlignmentService;
@Autowired
private final SpeakerService speakerService;
@Autowired
private final PreAlignmentService preAlignmentService;
// Show pre-alignments
@GetMapping("/pre-alignments")
public String showPreAlignments(Model model) {
// Show pre-alignments in model
model.addAttribute("preAlignments", preAlignmentService.findAll());
return "pre-alignments";
}
// Add pre-alignment to user alignment
@GetMapping("/pre-alignment/{id}")
public String showUserAlignment(@PathVariable("id") Long preAlignmentId, Model model) {
// Find pre-alignment and add to model
PreAlignment preAlignment = preAlignmentService.findById(preAlignmentId);
model.addAttribute("preAlignment", preAlignment);
// Initialize UserAlignmentSourceDTO2
int numberOfPreAlignmentSources = preAlignment.getPreAlignmentSources().size();
List<UserAlignmentSourceDTO2> userAlignmentSourceDTO2s = new ArrayList<>();
for (int i = 0; i < numberOfPreAlignmentSources; i++) {
UserAlignmentSourceDTO2 userAlignmentSourceDTO2 = UserAlignmentSourceDTO2.builder()
.modelName(preAlignment.getPreAlignmentSources().get(i).getSpeaker().getSpeakerModel().getName())
.build();
userAlignmentSourceDTO2s.add(userAlignmentSourceDTO2);
}
// Initialize UserAlignmentDTO and add to model
UserAlignmentDTO userAlignmentDTO = UserAlignmentDTO.builder()
.userAlignmentSources(userAlignmentSourceDTO2s)
.build();
model.addAttribute("userAlignmentDTO", userAlignmentDTO);
// Show distance units in model
model.addAttribute("units", List.of(UnitsEnum.METERS, UnitsEnum.FEET));
return "user-alignment";
}
// Create UserAlignment and calculate result
@PostMapping("/calculate/{id}")
public String calculateUserAlignment(@PathVariable("id") Long preAlignmentId, @ModelAttribute("userAlignmentDTO") UserAlignmentDTO userAlignmentDTO, Model model) {
// Find pre-alignment
PreAlignment preAlignment = preAlignmentService.findById(preAlignmentId);
// preAlignment.getPreAlignmentSources().get(0)
// Convert UserAlignmentDTO to UserAlignment
UserAlignment userAlignment = userAlignmentDTO.toUserAlignment(preAlignment);
// Calculate result
// userAlignment.calculateResult();
// Save user alignment
userAlignmentService.save(userAlignment);
// Show result
model.addAttribute("userAlignment", userAlignment);
return "result";
}
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/layout}"
th:with="activeMenuItem='align'"
xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<title>User Alignment</title>
</head>
<body>
<div layout:fragment="page-content">
<h1>User-Alignment</h1>
<form id="source-form"
th:object="${userAlignmentDTO}"
th:action="@{/align/calculate/{id}(id=${preAlignment.id})}"
method="post">
<th:block th:each="source,iter : *{userAlignmentSources}">
<label th:text="${iter.index + 1} + '.'"></label>
<label>Total [[${source.modelName}]] in array:</label>
<input type="number"
th:field="*{userAlignmentSources[__${iter.index}__].count}">
<label>Distance from [[${source.modelName}]] to FOH:</label>
<input type="number"
th:field="*{userAlignmentSources[__${iter.index}__].distance}">
</th:block>
<label>Unit</label>
<select th:field="*{units}">
<option th:each="unit : ${units}"
th:text="${unit.fullName}"
th:value="${unit}">meters
</option>
</select>
<button type="submit">Align</button>
</form>
</div>
</body>
</html>
My expectation is that when the user clicks the Align button, the form will post to the endpoint and be persisted to the database.
Invalid property 'userAlignmentSources[0]' of bean class [nathanLively.subAlignerjava.DTO.UserAlignmentDTO]: Illegal attempt to get property 'userAlignmentSources' threw exception
Looks like I was missing the proper NoArgs constructor for Thymeleaf.
@NoArgsConstructor
@AllArgsConstructor
From chatGPT:
Based on the error message, it looks like there is no default constructor for the UserAlignmentSourceDTO2 class. That means that Thymeleaf is unable to create an instance of this class when it tries to bind form data to a UserAlignmentDTO object.