Solucionado (ver solução)
Solucionado
(ver solução)
5
respostas

Pegar array de select multiple no controller spring

Bom dia,

Preciso resgatar o array de um select multiple e trazer para o meu controller.

Na minha thymeleaf possui este select :

<div class="input-field">
                        <select  id="select-permissoes" multiple="multiple" name="permissoes">
                           <option th:each="role : ${roles}"
                                th:value="${role.nome}"
                                th:text="${role.descricao}"></option>
                        </select>
                        <label>Permissões</label>
                    </div>

Eu tenho um objeto consultor que tem uma lista de roles dentro dele, ai preciso resgatar esse multiple select e jogar dentro do meu consultor para persistir as permissões, poderiam me ajudar?

5 respostas

Fala Renan, tudo bem ?

O problema aí é o que Spring não tem ideia de como mapear cada String que vai a partir do select multiple sob o name permissoes quando do submit do formulário.

Pra resolver essa questão é um pouquinho mais difícil. Precisamos passar para o Spring um PropertyEditor para nossa propriedade permissoes para que ele consiga entender que cada String recebida com essa chave deve virar um elemento de Permissoes dentro de uma lista.

Pra nossa sorte o Spring já tem implementações de PropertyEditor mais específicas para elementos de coleções =). Podemos usar, por exemplo, um CustomCollectionEditor.

Vamos ao exemplo:

Controller:

@Controller
@RequestMapping("/consultor")
public class ConsultorController {

    @GetMapping("/form")
    public ModelAndView form(Consultor consultor) {

        List<Role> roles = Arrays.asList(new Role(1, "ROLE_1"), 
                new Role(2, "ROLE_2"), 
                new Role(3, "ROLE-3"));

        ModelAndView modelAndView = new ModelAndView("index");
        modelAndView.addObject("roles", roles);
        return modelAndView;
    }

    @ResponseBody
    @PostMapping
    public String adiciona(Consultor consultor) {

        List<Role> roles = consultor.getRoles();
        roles.stream().forEach(role -> System.out.println(role));
        return "ok";
    }

Aqui temos um controller simples pra atender as requisicoes para o form e o submit do form.

Para esse exemplo temos as seguintes classes de modelo...

Consultor:

public class Consultor {

    public Integer id;
    public List<Role> roles = new ArrayList<>();

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public List<Role> getRoles() {
        return roles;
    }

    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }

}

Role:

public class Role {

    private Integer id;
    private String nome;

    public Role() {    
    }

    public Role(int id, String nome) {
        this.id = id;
        this.nome = nome;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getNome() {
        return nome;
    }

    public void setNome(String nome) {
        this.nome = nome;
    }

    @Override
    public String toString() {
        return "Role [id=" + id + ", nome=" + nome + "]";
    }
}

View

...
<form:form action="${url }" method="post" commandName="consultor">
    <form:select multiple="true" path="roles" items="${roles}" itemLabel="nome" itemValue="id" />
    <input type="submit" />
</form:form>
...

(Aqui estou usando JSP + taglib form do Spring Mvc, mas pouco importa)

html resultante:

<form action="/contexto/consultor" method="post">
    <select id="roles" name="roles" multiple="multiple">
        <option value="1">ROLE_1</option>
        <option value="2">ROLE_2</option>
        <option value="3">ROLE-3</option>
    </select>

    <input type="hidden" name="_roles" value="1">
    <input type="submit">
</form>

Com o código atual temos problemas as enviar (400). Mas se cadastrarmos para esse controller um PropertyEditor adequado o Spring consegue montar a lista com as roles.

Controller:

@Controller
@RequestMapping("/consultor")
public class ConsultorController {

    @GetMapping("/form")
    public ModelAndView form(Consultor consultor) {
        ...
    }

    @ResponseBody
    @PostMapping
    public String adiciona(Consultor consultor) {
        ...
    }

    @InitBinder
    protected void initBinder(WebDataBinder binder) throws Exception {

        CustomCollectionEditor rolesCollector = new CustomCollectionEditor(List.class) {
            @Override
            protected Object convertElement(Object element) {
                if (element instanceof String) {
                    Integer id = Integer.parseInt((String) element);

                    Role role = new Role();
                    role.setId(id);
                    return role;
                }
                throw new RuntimeException("Spring says: Não sei o que fazer com esse elemento: " + element);
            }
        };

        binder.registerCustomEditor(List.class, "roles", rolesCollector);
    }

Agora o spring já tem o custom editor registrado para a propriedade recebida do form, e já sabe o que fazer com seus multiplos valores.

Teste: form

Saída do console do método adiciona(..): saida

Espero ter ajudado. Abraço!

@PostMapping(path = "/add")
    @Transactional
    public String create(@Valid Consultor consultor,
            @RequestParam("permissoes") List<String> permissoes,
            ModelMap model) {        

        if (consultorServiceImpl.findByUsername(consultor.getUsername()) != null) {
            model.addAttribute("errorMessage", "Ops, este usuário já consta em nosso sistema");
            model.addAttribute("consultores", consultorServiceImpl.getAll());
            return "consultor/lista";
        }

        List<Role> roles = new ArrayList<Role>();
        for (String p : permissoes) {
            Role role = new Role();
            role.setNome(p);
            roles.add(role);
            role = null;
        }

        consultor.setRoles(roles);

        consultorServiceImpl.enviarEmailBoasVindas(consultor);

        consultor.criptarSenha();

        consultorServiceImpl.create(consultor);

        return "redirect:/consultores/form";

    }

Consegui resgatar com o @RequestParam, ai fiz um loop e depois adicionei no consultor.

Tive que mudar o name no Thymeleaf para o spring não tentar injetar no objeto.

Está funcionando,sabe me dizer se terei algum problema futuro se eu utilizar assim ?

solução!

Fala Renan ..

Eu pessoalmente faria da forma como vimos a primeira vista. Com a solução apresentada você dissocia a lista de permissões do Consultor, sobrando um monte de código não usual pro seu controller (pegar a lista separadamente, fazer loop terminar de montar o objeto .. esse é o tipo de código que queremos justamente evitar ao usar um framework como o Spring). Uma outra coisa é que se você precisar de algum tipo de validação, o que é recomendado, sobre a seleção das permissões, ela também está dissociada do @Valid Consultor consultor que você recebe no seu método, o que pode fazer sobrar ainda mais código para sua ação no controller pra garantir as devidas checagens.

Uma coisa que você poderia pensar se quiser seguir por esse caminho é incrementar sua solução isolando tanto o Consultor, quanto a List de permissões, em alguma outra classe que represente os dados enviados pelo formulário. Mas pessoalmente ainda configuraria o PropertyEditor do Spring. =)

Exemplo:

public class ConsultorForm {

    private String nome;
    ...
    private List<Permissao> permissoes;

    // getters and setters

    public Consutor build() {
        //termina de montar o consultor extraindo aquele codigo do controller
        // devolve o objeto pronto pra usar no controller
    }
}

Essa técnica pode ajudar a concentrar a responsabilidade de binding de toda a informação que vem da página, facilitando a vida do controller que volta a ter um código mais simples. Pode também ter validators específicos ou usar a Bean Validation, etc. Sempre é bom pensar nos pontos positivos e negativos que o código pode causar nas suas futuras manutenções. E lembre-se, os frameworks estão aí pra nos ajudar a não reescrever esses códigos indesejados de montagem de objetos, conversoes, validacoes, etc. Mesmo que as vezes precisemos nos aproximar mais de suas configurações internas é sempre melhor escrever um código usando a API do framework que já está testada e validada, do que comprometer nossos códigos tentando algum work around.

Espero ter ajudado. Abraço!

Rafael Rollo,

Muito obrigado amigo , funcionou bem com a sobrescrita da CustomCollectionEditor.

Fala Renan,

Que bom que funcionou =)

Abraço!