páginas que listam e cadastram, com validação
Na aula passada, criamos o projeto series com Spring Boot. Ao executar, ele sobe um servidor na porta 8080 e fica no ar, respondendo a cada requisição HTTP que chega.
O código de status é a primeira coisa que o cliente lê na resposta, e o primeiro dígito indica a família:
Cada operação do CRUD virou uma rota no SerieController, e cada rota termina num método do SerieDAO:
Cada pacote do projeto tem um papel:
O SerieController é um @RestController, e por isso o que cada método devolve vira JSON no corpo da resposta.
Na entrada, @RequestBody lê o JSON do corpo e monta uma Serie, chamando o setter de cada campo. No Postman, o corpo ia com o cabeçalho Content-Type: application/json, que avisa ao Spring em que formato os dados estão.
Vamos continuar no mesmo projeto do encontro 9. Ele já tem a entidade Serie, o SerieDAO, o acesso ao mesmo banco e as cinco rotas do CRUD, que recebem e devolvem JSON.
Uma página que lista as séries e um formulário que cadastra uma série nova, conferindo os dados antes de salvar no mesmo banco.
Para cadastrar uma série hoje, alguém precisa abrir o Postman, escolher o verbo POST, digitar o endereço e escrever um JSON sem errar nenhuma aspa.
Ou seja, a aplicação funciona para quem programa, mas não para quem vai usar o sistema no dia a dia, que espera abrir o navegador e preencher campos.
Vimos em Construção de Páginas Web I que o formulário é o elemento do HTML que coleta dados e os envia para um servidor:
Ao clicar em Cadastrar, o navegador monta uma requisição com o verbo do method, o caminho do action e, no corpo, um par nome=valor para cada campo, separados por &.
Esse formato se chama application/x-www-form-urlencoded. Ele carrega as mesmas informações que o JSON da aula passada, mas organiza e identifica o corpo de outra forma.
Para enviar dados a uma rota do servidor, um formulário HTML usa:
O formulário HTML não envia PUT nem DELETE diretamente. Por isso, as páginas de hoje usam GET para mostrar e POST para cadastrar.
Tudo o que colocamos em src/main/resources/static o Spring Boot entrega ao navegador do jeito que está, sem passar por nenhum controller.
Por exemplo: o arquivo static/cadastro.html fica disponível em http://localhost:8080/cadastro.html.
Ele é um arquivo fixo: o servidor não troca nenhuma parte do HTML antes de enviá-lo.
Nesta rota, @RequestBody recebe uma Serie a partir de um corpo JSON. Como o cabeçalho informa que o formulário enviou pares nome=valor, o Spring não encontra uma leitura compatível, não chama o método e responde 415.
Ou seja, o problema está no formato do corpo e na forma como essa rota foi criada para recebê-lo, e não nos valores de Arcane.
Para o formulário funcionar, precisamos de duas coisas que o SerieController não tem:
O SerieController fica como está, porque continua atendendo quem usa a API, e as páginas ganham um controller novo.
Para não misturar as rotas, as páginas ficam sob o caminho /paginas:
avisa ao Spring que esta classe recebe requisições HTTP e responde com páginas.
Uma página que lista as séries do banco não pode ser um HTML fixo na pasta static, porque o conteúdo muda a cada cadastro.
O Thymeleaf é uma biblioteca que gera essa página: escrevemos um HTML comum com alguns atributos th:, e antes de enviar a resposta o Thymeleaf troca esses atributos pelos dados que o controller entregou.
O Thymeleaf entra no projeto como mais uma dependência no pom.xml. Depois de salvar, o Maven baixa a biblioteca e o Spring Boot já a configura sozinho.
A linha spring.thymeleaf.cache=false faz o Thymeleaf reler o template a cada requisição durante o desenvolvimento.
Este método prepara duas coisas diferentes:
"series/lista" é o caminho da página dentro de templates, sem o .html. O Spring vai procurar templates/series/lista.html.
leva dados do controller até a página.
O nome precisa coincidir: "series" no controller e ${series} no template se referem ao mesmo dado.
A página é HTML comum, com dois atributos do Thymeleaf:
Em cada repetição, serie representa um objeto diferente da lista.
Quando o navegador pede GET/paginas/series:
Esta rota só mostra o formulário. Ela entrega à página uma Serie vazia, criada com new, com o nome "serie", que o formulário vai usar como objeto do cadastro.
Ainda não há dado enviado: esta requisição é um GET que prepara a página antes de a pessoa começar a preencher.
Em relação ao cadastro.html, o action passa a apontar para o controller novo, e entram dois atributos do Thymeleaf:
O * em *{nome} quer dizer: o atributo nome do objeto do th:object.
Agora a mesma página envia um POST. O Spring relaciona cada nome do formulário com uma propriedade de Serie, converte os valores e chama os setters.
Depois, o método salva pelo DAO e manda o navegador pedir a lista.
monta um objeto Java com os dados de um formulário.
É a mesma regra do @RequestBody, lendo o formato do formulário em vez de JSON.
Depois de salvar, o método devolve redirect:/paginas/series em vez do nome de uma página. O Spring responde 302 Found, com o endereço no cabeçalho Location, e o navegador faz sozinho um GET nesse endereço, que mostra a lista já com a série nova.
Se o método devolvesse direto "series/lista", a última requisição do navegador continuaria sendo o POST, e cada F5 cadastraria a mesma série de novo.
Ou seja, o cadastro aceita o que o formulário enviar, e a regra que diz o que é um dado válido ainda não está escrita em lugar nenhum.
O HTML tem o atributo required, que impede o envio de um campo vazio. Ele ajuda quem preenche, mas qualquer pessoa pode removê-lo pelas ferramentas do desenvolvedor (F12), ou enviar a requisição sem usar a página.
Logo, a regra que decide se um dado é válido precisa ficar no servidor, que é o único ponto por onde todo cadastro passa.
O Java tem uma especificação para isso, a Bean Validation: cada regra é uma anotação sobre o atributo da classe, e uma biblioteca confere o objeto inteiro de uma vez.
Assim como o JPA, é uma especificação, e a implementação que vem com o Spring Boot é o Hibernate Validator. As anotações ficam no pacote jakarta.validation.
Cada anotação é uma regra, e o texto em message é o que aparece para quem preencheu errado:
Assim, um campo vazio chega como null, e o @NotNull barra o cadastro com a nossa mensagem.
pede ao Spring que confira as regras de validação do objeto recebido.
guarda os erros de conversão e de validação do objeto recebido.
th:errors="*{nome}" mostra a mensagem do erro do atributo nome. Quando o atributo não tem erro, o Thymeleaf remove o span da página.
Como o método devolve o formulário com a mesma Serie que chegou, o th:field preenche de novo cada campo com o que foi digitado, e quem errou corrige só o campo marcado.
A loja de games usados da aula passada quer cadastrar os jogos pelo navegador, sem depender do Postman. No mesmo projeto series, use a entidade Jogo e o JogoDAO da prática anterior.
As páginas de hoje são HTML sem nenhum estilo. Para estilizar, usamos um arquivo CSS, que é um arquivo fixo como o cadastro.html, e por isso fica na pasta static.
A tag link, que já usamos em Construção de Páginas Web, vai no head de cada template que deve receber o estilo:
Ao abrir a página, o navegador faz uma segunda requisição, um GET /css/estilo.css, e aplica o estilo que recebeu.
O mesmo estilo.css serve para a lista e para o formulário, porque as duas páginas têm o mesmo link no head.
A regra form span deixa em vermelho as mensagens do th:errors, que são os span de dentro do formulário.
O extra é opcional: quem terminar a Prática 4 pode ligar o mesmo arquivo às páginas de jogos, ou criar um estilo próprio.

Quer trocar mais uma ideia? Me chama!