Как мне правильно передать коллекцию из моей формы в обработчик событий JS, а затем в контроллер REST?

Я пытаюсь переписать свои формы таким образом, чтобы не обновлять страницу. Другими словами, я не хочу, чтобы браузер делал какие-либо запросы GET/POST при отправке. jQuery должен помочь мне в этом. Вот моя форма (у меня их несколько):

    <!-- I guess this action doesn't make much sense anymore -->
    <form action = "/save-user" th:object = "${user}" method = "post">
         <input type = "hidden" name = "id" th:value = "${user.id}">
    
         <input type = "hidden" name = "username"
              th:value = "${user.username}">
    
         <input type = "hidden" name = "password"
              th:value = "${user.password}">
    
         <input type = "hidden" name = "name" th:value = "${user.name}">
    
         <input type = "hidden" name = "lastName"
              th:value = "${user.lastName}">
    
         <div class = "form-group">
              <label for = "departments">Department: </label>
              <select id = "departments" class = "form-control"
                       name = "department">
                  <option th:selected = "${user.department == 'accounting'}"
                          th:value = "accounting">Accounting
                  </option>
                  <option th:selected = "${user.department == 'sales'}"
                          th:value = "sales">Sales
                  </option>
                  <option th:selected = "${user.department == 'information technology'}"
                          th:value = "'information technology'">IT
                  </option>
                  <option th:selected = "${user.department == 'human resources'}"
                          th:value = "'human resources'">HR
                  </option>
                  <option th:selected = "${user.department == 'board of directors'}"
                          th:value = "'board of directors'">Board
                  </option>
              </select>
          </div>
    
          <div class = "form-group">
              <label for = "salary">Salary: </label>
              <input id = "salary" class = "form-control" name = "salary"
                     th:value = "${user.salary}"
                     min = "100000" aria-describedby = "au-salary-help-block"
                     required/>
              <small id = "au-salary-help-block"
                     class = "form-text text-muted">100,000+
              </small>
          </div>
    
          <input type = "hidden" name = "age" th:value = "${user.age}">
    
          <input type = "hidden" name = "email" th:value = "${user.email}">
    
          <input type = "hidden" name = "enabledByte"
                 th:value = "${user.enabledByte}">
          
          <!-- I guess I should JSON it somehow instead of turning into regular strings -->
          <input type = "hidden" th:name = "authorities"
                 th:value = "${#strings.toString(user.authorities)}"/>
    
          <input class = "btn btn-primary d-flex ml-auto" type = "submit"
                 value = "Submit">
      </form>

Вот мой JS:

$(document).ready(function () {
    $('form').on('submit', async function (event) {
        event.preventDefault();

        let user = {
            id: $('input[name=id]').val(),
            username: $('input[name=username]').val(),
            password: $('input[name=password]').val(),
            name: $('input[name=name]').val(),
            lastName: $('input[name=lastName]').val(),
            department: $('input[name=department]').val(),
            salary: $('input[name=salary]').val(),
            age: $('input[name=age]').val(),
            email: $('input[name=email]').val(),
            enabledByte: $('input[name=enabledByte]').val(),
            authorities: $('input[name=authorities]').val()

            /*
            ↑ i tried replacing it with authorities: JSON.stringify($('input[name=authorities]').val()), same result
            */
        };

        await fetch(`/users`, {
            method: 'PUT',
            headers: {
                ...getCsrfHeaders(),
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(user) // tried body : user too
        });

    });
});

function getCsrfHeaders() {
    let csrfToken = $('meta[name = "_csrf"]').attr('content');
    let csrfHeaderName = $('meta[name = "_csrf_header"]').attr('content');

    let headers = {};
    headers[csrfHeaderName] = csrfToken;
    return headers;
}

Вот мой обработчик контроллера REST:

    // maybe I'll make it void. i'm not sure i actually want it to return anything
    @PutMapping("/users")
    public User updateEmployee(@RequestBody User user) {
        service.save(user); // it's JPARepository's regular save()
        return user;
    }

Сущность User:

@Entity
@Table(name = "users")
@Data
@EqualsAndHashCode
public class User implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    private long id;
    @Column(nullable = false, unique = true)
    private String username;
    @Column(nullable = false)
    private String password;
    @Column
    private String name;
    @Column(name = "last_name")
    private String lastName;
    @Column
    private String department;
    @Column
    private int salary;
    @Column
    private byte age;
    @Column
    private String email;
    @Column(name = "enabled")
    private byte enabledByte;
    @ManyToMany
    @JoinTable(name = "user_role",
            joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id"),
                    @JoinColumn(name = "username", referencedColumnName = "username")},
            inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id"),
                    @JoinColumn(name = "role", referencedColumnName = "role")})
    @EqualsAndHashCode.Exclude
    private Set<Role> authorities;

Сущность Role:

@Entity
@Table(name = "roles")
@Data
@EqualsAndHashCode
public class Role implements GrantedAuthority {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    private long id;
    @Column(name = "role", nullable = false, unique = true)
    private String authority;
    @ManyToMany(mappedBy = "authorities")
    @EqualsAndHashCode.Exclude
    private Set<User> userList;

Когда я нажимаю кнопку отправки, я получаю это в своей консоли

WARN 18252 --- [io-8080-exec-10] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `java.util.HashSet<pp.spring_bootstrap.models.Role>` from String value (token `JsonToken.VALUE_STRING`)]

Кажется, я должен каким-то образом передать JSON-представление этого Collection, а не просто String. В моем предыдущем проекте без использования jQuery String была успешно десериализована с помощью моего пользовательского Formatter

@Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addFormatter(new Formatter<Set<Role>>() {
            @Override
            public Set<Role> parse(String text, Locale locale) {
                Set<Role> roleSet = new HashSet<>();
                String[] roles = text.split("^\\[|]$|(?<=]),\\s?");
                for (String roleString : roles) {
                    if (roleString.length() == 0) continue;
                    String authority =
                            roleString.substring(roleString.lastIndexOf(" = ") + 2,
                                    roleString.indexOf("]") - 1);
                    roleSet.add(service.getRoleByName(authority));
                }
                return roleSet;
            }

            @Override
            public String print(Set<Role> object, Locale locale) {
                return null;
            }
        });
    }

Я погуглил, и оказалось, что у Thymeleaf нет никакого метода toJson(). Я имею в виду, что могу написать свои собственные методы, но я все равно не знаю, как их использовать в шаблонах Thymeleaf. К тому же это может быть не самое оптимальное решение

Это загрузочный проект, поэтому у меня есть библиотека привязки данных Джексона.

Как мне правильно передать коллекцию из моей формы в обработчик событий JS, а затем в контроллер REST?

Я изучил несколько похожих вопросов, предложенных StackOverflow. Они не кажутся релевантными (например, они связаны с разными языками программирования, такими как C# или PHP).

UPD: только что попробовал. Жаль, что это тоже не сработало! (сообщение об ошибке такое же)

// inside my config
    @Bean
    public Function<Set<Role>, String> jsonify() {
        return s -> {
            StringJoiner sj = new StringJoiner(", ", "{", "}");
            for (Role role : s) {
                sj.add(String.format("{ \"id\" : %d, \"authority\" : \"%s\" }", role.getId(), role.getAuthority()));
            }
            return sj.toString();
        };
    }
    <input type = "hidden" th:name = "authorities" 
           th:value = "${@jsonify.apply(user.authorities)}"/>

Однако метод работает так, как ожидалось.

$(document).ready(function () {
    $('form').on('submit', async function (event) {
        /*
        ↓ logs:
          authorities input: {{ "id" : 1, "authority" : "USER" }}
        */
        console.info('authorities input: ' + 
                   $('input[name=authorities]').val());

UPD2: GPT4 предложил это

authorities: JSON.parse($('input[name=authorities]').val())

а сейчас вообще странно. База данных осталась неизменной, НО! консоль IDE теперь не имеет ошибок и вообще не упоминает запрос PUT (он был там при предыдущих попытках)! Кроме того, в журнале браузера есть это сообщение

Uncaught (in promise) SyntaxError: Expected property name or '}' in JSON at position 1
    at JSON.parse (<anonymous>)
    at HTMLFormElement.<anonymous> (script.js:28:31)
    at HTMLFormElement.dispatch (jquery.slim.min.js:2:43114)
    at v.handle (jquery.slim.min.js:2:41098)

Я не знаю, что это значит!

UPD3: GPT4 шустрый. Во всяком случае, умнее меня. Это было абсолютно правильно. Причина, по которой это не сработало в UPD2, заключалась в том, что я проигнорировал еще одну вещь, которую он сказал:

Поле полномочий следует отправлять в виде массива объектов, а не строки.

Это означает, что я должен был использовать квадратные, а не фигурные скобки в качестве префикса и суффикса StringJoiner:

    // I also added some line breaks, but I doubt it was necessary
    @Bean
    public Function<Set<Role>, String> jsonify() {
        return s -> {
            StringJoiner sj = new StringJoiner(",\n", "[\n", "\n]");
            for (Role role : s) {
                sj.add(String.format("{\n\"id\" : %d,\n\"authority\" : \"%s\"\n}", role.getId(), role.getAuthority()));
            }
            return sj.toString();
        };
    }

Я также изменил, например, это

username: $('input[name=username]').val()

к этому (глупо было с моей стороны не сделать это сразу)

username: $(this).find('input[name=username]').val()

и – альт – теперь это работает!

И GPT4 также заметил, что я использовал

'input[name=department]'

вместо

'select[name=department]'

я тоже это исправил

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
0
52
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий
  1. Это должен быть массив объектов (даже если это Collection, а не массив), поэтому

new StringJoiner(", ", "{", "}")new StringJoiner(", ", "[", "]")

  1. Он должен быть нацелен на дочерние элементы формы, поэтому

username: $('input[name=username]').val()username: $(this).find('input[name=username]').val() или еще лучше username: $(this).find('[name=username]').val() и так далее

  1. department представлен элементом <select>, поэтому

'input[name=department]''select[name=department]' или '[name=department]'

Другие вопросы по теме