Spring web mvc атрибут модели Thymeleaf для редактирования списка в объекте

#java #spring #thymeleaf #modelattribute

#java #spring #thymeleaf #атрибут модели

Вопрос:

У меня есть форма для редактирования пользователя, у пользователя есть роли, которые представляют собой список объектов типа Authority, я хочу иметь возможность использовать флажки (необязательно) для установки ролей, которые будут у пользователя, но я понятия не имею, как реализовать форму в thymeleaf и какпередайте пользовательский объект с заданными ролями контроллеру.

Это мой пользователь

 import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;

import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
public class User implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name="username", nullable = false, unique = true)
    @NotBlank @Size(min=5, message = "Не менeе 5 знаков")
    private String username;

    @NotBlank @Size(min=5, message = "Не менeе 5 знаков")
    @Column(name = "password")
    private String password;

    @Column(name = "enabled")
    private boolean enabled;

    @Column(name = "name")
    private String name;

    @Column(name = "surname")
    private String surname;

    @Column(name = "email", nullable = false, unique = true)
    private String email;

    @ManyToMany(cascade = {
            CascadeType.PERSIST,
            CascadeType.MERGE,
            CascadeType.DETACH,
            CascadeType.REFRESH
    }, fetch = FetchType.EAGER)
    @JoinTable(
            name = "users_authorities",
            joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "authority_id")
    )
    private Set<Authority> authorities = new HashSet<>();

    public User(String username, String password, boolean enabled,
                String name, String surname, String email) {
        this.username = username;
        this.password = password;
        this.enabled = enabled;
        this.name = name;
        this.surname = surname;
        this.email = email;
    }

    public void addAuthority(Authority authority) {
        this.authorities.add(authority);
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

}
 

Это мой редактор пользовательских форм редактирования

 @GetMapping("/users/{id}/edit")
public String editUser(@PathVariable("id") Long id, Model model) {

    model.addAttribute("user", userService.findById(id));
    model.addAttribute("allAuthorities", authorityService.findAll());

    return "users/edit-user";
}
 

Это мое представление формы редактирования пользователя

 <body>
<form th:method="PUT" th:action="@{/admin/users/{id}(id=${user.getId()})}" th:object="${user}">
    <input type="hidden" th:field="*{id}" id="id">

    <label for="username">Username: </label>
    <input type="text" th:field="*{username}" id="username" placeholder="username">
    <br><br>

    <div sec:authorize="hasRole('ROLE_ADMIN')">
        <label for="enabled">Enabled </label>
        <input type="checkbox" name="enabled" th:field="*{enabled}" id="enabled">
        <br><br>
    </div>

    <label for="name">Name: </label>
    <input type="text" th:field="*{name}" id="name" placeholder="Name">
    <br><br>

    <label for="surname">Surname: </label>
    <input type="text" th:field="*{surname}" id="surname" placeholder="Surname">
    <br><br>

    <label for="email">Email: </label>
    <input type="text" th:field="*{email}" id="email" placeholder="Email">
    <br><br>

    <div th:each="auth:${allAuthorities}">
        <label>
            <span th:text="${auth.authority}"></span>
            <input type="checkbox" name="authorities" th:checked="${user.authorities.contains(auth)}">
        </label>
    </div>

    <input type="submit" value="Edit">
</form>
</body>
 

Он помещает contoller, он получает данные из моей формы

 @PutMapping("/users/{id}")
public String editUser(@PathVariable("id") Long id,
                       @ModelAttribute("user") User user,
                       @RequestParam("authorities") List<Authority> authorities) {
    user.setId(id);
    userService.update(user);

    return "redirect:/admin/users";
}
 

И это мой класс полномочий, если вам нужно

 @Entity
@Table(name = "authorities")
@Data
@NoArgsConstructor
public class Authority implements GrantedAuthority {
    @Id
    private Long id;

    @Column(name = "authority")
    private String authority;

    @Transient
    @ManyToMany(mappedBy = "authorities")
    private Set<User> users;

    public Authority(Long id, String authority) {
        this.id = id;
        this.authority = authority;
    }
}
 

Я пытаюсь передать список ролей отдельно от пользовательского объекта, но это также не работает и выдает ошибку неправильного запроса.

Комментарии:

1. Не могли бы вы добавить какой-нибудь код, который вы пробовали, или что-то эквивалентное?

Ответ №1:

Чтобы решить проблему, я добавил новое проверенное поле в Entity authority

 @Entity
@Table(name = "authorities")
@Data
@NoArgsConstructor
public class Authority implements GrantedAuthority {
    @Id
    private Long id;

    @Column(name = "authority", nullable = false, unique = true)
    private String authority;

    @Transient
    private boolean checked;

    public Authority(Long id, String authority) {
        this.id = id;
        this.authority = authority;
    }
}
 

Перед отправкой представления в него я изменил поле authorites пользовательского объекта.

 @GetMapping("/users/{id}/edit")
public String editUser(@PathVariable("id") Long id, Model model) {
    User user = userService.findById(id);
    List<Authority> allAuthorities = authorityService.findAll();

    for(Authority auth : allAuthorities) {
        if(user.getAuthorities().contains(auth)) {
            auth.setChecked(true);
        }
    }

    user.setAuthorities(allAuthorities);

    model.addAttribute("user", user);
    return "users/edit-user";
}
 

Я также изменил шаблон thymeleaf

 <form th:method="PUT" th:action="@{/admin/users/{id}(id=${user.getId()})}" th:object="${user}">

    <label for="username">Username: </label>
    <input type="text" th:field="*{username}" id="username" placeholder="username">
    <br><br>

    <div sec:authorize="hasRole('ROLE_ADMIN')">
        <label for="enabled">Enabled </label>
        <input type="checkbox" name="enabled" th:field="*{enabled}" id="enabled">
        <br><br>
    </div>

    <label for="name">Name: </label>
    <input type="text" th:field="*{name}" id="name" placeholder="Name">
    <br><br>

    <label for="surname">Surname: </label>
    <input type="text" th:field="*{surname}" id="surname" placeholder="Surname">
    <br><br>

    <label for="email">Email: </label>
    <input type="text" th:field="*{email}" id="email" placeholder="Email">
    <br><br>

    <div th:each="auth, itemStat: ${user.authorities}">
        <label>
            <span th:text="${auth.authority}"></span>
            <input type="hidden"
                   th:field="*{authorities[__${itemStat.index}__].id}">
            <input type="hidden"
                   th:field="*{authorities[__${itemStat.index}__].authority}">
            <input type="checkbox" th:checked="${auth.checked}"
                   th:field="*{authorities[__${itemStat.index}__].checked}">
        </label>
    </div>

    <input type="submit" value="Edit">
</form>
 

Мой контроллер put выглядит следующим образом

 @PutMapping("/users/{id}")
public String editUser(@PathVariable("id") Long id,
                       @ModelAttribute("user") User user) {
    List<Authority> userAuthorities = user.getAuthorities();
    userAuthorities.removeIf(auth -> !auth.isChecked());

    user.setId(id);
    userService.update(user);

    return "redirect:/admin/users";
}
 

Мои методы сохранения и удаления из UserService выглядят следующим образом (если это будет полезно)

 @Override
@Transactional
public User save(User user) {
    Optional<User> userFromDB = userRepository.findByUsername(user.getUsername());
    Optional<Authority> userRole;
    
    if(userFromDB.isPresent()) {
        return null;
    }

    userRole = authorityRepository.findByAuthority("ROLE_USER");
    user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));

    userRole.ifPresent(user::addAuthority);

    return userRepository.save(user);
}

@Override
@Transactional
public User update(User user) {
    Optional<User> userFromDB = userRepository.findById(user.getId());

    if(userFromDB.isPresent()){
        //Adding the password for successfully update user in DB
        user.setPassword(userFromDB.get().getPassword());
        return userRepository.save(user);
    }

    return null;
}
 

Возможно, это не самое элегантное решение, но я еще не нашел другого