Search code examples
javahtmlspringthymeleaf

How to bind fields in a list to a Thymeleaf form? [Bean property is not readable]


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.

What I tried

DTO

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;
    }
}

Controller

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";
    }

Thymeleaf template

<!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>

Expectation

My expectation is that when the user clicks the Align button, the form will post to the endpoint and be persisted to the database.

Error

Invalid property 'userAlignmentSources[0]' of bean class [nathanLively.subAlignerjava.DTO.UserAlignmentDTO]: Illegal attempt to get property 'userAlignmentSources' threw exception

Solution

  • 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.