Dificuldade em redirecionar usuário usando Spring Boot, Thymeleaf, JQuery

Olá!

Estou desenvolvendo um sistema usando Spring Boot, Spring Data, Thymeleaf, Bootstrap, HTML5, JQuery e estou finalizando uma tela de “envio de lotes de boletos”, que contém uma tabela “dinâmica” com os boletos solicitados pelo usuário em um momento anterior.

A primeira coluna da tabela é um checkbox que o usuário vai usar pra selecionar os boletos que vão fazer parte do lote.

<table id="boletossolicitados" class="table table-hover">
            <thead>
                <tr class="table-active">
                    <th><input id="checktodos" type="checkbox" title="Selecionar todos boletos"></th>
                    <th>Exercício</th>
                    <th>Nosso número</th>
                    <th>Código do contribuinte</th>
                    <th>Razão social</th>
                    <th>CPF/CNPJ</th>
                    <th>Valor principal</th>
                    <th>Valor artigo 600 da CLT</th>
                    <th>Valor Lei 8022/1990</th>
                    <th>Data do pagamento</th>
                    <th>Valor do pagamento</th>
                    <th>Forma de cálculo</th>
                    <th>CPF/CNPJ fonte pagadora</th>
                </tr>
            </thead>
            <tbody>
                <tr th:each="boleto : ${boletosolicitados}" class="table-light">
                    <td>
                        <div class="form-check">
                            <input type="checkbox" class="form-check-input" th:id="${boleto.id}">
                        </div>
                    </td>
                    <td th:text="${boleto.exercicio}"></td>
                    <td th:text="${boleto.nossoNumeroAtual}"></td>
                    <td th:text="${boleto.contribuinte.codigo}"></td>
                    <td th:text="${boleto.contribuinte.razaoSocial}"></td>
                    <td th:text="${boleto.contribuinte.numeroDoDocumentoFormatado}"></td>
                    <td th:text="${boleto.valorPrincipalDoBoleto}"></td>
                    <td th:text="${boleto.valorEstimado600CLT}"></td>
                    <td th:text="${boleto.valorEstimado8022}"></td>
                    <td th:text="${boleto.dataPagamentoCR}"></td>
                    <td th:text="${boleto.valorPagamentoCR}"></td>
                    <td th:text="${boleto.formaDeCalculoUtilizada.label}"></td>
                    <td th:text="${boleto.cpfcnpjFontePagtoCR}"></td>
                </tr>
            </tbody>
        </table>

        <a id="enviarlotebtn" class="btn btn-primary btn-lg" title="Enviar lote para a FAESP com os boletos selecionados acima" href="#">Enviar lote</a>

Quando o usuário clica no botão “Enviar lote”, eu verifico(via JavaScript) a existência de pelo menos um boleto selecionado, caso contrário, é exibida uma mensagem informando da necessidade de selecionar boletos para o envio.

Se estiver tudo ok, um modal é exibido para confirmar o envio do lote:

<!-- MODAL CONFIRMAÇÃO ENVIO LOTE -->
    <div class="confirmacaoenviolote">
        <div class="modal fade" id="confirmacaoenvioloteModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalCenterTitle" aria-hidden="true">
          <div class="modal-dialog modal-dialog-centered" role="document">
            <div class="modal-content">
              <div class="modal-header">
                <h5 class="modal-title" id="exampleModalCenterTitle">ATENÇÃO !</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                  <span aria-hidden="true">&times;</span>
                </button>
              </div>
              <div class="modal-body">
                <h5>Confirma o envio do lote de boleto(s) ?</h5>
              </div>
              <div class="modal-footer">
                <a class="btn btn-primary" id="confirmarEnvioBtn" th:href="@{enviarlote}">Confirmar</a>
                <button type="button" class="btn btn-secondary" data-dismiss="modal">Não</button>
              </div>
            </div>
          </div>
        </div>
    </div>

Se estiver tudo ok, o código javascript abaixo é executado para obter os id´s dos boletos selecionados (note que atribui o id do boleto ao id do checkbox usando th:id="${boleto.id}" do thymeleaf):

    $('.confirmacaoenviolote #confirmarEnvioBtn').on('click', function(event){
    event.preventDefault();
    var idsvar = [];

    $("#boletosselecionados tr").each(function(){
        if($(this).find("input[type='checkbox']").is(":checked")){
            var id = $(this).find("input[type='checkbox']").attr('id');
            if(id != "checktodos"){
                idsvar.push(id);
            }
        }
    });

    var data = {ids: idsvar};
    var url = $(this).attr('href');

    $.post(url, data);
});

O médoto “$.post(url, data);” executa o seguinte método no meu Spring Boot Controller:

@PostMapping(value = "/enviarlote")
public ModelAndView enviarLoteBoleto(@RequestParam("ids[]") String[] ids) {
    HashMap<String, Object> retorno = this.loteBoletoCobrancaService.verificarBoletosLimitaMaximoUltrapassado(ids);
    if(retorno!=null) {
        String view = (String) retorno.get("redirecionarpagina");
        ModelAndView mv = new ModelAndView(view);
        mv.addObject("boletosselecionados", retorno.get("boletosselecionados"));
        mv.addObject("boletoslimiteultrapassado", retorno.get("boletoslimiteultrapassado"));    
        mv.addObject("impossibilidadecriacaolote", retorno.get("impossibilidadecriacaolote"));
        mv.addObject("boletosdivergentes", retorno.get("boletosdivergentes"));
        mv.addObject("haBoletosLimiteMinimoUltrapassado", retorno.get("haBoletosLimiteMinimoUltrapassado"));

        return mv;
    }

    return new ModelAndView("el/envioDeLotes");
} 

O método “verificarBoletosLimitaMaximoUltrapassado()” na minha @Service “loteBoletoCobrancaService” verifica se os boletos selecionados para envio estão dentro de um prazo mínimo de pagamento (defini o mínimo de 10 dias para o pagamento do boleto contando a partir da data de envio do lote), se houver boletos com o prazo ultrapassado, o usuário deverá ser redirecionado para outra página que contém uma tabela com os boletos de prazo ultrapassado, e decidirá se continuará com o envio ou voltar e selecionar novos boletos.

Se não houver boletos com prazo ultrapassado, o método “verificarBoletosLimitaMaximoUltrapassado()” chamará outro método dentro da minha @Service que verificará se há boletos com valores divergentes (valor de pagamento informado pelo usuário abaixo do valor calculado do boleto), e caso houver, o usuário deverá ser redirecionado também para outra página que contém também uma tabela com os boletos com valores divergentes. E decidirá se continua com o processo de envio ou volta e seleciona novos boletos.

Se estiver tudo ok após todas as verificações na minha @Service, a página “envio de lotes de boletos” deverá ser atualizada, uma mensagem de sucesso será exibida e a tabela de boletos solicitados não conterá mais os boletos enviados no lote.

Observação: O método “verificarBoletosLimitaMaximoUltrapassado()” retorna um HashMap contendo os objetos e uma String “redirecionarpagina” que informará a página que o usuário deverá ser redirecionado.

A lógica toda está pronta, a minha dificuldade está em redirecionar o usuário de acordo com a view que eu setar no meu ModelAndView do método no @Controller

Não sei se o método $.post é a melhor opção pois quando todo o código(já testei depurando, o parâmetro “ids” chega com os id´s corretamente e o código é executado sem erros) no meu @Controller é executado, não acontece nada.

Creio que o ajax não é a melhor alternativa neste caso, pois o usuário será redirecionado para outra página, o processo de envio do lote vai funcionar como um Wizard…

Alguém teria uma solução para o meu caso? Obrigado…

Se voce esta usando ajax, o metodo do controller nao vai redirecionar. Esse travamento deve ser no javascript definindo um metodo para success e para error.

No controller o metodo precisa retornar uma ResponseEntity com os parametros que voce quer que o js trate.

public @ResponseBody ResponseEntity<?> salvar(@RequestBody Object obj) {}

Estou usando ajax porque foi a forma que encontrei pra passar como parâmetro os id´s para meu Controller, e preciso que o método no Controller redirecione o usuário por que faço uso dos objetos atribuídos ao ModelAndView. No meu caso, talvez seja melhor passar os parâmetros para meu Controller de outra forma ao invés de usar ajax…

Consigo atribuir os objetos do ModelAndView ao ResponseEntity e acessa-los na outra página redirecionada? E como faria para redirecionar o usuário para uma outra página usando ajax??

Voce pode fazer isso usando os proprios recursos do thymeleaf. Basta voce ter um objeto DTO que tenha apenas dois campos. O id do boleto e um boleano para saber quem foi selecionado… ai no seu service, voce percorre esse objeto para ver quem esta selecionado. E faz a sua logica.

public class BoletoSelecionadoDTO {
     private Integer id;
     private Boolean selecionado;

     //gets and sets
}

O seu metodo no controller ira receber um array desses objetos

@PostMapping(value = "/enviarlote")
public ModelAndView enviarLoteBoleto(List<BoletoSelecionadoDTO> boletosSelecionados) {
} 

o segredo esta na montagem da sua tabela, voce vai fazer assim:

<tr th:each="boleto, iterStat : ${boletosolicitados}" class="table-light">
    <td>
        <div class="form-check">
            <input type="checkbox" class="form-check-input"                                          
                   th:name="|boletosSelecionados[${iterStat.index}].selecionado|"                                         
                   th:id="|boletosSelecionados[${iterStat.index}].selecionado|"
                   th:value=${false}>
        </div>
    </td>
    <input type="hidden" 
           th:id="|boletosSelecionados[${iterStat.index}].id|" 
           th:name="|boletosSelecionados[${iterStat.index}].id|" 
           th:value="${boleto.id}" />
</tr>

entao voce vai ter em cada linha da tabela uma referencia ao id e ao boleto que foi selecionado.
quando voce submeter o formulario, ira receber esse array com os ids dos boletos e o atributo que esta selecionado ou nao. o segreto é pegar o iterStat do thymeleaf e fazer esse esquema com o th:name e o th:id para o spring conseguir montar o objeto no controller.

Exatamente isso que eu fiz. Mas ao invés de criar um DTO com id e um boleano, eu criei com a lista de boletos. Eu alimento essa lista antes de exibir a página e listo no form.

Meu DTO:

private List<BoletoCobranca> boletos;

public BoletoCobrancaDTO(List<BoletoCobranca> boletos) {
	this.boletos = boletos;
}

public List<BoletoCobranca> getBoletos() {
	return boletos;
}

public void setBoletos(List<BoletoCobranca> boletos) {
	this.boletos = boletos;
}

Meu form:

<form id="envioLoteId" th:action="@{/enviarlote}" th:object="${boletosSelecionados}" method="post">
<table>
	<tr th:each="boleto, itemStat : *{boletos}" class="table-light">
	    <input type="hidden" th:name="|boletos[${itemStat.index}].id|" th:value="${boleto.id}">
	    <td>
	       <div class="form-check">
		       <input type="checkbox" class="form-check-input" th:field="*{boletos[__${itemStat.index}__].boletoSelecionadoEnvioLote}">
		   </div>
	    </td>
    </tr>
</table> 
</form>

Quando o usuário clica no checkbok eu seto um atributo (boletoSelecionadoEnvioLote) boleano na entity boleto.

Controller:

@PostMapping(value = "/enviarlote")
public ModelAndView enviarLoteBoleto(@ModelAttribute("boletosSelecionados") BoletoCobrancaDTO boletosSelecionados){
}

Tentei adaptar sua alternativa mas está dando esse erro:

2020-06-09 17:13:52.022 ERROR 70892 --- [nio-8080-exec-5] o.s.b.w.servlet.support.ErrorPageFilter  : Forwarding to error page from request [/enviarlote] due to exception [No primary or default constructor found for interface java.util.List]

java.lang.IllegalStateException: No primary or default constructor found for interface java.util.List
	at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.createAttribute(ModelAttributeMethodProcessor.java:219) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor.createAttribute(ServletModelAttributeMethodProcessor.java:85) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument(ModelAttributeMethodProcessor.java:139) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:167) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:134) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:879) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:793) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:661) ~[servlet-api.jar:na]
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:742) ~[servlet-api.jar:na]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:230) [catalina.jar:9.0.0.M19]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165) [catalina.jar:9.0.0.M19]
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-websocket.jar:9.0.0.M19]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:192) [catalina.jar:9.0.0.M19]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165) [catalina.jar:9.0.0.M19]
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:192) [catalina.jar:9.0.0.M19]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165) [catalina.jar:9.0.0.M19]
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:192) [catalina.jar:9.0.0.M19]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165) [catalina.jar:9.0.0.M19]
	at org.springframework.boot.web.servlet.support.ErrorPageFilter.doFilter(ErrorPageFilter.java:128) [spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
	at org.springframework.boot.web.servlet.support.ErrorPageFilter.access$000(ErrorPageFilter.java:66) [spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
	at org.springframework.boot.web.servlet.support.ErrorPageFilter$1.doFilterInternal(ErrorPageFilter.java:103) [spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.boot.web.servlet.support.ErrorPageFilter.doFilter(ErrorPageFilter.java:121) [spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:192) [catalina.jar:9.0.0.M19]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165) [catalina.jar:9.0.0.M19]
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) [spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:192) [catalina.jar:9.0.0.M19]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165) [catalina.jar:9.0.0.M19]
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:199) [catalina.jar:9.0.0.M19]
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) [catalina.jar:9.0.0.M19]
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:475) [catalina.jar:9.0.0.M19]
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140) [catalina.jar:9.0.0.M19]
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:80) [catalina.jar:9.0.0.M19]
	at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:624) [catalina.jar:9.0.0.M19]
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87) [catalina.jar:9.0.0.M19]
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:341) [catalina.jar:9.0.0.M19]
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:498) [tomcat-coyote.jar:9.0.0.M19]
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) [tomcat-coyote.jar:9.0.0.M19]
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:796) [tomcat-coyote.jar:9.0.0.M19]
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1368) [tomcat-coyote.jar:9.0.0.M19]
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-coyote.jar:9.0.0.M19]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) [na:1.8.0_181]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) [na:1.8.0_181]
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-util.jar:9.0.0.M19]
	at java.lang.Thread.run(Unknown Source) [na:1.8.0_181]
Caused by: java.lang.NoSuchMethodException: java.util.List.<init>()
	at java.lang.Class.getConstructor0(Unknown Source) ~[na:1.8.0_181]
	at java.lang.Class.getDeclaredConstructor(Unknown Source) ~[na:1.8.0_181]
	at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.createAttribute(ModelAttributeMethodProcessor.java:216) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	... 57 common frames omitted