Lição 1: unidade federativa
Nesta lição, você vai gerar o projeto e implementar, camada por camada, o agregado unidade federativa do subdomínio Geografia. Ao final, o projeto corresponde ao diretório exemplo1 do repositório de D5.
Antes de começar, confira os pré-requisitos do tutorial.
Projeto Maven
Em vez de criar o projeto manualmente, use o gerador, que produz a estrutura de diretórios, os arquivos pom.xml e as classes de infraestrutura que não dependem do domínio, seguindo as convenções. Informe:
- groupId:
com.example; - Nome do projeto:
exemplo; - Descrição:
Projeto de exemplo do tutorial de D5; - Versão: o valor sugerido,
1.0.0-SNAPSHOT; - Subdomínios: informe o primeiro e clique em + para acrescentar os campos do segundo. Cada subdomínio tem nome, tipo e descrição:
- nome
geografia, tipo Subdomínio de suporte e descriçãoUnidades federativas e municípios do Brasil; - nome
crm, tipo Subdomínio principal e descriçãoRelacionamento com os clientes;
- nome
- Usar o framework D5: marcado;
- Incluir AGENTS.md e skills: desmarcado, porque este tutorial não utiliza agentes de IA;
- Frontend: Angular.
Extraia o arquivo exemplo.zip obtido. O projeto gerado tem os módulos:
exemplo-dominio-compartilhado, do qual os módulos correspondentes a subdomínios dependem e que vai conter os conceitos que não pertencem a nenhum subdomínio específico e são comuns a todos eles, como os papéis de autorização; veja Dependências entre módulos de domínio;exemplo-dominio-geografia, correspondente ao subdomínio Geografia;exemplo-dominio-crm, correspondente ao subdomínio CRM, que permanece vazio até a lição 3, quando o cliente é criado;exemplo-aplicacao, com os serviços de aplicação, projeções e consultas de todos os subdomínios;exemplo-infraestrutura, com as implementações JPA dos repositórios dos agregados de todos os subdomínios;exemplo-ui-backend, com os controladores REST que expõem os agregados de todos os subdomínios e omainda aplicação Spring Boot;exemplo-ui-frontend, com a aplicação Angular que oferece a interface com o usuário. Este tutorial trata apenas do backend: não altere esse módulo.
Os módulos correspondem às camadas da arquitetura limpa: domínio, aplicação, infraestrutura e UI. Diferente do domínio, que é decomposto em um módulo por subdomínio porque cada um tem suas próprias regras de negócio, a aplicação, a infraestrutura e o backend são camadas técnicas transversais que consultam, persistem e expõem os agregados de todos os subdomínios da mesma forma — por isso cada uma é um único módulo.
Diretórios
Cada módulo Java segue a estrutura padrão do Maven, com src/main/java, src/main/resources, src/test/java e src/test/resources. Já o módulo ui-frontend contém a aplicação Angular em src/main/angular:
exemplo
├── pom.xml
├── dominio-compartilhado
├── dominio-geografia
├── dominio-crm
├── aplicacao
├── infraestrutura
├── ui-backend
└── ui-frontend
Arquivos pom.xml
O pom.xml do projeto raiz herda de d5-pai — que define Java 25 e Spring Boot 4.1 e gerencia as versões dos módulos do framework, declarados, por isso, sem <version> — e declara os sete módulos.
O d5-pai é publicado no repositório de snapshots do Maven Central, e não no repositório principal, porque a versão atual, 1.0.0-SNAPSHOT, ainda não é uma versão final. Por isso, o pom.xml do projeto raiz declara esse repositório em <repositories>; os módulos o herdam.
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>dev.sauloaraujo</groupId>
<artifactId>d5-pai</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<groupId>com.example</groupId>
<artifactId>exemplo-pai</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<repositories>
<repository>
<id>central-snapshots</id>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<modules>
<module>dominio-compartilhado</module>
<module>dominio-crm</module>
<module>dominio-geografia</module>
<module>aplicacao</module>
<module>infraestrutura</module>
<module>ui-backend</module>
<module>ui-frontend</module>
</modules>
</project>
Cada módulo herda de exemplo-pai. O compartilhado depende de d5-framework-dominio; o de subdomínio depende apenas do compartilhado, através do qual recebe d5-framework-dominio. A aplicação depende de d5-framework-aplicacao e dos módulos de subdomínio cujos serviços de aplicação implementa. A infraestrutura depende de d5-framework-infraestrutura e do módulo de aplicação cujos repositórios de aplicação implementa — os módulos de subdomínio chegam transitivamente por ele. O backend depende de d5-framework-ui-backend, do módulo de aplicação cujos controladores expõem, do domínio compartilhado — cujos papéis de autorização a configuração de segurança usa — e, apenas em tempo de execução, do módulo de infraestrutura — que fornece as implementações concretas dos repositórios — e de d5-framework-springdoc, que gera a documentação OpenAPI da API REST. Essas dependências seguem a regra da dependência da arquitetura limpa: nenhum módulo de domínio ou de aplicação depende da infraestrutura ou da UI. Por fim, o frontend tem o packaging pom e não depende de nenhum outro módulo — o Maven não o compila, e a aplicação Angular é construída com as ferramentas do próprio Angular.
Cada módulo depende também, em escopo de teste, do módulo de teste do framework correspondente à sua camada. Além disso, o backend declara o spring-boot-maven-plugin, que empacota a aplicação em um jar executável e oferece o objetivo spring-boot:run, usado mais adiante, e, como dependência opcional, o Spring Boot DevTools, que reinicia a aplicação automaticamente quando o código é recompilado durante o desenvolvimento.
O diagrama a seguir ilustra essas dependências, tanto entre os módulos do exemplo quanto com os módulos do framework, omitindo o frontend, que não tem nenhuma; uma seta de A para B indica que A depende de B, e uma seta tracejada indica uma dependência restrita ao tempo de execução. Por enquanto, exemplo-dominio-crm depende apenas do domínio compartilhado; a dependência dele em exemplo-dominio-geografia só é acrescentada na lição 3:
graph TD
subgraph Framework
FD[d5-framework-dominio]
FA[d5-framework-aplicacao]
FI[d5-framework-infraestrutura]
FU[d5-framework-ui-backend]
FS[d5-framework-springdoc]
end
subgraph exemplo
DC[exemplo-dominio-compartilhado]
DG[exemplo-dominio-geografia]
DR[exemplo-dominio-crm]
AP[exemplo-aplicacao]
IN[exemplo-infraestrutura]
UI[exemplo-ui-backend]
end
DC --> FD
DG --> DC
DR --> DC
AP --> FA
AP --> DG
AP --> DR
IN --> FI
IN --> AP
UI --> FU
UI --> AP
UI --> DC
UI -.-> FS
UI -.-> IN
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>exemplo-pai</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>exemplo-dominio-compartilhado</artifactId>
<dependencies>
<dependency>
<groupId>dev.sauloaraujo</groupId>
<artifactId>d5-framework-dominio</artifactId>
</dependency>
<dependency>
<groupId>dev.sauloaraujo</groupId>
<artifactId>d5-framework-dominio-teste</artifactId>
<type>pom</type>
<scope>test</scope>
</dependency>
</dependencies>
</project>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>exemplo-pai</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>exemplo-dominio-geografia</artifactId>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>exemplo-dominio-compartilhado</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>dev.sauloaraujo</groupId>
<artifactId>d5-framework-dominio-teste</artifactId>
<type>pom</type>
<scope>test</scope>
</dependency>
</dependencies>
</project>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>exemplo-pai</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>exemplo-dominio-crm</artifactId>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>exemplo-dominio-compartilhado</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>dev.sauloaraujo</groupId>
<artifactId>d5-framework-dominio-teste</artifactId>
<type>pom</type>
<scope>test</scope>
</dependency>
</dependencies>
</project>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>exemplo-pai</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>exemplo-aplicacao</artifactId>
<dependencies>
<dependency>
<groupId>dev.sauloaraujo</groupId>
<artifactId>d5-framework-aplicacao</artifactId>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>exemplo-dominio-crm</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>exemplo-dominio-geografia</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>dev.sauloaraujo</groupId>
<artifactId>d5-framework-aplicacao-teste</artifactId>
<type>pom</type>
<scope>test</scope>
</dependency>
</dependencies>
</project>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>exemplo-pai</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>exemplo-infraestrutura</artifactId>
<dependencies>
<dependency>
<groupId>dev.sauloaraujo</groupId>
<artifactId>d5-framework-infraestrutura</artifactId>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>exemplo-aplicacao</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>dev.sauloaraujo</groupId>
<artifactId>d5-framework-infraestrutura-teste</artifactId>
<type>pom</type>
<scope>test</scope>
</dependency>
</dependencies>
</project>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>exemplo-pai</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>exemplo-ui-backend</artifactId>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>dev.sauloaraujo</groupId>
<artifactId>d5-framework-ui-backend</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>exemplo-aplicacao</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>exemplo-dominio-compartilhado</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>dev.sauloaraujo</groupId>
<artifactId>d5-framework-ui-backend-teste</artifactId>
<type>pom</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>dev.sauloaraujo</groupId>
<artifactId>d5-framework-springdoc</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>exemplo-infraestrutura</artifactId>
<version>${project.version}</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>exemplo-pai</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>exemplo-ui-frontend</artifactId>
<packaging>pom</packaging>
</project>
Unidade federativa
Uma unidade federativa é identificada por um código IBGE. Além disso, possui sigla e nome únicos. A sigla e o código IBGE são objetos de valor associados ao agregado unidade federativa. Seguindo as convenções, o pacote dos três é com.example.exemplo.dominio.geografia.unidadefederativa. Crie o package-info.java desse pacote, anotado com @NullMarked:
@org.jspecify.annotations.NullMarked
package com.example.exemplo.dominio.geografia.unidadefederativa;
Domínio
Sigla
A sigla é formada por duas letras maiúsculas. Portanto, seu formato pode ser definido através de uma expressão regular. Objetos de valor desse tipo herdam de ObjetoValorRegexAbstrato.
package com.example.exemplo.dominio.geografia.unidadefederativa;
import dev.sauloaraujo.d5.framework.dominio.impl.ObjetoValorRegexAbstrato;
public final class UnidadeFederativaSigla extends ObjetoValorRegexAbstrato {
public static final int TAMANHO = 2;
private static final String FORMATO = "[A-Z]{" + TAMANHO + "}";
private static final String MENSAGEM = "A sigla da unidade federativa deve conter " + TAMANHO
+ " letras maiúsculas";
public UnidadeFederativaSigla(String valor) {
super(valor, FORMATO, MENSAGEM);
}
}
Código IBGE
O código IBGE é formado por dois dígitos e, portanto, também herda de ObjetoValorRegexAbstrato. Adicionalmente, como o código IBGE identifica uma unidade federativa, sua classe implementa IIdentificador.
package com.example.exemplo.dominio.geografia.unidadefederativa;
import dev.sauloaraujo.d5.framework.dominio.IIdentificador;
import dev.sauloaraujo.d5.framework.dominio.impl.ObjetoValorRegexAbstrato;
public final class UnidadeFederativaCodigoIbge extends ObjetoValorRegexAbstrato implements IIdentificador {
public static final int TAMANHO = 2;
private static final String FORMATO = "\\d{" + TAMANHO + "}";
private static final String MENSAGEM = "O código IBGE da unidade federativa deve conter " + TAMANHO + " dígitos";
public UnidadeFederativaCodigoIbge(String valor) {
super(valor, FORMATO, MENSAGEM);
}
}
Agregado
Unidade federativa, código IBGE e sigla formam um agregado, com UnidadeFederativa como raiz — por isso ela implementa IAgregado, parametrizada pelo tipo do identificador. Seu construtor valida os argumentos, rejeitando um nome em branco. Como as demais classes do domínio, ela é final.
package com.example.exemplo.dominio.geografia.unidadefederativa;
import java.util.Objects;
import java.util.Optional;
import org.apache.commons.lang3.Validate;
import dev.sauloaraujo.d5.framework.dominio.IAgregado;
public final class UnidadeFederativa implements IAgregado<UnidadeFederativaCodigoIbge> {
private final UnidadeFederativaCodigoIbge identificador;
private final UnidadeFederativaSigla sigla;
private final String nome;
public UnidadeFederativa(UnidadeFederativaCodigoIbge identificador, UnidadeFederativaSigla sigla, String nome) {
this.identificador = Objects.requireNonNull(identificador);
this.sigla = Objects.requireNonNull(sigla);
this.nome = Validate.notBlank(nome, "Nome da unidade federativa em branco");
}
@Override
public Optional<UnidadeFederativaCodigoIbge> getIdentificador() {
return Optional.of(identificador);
}
public UnidadeFederativaSigla getSigla() {
return sigla;
}
public String getNome() {
return nome;
}
}
Repositório
Serviços acessam unidades federativas persistidas através de um repositório. Seguindo as convenções, a interface do repositório fica no pacote com.example.exemplo.dominio.geografia.unidadefederativa.spi — o domínio declara a interface, e a infraestrutura a implementa; veja Inversão de dependência e pacotes spi — e estende IAgregadoRepositorio. Adicionalmente, expõe existePorSigla e existePorNome, que o serviço usa para garantir que a sigla e o nome de uma unidade federativa sejam únicos. Ambos recebem o identificador da própria unidade federativa a ignorar na verificação, para que alterá-la não a faça conflitar consigo mesma. Crie o package-info.java desse pacote, anotado com @NullMarked:
@org.jspecify.annotations.NullMarked
package com.example.exemplo.dominio.geografia.unidadefederativa.spi;
package com.example.exemplo.dominio.geografia.unidadefederativa.spi;
import java.util.Optional;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativa;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaSigla;
import dev.sauloaraujo.d5.framework.dominio.spi.IAgregadoRepositorio;
public interface IUnidadeFederativaRepositorio
extends IAgregadoRepositorio<UnidadeFederativaCodigoIbge, UnidadeFederativa> {
boolean existePorSigla(UnidadeFederativaSigla sigla, Optional<UnidadeFederativaCodigoIbge> identificadorIgnorado);
boolean existePorNome(String nome, Optional<UnidadeFederativaCodigoIbge> identificadorIgnorado);
}
Autorização
As regras de autorização sobre um agregado usam papéis compartilhados por todos os subdomínios. Seguindo as convenções, o gerador já criou, no módulo exemplo-dominio-compartilhado, pacote com.example.exemplo.dominio, um enum que os declara, e o package-info.java desse pacote, anotado com @NullMarked:
@org.jspecify.annotations.NullMarked
package com.example.exemplo.dominio;
package com.example.exemplo.dominio;
public enum Autorizacao {
ADMINISTRADOR, USUARIO
}
Serviço
Operações sobre unidades federativas são encapsuladas em um serviço que estende AgregadoServicoAbstrato. Ele implementa criarAutorizacoes consultando o ISegurancaServico recebido no construtor para verificar se o usuário possui a autorização ADMINISTRADOR, devolvendo um AgregadoAutorizacoes que libera a busca a qualquer usuário e as demais operações somente a administradores. Também sobrescreve validarInclusaoAlteracao para usar existePorSigla e existePorNome, ignorando o identificador da própria unidade federativa, e impedir que uma inclusão ou alteração deixe duas unidades federativas com a mesma sigla ou o mesmo nome.
Note que o serviço não leva nenhuma anotação do Spring: ele, os repositórios e o serviço de aplicação são registrados como beans automaticamente.
package com.example.exemplo.dominio.geografia.unidadefederativa;
import java.util.Objects;
import java.util.Optional;
import com.example.exemplo.dominio.Autorizacao;
import com.example.exemplo.dominio.geografia.unidadefederativa.spi.IUnidadeFederativaRepositorio;
import dev.sauloaraujo.d5.framework.dominio.impl.AgregadoAutorizacoes;
import dev.sauloaraujo.d5.framework.dominio.impl.AgregadoServicoAbstrato;
import dev.sauloaraujo.d5.framework.dominio.spi.ISegurancaServico;
public final class UnidadeFederativaServico extends
AgregadoServicoAbstrato<UnidadeFederativaCodigoIbge, UnidadeFederativa, AgregadoAutorizacoes<UnidadeFederativa>> {
public static final String IDENTIFICADOR_INEXISTENTE_MODELO = "Não existe unidade federativa com o identificador %s";
public static final String IDENTIFICADOR_EXISTENTE_MODELO = "Já existe uma unidade federativa com o identificador %s";
public static final String SIGLA_EXISTENTE_MODELO = "Já existe uma unidade federativa com a sigla %s";
public static final String NOME_EXISTENTE_MODELO = "Já existe uma unidade federativa com o nome %s";
private final IUnidadeFederativaRepositorio repositorio;
public UnidadeFederativaServico(ISegurancaServico segurancaServico, IUnidadeFederativaRepositorio repositorio) {
super(segurancaServico, repositorio, IDENTIFICADOR_EXISTENTE_MODELO, IDENTIFICADOR_INEXISTENTE_MODELO);
this.repositorio = Objects.requireNonNull(repositorio);
}
@Override
public AgregadoAutorizacoes<UnidadeFederativa> criarAutorizacoes(
Optional<UnidadeFederativaCodigoIbge> identificador) {
Objects.requireNonNull(identificador);
var administrador = getSegurancaServico().possuiAutorizacao(Autorizacao.ADMINISTRADOR.name());
return new AgregadoAutorizacoes<>(administrador, true, administrador, administrador);
}
@Override
protected void validarInclusaoAlteracao(UnidadeFederativa agregado) {
Objects.requireNonNull(agregado);
var identificador = agregado.getIdentificador();
validarInexistencia(sigla -> repositorio.existePorSigla(sigla, identificador), agregado.getSigla(),
SIGLA_EXISTENTE_MODELO);
validarInexistencia(nome -> repositorio.existePorNome(nome, identificador), agregado.getNome(),
NOME_EXISTENTE_MODELO);
}
}
Testes
O comportamento do serviço é especificado com cenários em Gherkin e automatizado com Cucumber. O gerador já criou a suíte que conecta o Cucumber ao JUnit Platform e seleciona os cenários em src/test/resources — todo módulo de subdomínio tem a sua:
package com.example.exemplo.dominio.geografia;
import org.junit.platform.suite.api.ConfigurationParameter;
import org.junit.platform.suite.api.IncludeEngines;
import org.junit.platform.suite.api.SelectDirectories;
import org.junit.platform.suite.api.Suite;
import io.cucumber.junit.platform.engine.Constants;
@Suite(failIfNoTests = false)
@IncludeEngines("cucumber")
@ConfigurationParameter(key = Constants.GLUE_PROPERTY_NAME, value = "com.example.exemplo.dominio.geografia")
@SelectDirectories("src/test/resources")
public class CucumberTests {
}
Escreva os cenários cobrindo inclusão, busca, alteração e exclusão de uma unidade federativa, incluindo as regras de autorização e de unicidade de sigla e nome. O comentário # language: pt, na primeira linha, faz o Cucumber reconhecer as palavras-chave do Gherkin em português — Funcionalidade, Contexto, Cenário, Dado, Quando, Então e E:
# language: pt
Funcionalidade: Gerenciar unidades federativas
Para manter o cadastro de unidades federativas da aplicação
Como usuário do sistema
Quero incluir, buscar, alterar e excluir unidades federativas,
respeitando as regras de unicidade de sigla e nome e de autorização
Contexto:
Dado que não existe nenhuma unidade federativa cadastrada
Cenário: Incluir uma unidade federativa
Dado que sou um usuário administrador
Quando eu incluo a unidade federativa de código IBGE "35", sigla "SP" e nome "São Paulo"
Então a unidade federativa é incluída com sucesso
E a unidade federativa de código IBGE "35" passa a existir
Cenário: Não é possível incluir uma unidade federativa sem autorização de administrador
Dado que sou um usuário sem autorização de administrador
Quando eu tento incluir a unidade federativa de código IBGE "35", sigla "SP" e nome "São Paulo"
Então a inclusão é rejeitada por falta de autorização
Cenário: Não é possível incluir uma unidade federativa com código IBGE já existente
Dado que sou um usuário administrador
E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
Quando eu tento incluir a unidade federativa de código IBGE "35", sigla "RJ" e nome "Rio de Janeiro"
Então a inclusão é rejeitada porque já existe uma unidade federativa com o identificador "35"
Cenário: Não é possível incluir uma unidade federativa com sigla já existente
Dado que sou um usuário administrador
E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
Quando eu tento incluir a unidade federativa de código IBGE "33", sigla "SP" e nome "Rio de Janeiro"
Então a inclusão é rejeitada porque já existe uma unidade federativa com a sigla "SP"
Cenário: Não é possível incluir uma unidade federativa com nome já existente
Dado que sou um usuário administrador
E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
Quando eu tento incluir a unidade federativa de código IBGE "33", sigla "RJ" e nome "São Paulo"
Então a inclusão é rejeitada porque já existe uma unidade federativa com o nome "São Paulo"
Cenário: Não é possível incluir uma unidade federativa com nome já existente, ainda que com outras maiúsculas e minúsculas
Dado que sou um usuário administrador
E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
Quando eu tento incluir a unidade federativa de código IBGE "33", sigla "RJ" e nome "SÃO PAULO"
Então a inclusão é rejeitada porque já existe uma unidade federativa com o nome "SÃO PAULO"
Cenário: Buscar uma unidade federativa existente
Dado que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
E que sou um usuário sem autorização de administrador
Quando eu busco a unidade federativa de código IBGE "35"
Então a unidade federativa encontrada tem sigla "SP" e nome "São Paulo"
Cenário: Não é possível buscar uma unidade federativa inexistente
Dado que sou um usuário sem autorização de administrador
Quando eu tento buscar a unidade federativa de código IBGE "35"
Então a busca é rejeitada porque não existe unidade federativa com o identificador "35"
Cenário: Alterar uma unidade federativa
Dado que sou um usuário administrador
E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
Quando eu altero a unidade federativa de código IBGE "35" para sigla "SP" e nome "Estado de São Paulo"
Então a unidade federativa é alterada com sucesso
E a unidade federativa de código IBGE "35" passa a ter sigla "SP" e nome "Estado de São Paulo"
Cenário: Alterar uma unidade federativa mantendo sua própria sigla e nome
Dado que sou um usuário administrador
E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
Quando eu altero a unidade federativa de código IBGE "35" para sigla "SP" e nome "São Paulo"
Então a unidade federativa é alterada com sucesso
Cenário: Não é possível alterar uma unidade federativa sem autorização de administrador
Dado que sou um usuário sem autorização de administrador
E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
Quando eu tento alterar a unidade federativa de código IBGE "35" para sigla "SP" e nome "Estado de São Paulo"
Então a alteração é rejeitada por falta de autorização
Cenário: Não é possível alterar uma unidade federativa inexistente
Dado que sou um usuário administrador
Quando eu tento alterar a unidade federativa de código IBGE "35" para sigla "SP" e nome "São Paulo"
Então a alteração é rejeitada porque não existe unidade federativa com o identificador "35"
Cenário: Não é possível alterar uma unidade federativa para a sigla de outra
Dado que sou um usuário administrador
E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
E que existe uma unidade federativa com código IBGE "33", sigla "RJ" e nome "Rio de Janeiro"
Quando eu tento alterar a unidade federativa de código IBGE "33" para sigla "SP" e nome "Rio de Janeiro"
Então a alteração é rejeitada porque já existe uma unidade federativa com a sigla "SP"
Cenário: Excluir uma unidade federativa
Dado que sou um usuário administrador
E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
Quando eu excluo a unidade federativa de código IBGE "35"
Então a unidade federativa é excluída com sucesso
E a unidade federativa de código IBGE "35" deixa de existir
Cenário: Não é possível excluir uma unidade federativa sem autorização de administrador
Dado que sou um usuário sem autorização de administrador
E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
Quando eu tento excluir a unidade federativa de código IBGE "35"
Então a exclusão é rejeitada por falta de autorização
Cenário: Não é possível excluir uma unidade federativa inexistente
Dado que sou um usuário administrador
Quando eu tento excluir a unidade federativa de código IBGE "35"
Então a exclusão é rejeitada porque não existe unidade federativa com o identificador "35"
Implemente as step definitions que ligam os cenários ao serviço. Como UnidadeFederativaServico depende das abstrações ISegurancaServico e IUnidadeFederativaRepositorio, use o Mockito para dublá-las: a segurança simula a autorização do usuário, e o repositório simula a persistência através de um mapa em memória, preservando as mesmas regras de unicidade que uma implementação real deve respeitar — inclusive a comparação de nomes que ignora maiúsculas e minúsculas. Cada método anotado com @Dado, @Quando ou @Entao implementa os passos cujo texto corresponde à sua expressão, na qual {string} captura um texto entre aspas e o passa como argumento. O comportamento de cada método dos dublês é definido com when(...).thenAnswer(...) ou, para os métodos void, como alterar e excluir, que não podem ser passados a when, com doAnswer(...).when(...).
package com.example.exemplo.dominio.geografia.unidadefederativa;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import com.example.exemplo.dominio.Autorizacao;
import com.example.exemplo.dominio.geografia.unidadefederativa.spi.IUnidadeFederativaRepositorio;
import dev.sauloaraujo.d5.framework.dominio.AgregadoExistenteExcecao;
import dev.sauloaraujo.d5.framework.dominio.AgregadoInexistenteExcecao;
import dev.sauloaraujo.d5.framework.dominio.spi.ISegurancaServico;
import dev.sauloaraujo.d5.framework.dominio.spi.NaoAutorizadoExcecao;
import io.cucumber.java.pt.Dado;
import io.cucumber.java.pt.Entao;
import io.cucumber.java.pt.Quando;
public final class UnidadeFederativaStepDefinitions {
private final Map<String, UnidadeFederativa> unidadesFederativas = new HashMap<>();
private final ISegurancaServico segurancaServico = mock(ISegurancaServico.class);
private final IUnidadeFederativaRepositorio repositorio = mock(IUnidadeFederativaRepositorio.class);
private final UnidadeFederativaServico servico = new UnidadeFederativaServico(segurancaServico, repositorio);
private UnidadeFederativa resultadoInclusao;
private UnidadeFederativa resultadoBusca;
private RuntimeException excecaoCapturada;
public UnidadeFederativaStepDefinitions() {
when(repositorio.existePorIdentificador(any()))
.thenAnswer(chamada -> unidadesFederativas.containsKey(chave(chamada.getArgument(0))));
when(repositorio.buscarPorIdentificador(any())).thenAnswer(chamada -> {
UnidadeFederativaCodigoIbge codigoIbge = chamada.getArgument(0);
var unidadeFederativa = unidadesFederativas.get(chave(codigoIbge));
if (unidadeFederativa == null) {
throw new AgregadoInexistenteExcecao(
String.format(UnidadeFederativaServico.IDENTIFICADOR_INEXISTENTE_MODELO, codigoIbge));
}
return unidadeFederativa;
});
when(repositorio.existePorSigla(any(), any())).thenAnswer(chamada -> {
UnidadeFederativaSigla sigla = chamada.getArgument(0);
Optional<UnidadeFederativaCodigoIbge> identificadorIgnorado = chamada.getArgument(1);
return unidadesFederativas.values().stream().anyMatch(
candidata -> candidata.getSigla().equals(sigla) && !foiIgnorada(candidata, identificadorIgnorado));
});
when(repositorio.existePorNome(any(), any())).thenAnswer(chamada -> {
String nome = chamada.getArgument(0);
Optional<UnidadeFederativaCodigoIbge> identificadorIgnorado = chamada.getArgument(1);
return unidadesFederativas.values().stream()
.anyMatch(candidata -> candidata.getNome().equalsIgnoreCase(nome)
&& !foiIgnorada(candidata, identificadorIgnorado));
});
when(repositorio.incluir(any())).thenAnswer(chamada -> {
UnidadeFederativa unidadeFederativa = chamada.getArgument(0);
unidadesFederativas.put(chave(unidadeFederativa.getIdentificador().orElseThrow()), unidadeFederativa);
return unidadeFederativa;
});
doAnswer(chamada -> {
UnidadeFederativa unidadeFederativa = chamada.getArgument(0);
unidadesFederativas.put(chave(unidadeFederativa.getIdentificador().orElseThrow()), unidadeFederativa);
return null;
}).when(repositorio).alterar(any());
doAnswer(chamada -> {
UnidadeFederativaCodigoIbge codigoIbge = chamada.getArgument(0);
unidadesFederativas.remove(chave(codigoIbge));
return null;
}).when(repositorio).excluir(any());
}
private static String chave(UnidadeFederativaCodigoIbge codigoIbge) {
return codigoIbge.toString();
}
private static boolean foiIgnorada(UnidadeFederativa candidata,
Optional<UnidadeFederativaCodigoIbge> identificadorIgnorado) {
return identificadorIgnorado.isPresent() && candidata.getIdentificador().equals(identificadorIgnorado);
}
private static UnidadeFederativa criar(String codigoIbge, String sigla, String nome) {
return new UnidadeFederativa(new UnidadeFederativaCodigoIbge(codigoIbge), new UnidadeFederativaSigla(sigla),
nome);
}
@Dado("que não existe nenhuma unidade federativa cadastrada")
public void que_nao_existe_nenhuma_unidade_federativa_cadastrada() {
unidadesFederativas.clear();
}
@Dado("que sou um usuário administrador")
public void que_sou_um_usuario_administrador() {
when(segurancaServico.possuiAutorizacao(Autorizacao.ADMINISTRADOR.name())).thenReturn(true);
}
@Dado("que sou um usuário sem autorização de administrador")
public void que_sou_um_usuario_sem_autorizacao_de_administrador() {
when(segurancaServico.possuiAutorizacao(Autorizacao.ADMINISTRADOR.name())).thenReturn(false);
}
@Dado("que existe uma unidade federativa com código IBGE {string}, sigla {string} e nome {string}")
public void que_existe_uma_unidade_federativa(String codigoIbge, String sigla, String nome) {
var unidadeFederativa = criar(codigoIbge, sigla, nome);
unidadesFederativas.put(chave(unidadeFederativa.getIdentificador().orElseThrow()), unidadeFederativa);
}
@Quando("eu incluo a unidade federativa de código IBGE {string}, sigla {string} e nome {string}")
public void eu_incluo_a_unidade_federativa(String codigoIbge, String sigla, String nome) {
incluir(codigoIbge, sigla, nome);
}
@Quando("eu tento incluir a unidade federativa de código IBGE {string}, sigla {string} e nome {string}")
public void eu_tento_incluir_a_unidade_federativa(String codigoIbge, String sigla, String nome) {
incluir(codigoIbge, sigla, nome);
}
private void incluir(String codigoIbge, String sigla, String nome) {
try {
resultadoInclusao = servico.incluir(criar(codigoIbge, sigla, nome));
} catch (RuntimeException excecao) {
excecaoCapturada = excecao;
}
}
@Quando("eu busco a unidade federativa de código IBGE {string}")
public void eu_busco_a_unidade_federativa(String codigoIbge) {
buscar(codigoIbge);
}
@Quando("eu tento buscar a unidade federativa de código IBGE {string}")
public void eu_tento_buscar_a_unidade_federativa(String codigoIbge) {
buscar(codigoIbge);
}
private void buscar(String codigoIbge) {
try {
resultadoBusca = servico.buscar(new UnidadeFederativaCodigoIbge(codigoIbge));
} catch (RuntimeException excecao) {
excecaoCapturada = excecao;
}
}
@Quando("eu altero a unidade federativa de código IBGE {string} para sigla {string} e nome {string}")
public void eu_altero_a_unidade_federativa(String codigoIbge, String sigla, String nome) {
alterar(codigoIbge, sigla, nome);
}
@Quando("eu tento alterar a unidade federativa de código IBGE {string} para sigla {string} e nome {string}")
public void eu_tento_alterar_a_unidade_federativa(String codigoIbge, String sigla, String nome) {
alterar(codigoIbge, sigla, nome);
}
private void alterar(String codigoIbge, String sigla, String nome) {
try {
servico.alterar(criar(codigoIbge, sigla, nome));
} catch (RuntimeException excecao) {
excecaoCapturada = excecao;
}
}
@Quando("eu excluo a unidade federativa de código IBGE {string}")
public void eu_excluo_a_unidade_federativa(String codigoIbge) {
excluir(codigoIbge);
}
@Quando("eu tento excluir a unidade federativa de código IBGE {string}")
public void eu_tento_excluir_a_unidade_federativa(String codigoIbge) {
excluir(codigoIbge);
}
private void excluir(String codigoIbge) {
try {
servico.excluir(new UnidadeFederativaCodigoIbge(codigoIbge));
} catch (RuntimeException excecao) {
excecaoCapturada = excecao;
}
}
@Entao("a unidade federativa é incluída com sucesso")
public void a_unidade_federativa_e_incluida_com_sucesso() {
assertNull(excecaoCapturada);
assertNotNull(resultadoInclusao);
}
@Entao("a unidade federativa de código IBGE {string} passa a existir")
public void a_unidade_federativa_passa_a_existir(String codigoIbge) {
assertTrue(unidadesFederativas.containsKey(codigoIbge));
}
@Entao("a inclusão é rejeitada por falta de autorização")
public void a_inclusao_e_rejeitada_por_falta_de_autorizacao() {
assertInstanceOf(NaoAutorizadoExcecao.class, excecaoCapturada);
}
@Entao("a inclusão é rejeitada porque já existe uma unidade federativa com o identificador {string}")
public void a_inclusao_e_rejeitada_pelo_identificador(String codigoIbge) {
assertMensagemDeExistencia(UnidadeFederativaServico.IDENTIFICADOR_EXISTENTE_MODELO, codigoIbge);
}
@Entao("a inclusão é rejeitada porque já existe uma unidade federativa com a sigla {string}")
public void a_inclusao_e_rejeitada_pela_sigla(String sigla) {
assertMensagemDeExistencia(UnidadeFederativaServico.SIGLA_EXISTENTE_MODELO, sigla);
}
@Entao("a inclusão é rejeitada porque já existe uma unidade federativa com o nome {string}")
public void a_inclusao_e_rejeitada_pelo_nome(String nome) {
assertMensagemDeExistencia(UnidadeFederativaServico.NOME_EXISTENTE_MODELO, nome);
}
@Entao("a unidade federativa encontrada tem sigla {string} e nome {string}")
public void a_unidade_federativa_encontrada_tem(String sigla, String nome) {
assertEquals(new UnidadeFederativaSigla(sigla), resultadoBusca.getSigla());
assertEquals(nome, resultadoBusca.getNome());
}
@Entao("a busca é rejeitada porque não existe unidade federativa com o identificador {string}")
public void a_busca_e_rejeitada_por_inexistencia(String codigoIbge) {
assertMensagemDeInexistencia(codigoIbge);
}
@Entao("a unidade federativa é alterada com sucesso")
public void a_unidade_federativa_e_alterada_com_sucesso() {
assertNull(excecaoCapturada);
}
@Entao("a unidade federativa de código IBGE {string} passa a ter sigla {string} e nome {string}")
public void a_unidade_federativa_passa_a_ter(String codigoIbge, String sigla, String nome) {
var unidadeFederativa = unidadesFederativas.get(codigoIbge);
assertNotNull(unidadeFederativa);
assertEquals(new UnidadeFederativaSigla(sigla), unidadeFederativa.getSigla());
assertEquals(nome, unidadeFederativa.getNome());
}
@Entao("a alteração é rejeitada por falta de autorização")
public void a_alteracao_e_rejeitada_por_falta_de_autorizacao() {
assertInstanceOf(NaoAutorizadoExcecao.class, excecaoCapturada);
}
@Entao("a alteração é rejeitada porque não existe unidade federativa com o identificador {string}")
public void a_alteracao_e_rejeitada_por_inexistencia(String codigoIbge) {
assertMensagemDeInexistencia(codigoIbge);
}
@Entao("a alteração é rejeitada porque já existe uma unidade federativa com a sigla {string}")
public void a_alteracao_e_rejeitada_pela_sigla(String sigla) {
assertMensagemDeExistencia(UnidadeFederativaServico.SIGLA_EXISTENTE_MODELO, sigla);
}
@Entao("a unidade federativa é excluída com sucesso")
public void a_unidade_federativa_e_excluida_com_sucesso() {
assertNull(excecaoCapturada);
}
@Entao("a unidade federativa de código IBGE {string} deixa de existir")
public void a_unidade_federativa_deixa_de_existir(String codigoIbge) {
assertFalse(unidadesFederativas.containsKey(codigoIbge));
}
@Entao("a exclusão é rejeitada por falta de autorização")
public void a_exclusao_e_rejeitada_por_falta_de_autorizacao() {
assertInstanceOf(NaoAutorizadoExcecao.class, excecaoCapturada);
}
@Entao("a exclusão é rejeitada porque não existe unidade federativa com o identificador {string}")
public void a_exclusao_e_rejeitada_por_inexistencia(String codigoIbge) {
assertMensagemDeInexistencia(codigoIbge);
}
private void assertMensagemDeExistencia(String modelo, Object argumento) {
assertInstanceOf(AgregadoExistenteExcecao.class, excecaoCapturada);
assertEquals(String.format(modelo, argumento), excecaoCapturada.getMessage());
}
private void assertMensagemDeInexistencia(Object argumento) {
assertInstanceOf(AgregadoInexistenteExcecao.class, excecaoCapturada);
assertEquals(String.format(UnidadeFederativaServico.IDENTIFICADOR_INEXISTENTE_MODELO, argumento),
excecaoCapturada.getMessage());
}
}
Execute os cenários no diretório exemplo:
mvn -pl dominio-geografia -am test
-pl restringe o build ao módulo dominio-geografia, e -am inclui nele os módulos dos quais esse módulo depende — aqui, dominio-compartilhado —, que, sem essa opção, o Maven procuraria no repositório local. O resultado deve informar 16 testes, um por cenário, sem falhas.
Aplicação
A pesquisa de unidades federativas, ao contrário das demais operações, não passa pelas regras do agregado: ela pertence ao lado de consulta e consulta diretamente uma projeção somente leitura, com paginação, ordenação e filtro. Seguindo as convenções, as classes ficam no módulo exemplo-aplicacao, pacote com.example.exemplo.aplicacao.geografia.unidadefederativa. Crie o package-info.java desse pacote, anotado com @NullMarked:
@org.jspecify.annotations.NullMarked
package com.example.exemplo.aplicacao.geografia.unidadefederativa;
Projeção
A projeção de unidade federativa contém seu identificador, sua sigla e seu nome — o suficiente para, por exemplo, popular uma lista de seleção. Assim como no agregado, o identificador e a sigla são representados por seus objetos de valor; o construtor que recebe Strings existe para que a consulta JPQL possa instanciar a projeção diretamente. Ela implementa IProjecao, parametrizada pelo agregado que representa.
package com.example.exemplo.aplicacao.geografia.unidadefederativa;
import java.util.Objects;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativa;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaSigla;
import dev.sauloaraujo.d5.framework.aplicacao.IProjecao;
public final class UnidadeFederativaProjecao implements IProjecao<UnidadeFederativa> {
private final UnidadeFederativaCodigoIbge identificador;
private final UnidadeFederativaSigla sigla;
private final String nome;
public UnidadeFederativaProjecao(UnidadeFederativaCodigoIbge identificador, UnidadeFederativaSigla sigla,
String nome) {
this.identificador = Objects.requireNonNull(identificador);
this.sigla = Objects.requireNonNull(sigla);
this.nome = Objects.requireNonNull(nome);
}
public UnidadeFederativaProjecao(String identificador, String sigla, String nome) {
this(new UnidadeFederativaCodigoIbge(identificador), new UnidadeFederativaSigla(sigla), nome);
}
public UnidadeFederativaCodigoIbge getIdentificador() {
return identificador;
}
public UnidadeFederativaSigla getSigla() {
return sigla;
}
public String getNome() {
return nome;
}
}
Ordenação
As propriedades pelas quais a projeção pode ser ordenada são declaradas em um enum que implementa IOrdenacao, parametrizado pela projeção.
package com.example.exemplo.aplicacao.geografia.unidadefederativa;
import dev.sauloaraujo.d5.framework.aplicacao.IOrdenacao;
public enum UnidadeFederativaOrdenacao implements IOrdenacao<UnidadeFederativaProjecao> {
IDENTIFICADOR, SIGLA, NOME
}
Consulta
Os parâmetros da pesquisa são representados por uma classe que estende Consulta, acrescentando os atributos sigla e nome, usados para filtrar as unidades federativas por um trecho da sigla e do nome.
package com.example.exemplo.aplicacao.geografia.unidadefederativa;
import java.util.Objects;
import java.util.Optional;
import dev.sauloaraujo.d5.framework.aplicacao.impl.Consulta;
public final class UnidadeFederativaConsulta extends Consulta<UnidadeFederativaOrdenacao> {
private final Optional<String> sigla;
private final Optional<String> nome;
public UnidadeFederativaConsulta(Optional<UnidadeFederativaOrdenacao> ordenacao, Optional<Boolean> descendente,
Optional<Integer> indicePagina, Optional<Integer> tamanhoPagina, Optional<String> sigla,
Optional<String> nome) {
super(ordenacao, descendente, indicePagina, tamanhoPagina);
this.sigla = Objects.requireNonNull(sigla);
this.nome = Objects.requireNonNull(nome);
}
public Optional<String> getSigla() {
return sigla;
}
public Optional<String> getNome() {
return nome;
}
}
Repositório de aplicação
Assim como o repositório do agregado, a pesquisa de projeções é acessada através de um repositório — neste caso, um repositório de aplicação. Sua interface fica no pacote com.example.exemplo.aplicacao.geografia.unidadefederativa.spi e estende IAgregadoAplicacaoRepositorio, parametrizada pela projeção e pela consulta de unidade federativa. Crie o package-info.java desse pacote, anotado com @NullMarked:
@org.jspecify.annotations.NullMarked
package com.example.exemplo.aplicacao.geografia.unidadefederativa.spi;
package com.example.exemplo.aplicacao.geografia.unidadefederativa.spi;
import com.example.exemplo.aplicacao.geografia.unidadefederativa.UnidadeFederativaConsulta;
import com.example.exemplo.aplicacao.geografia.unidadefederativa.UnidadeFederativaProjecao;
import dev.sauloaraujo.d5.framework.aplicacao.spi.IAgregadoAplicacaoRepositorio;
public interface IUnidadeFederativaAplicacaoRepositorio
extends IAgregadoAplicacaoRepositorio<UnidadeFederativaProjecao, UnidadeFederativaConsulta> {
}
Serviço de aplicação
A pesquisa é encapsulada em um serviço de aplicação que estende AgregadoAplicacaoServicoAbstrato. Ele recebe pelo construtor o repositório de aplicação e o UnidadeFederativaServico, reaproveitado apenas para validar se o usuário está autorizado a buscar unidades federativas.
package com.example.exemplo.aplicacao.geografia.unidadefederativa;
import com.example.exemplo.aplicacao.geografia.unidadefederativa.spi.IUnidadeFederativaAplicacaoRepositorio;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativa;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaServico;
import dev.sauloaraujo.d5.framework.aplicacao.impl.AgregadoAplicacaoServicoAbstrato;
import dev.sauloaraujo.d5.framework.dominio.impl.AgregadoAutorizacoes;
public final class UnidadeFederativaAplicacaoServico extends
AgregadoAplicacaoServicoAbstrato<UnidadeFederativaCodigoIbge, UnidadeFederativa, AgregadoAutorizacoes<UnidadeFederativa>, UnidadeFederativaProjecao, UnidadeFederativaConsulta> {
public UnidadeFederativaAplicacaoServico(IUnidadeFederativaAplicacaoRepositorio repositorio,
UnidadeFederativaServico servico) {
super(repositorio, servico);
}
}
Infraestrutura
Entidade JPA
A persistência de unidade federativa é implementada no módulo exemplo-infraestrutura, no pacote com.example.exemplo.infraestrutura.geografia.unidadefederativa. Crie o package-info.java desse pacote, anotado com @NullMarked:
@org.jspecify.annotations.NullMarked
package com.example.exemplo.infraestrutura.geografia.unidadefederativa;
O primeiro passo é a entidade JPA, que mapeia o agregado para a tabela UFI_UNIDADE_FEDERATIVA. Ela implementa IAgregadoJpa, parametrizada pelo agregado que representa, e declara as restrições das colunas com as anotações do Bean Validation, deixando na @Column apenas o que elas não expressam; veja Entidades JPA. Todas as colunas são obrigatórias (@NotNull), e o tamanho máximo de cada uma é declarado com @Size: o do identificador e o da sigla vêm das constantes dos objetos de valor correspondentes, e o do nome, que não é uma regra do domínio, mas um limite da tabela, é escrito diretamente. O Hibernate usa essas anotações para gerar as colunas e para validar a entidade antes de gravá-la — um nome com mais de 50 caracteres resulta em 422, e não em um erro do banco de dados. A coluna do identificador não pode ser alterada, e as de sigla e nome são únicas, para que o banco de dados rejeite as duplicidades que a validação do serviço não impede sob concorrência. A restrição do nome, porém, diferencia maiúsculas de minúsculas, ao contrário da validação do serviço: sob concorrência, ela impede apenas nomes idênticos.
package com.example.exemplo.infraestrutura.geografia.unidadefederativa;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativa;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaSigla;
import dev.sauloaraujo.d5.framework.infraestrutura.IAgregadoJpa;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
@Entity
@Table(name = "UFI_UNIDADE_FEDERATIVA")
public class UnidadeFederativaJpa implements IAgregadoJpa<UnidadeFederativa> {
@Id
@Column(name = "UFI_ID", updatable = false)
@NotNull
@Size(max = UnidadeFederativaCodigoIbge.TAMANHO)
private String identificador;
@Column(name = "UFI_SG", unique = true)
@NotNull
@Size(max = UnidadeFederativaSigla.TAMANHO)
private String sigla;
@Column(name = "UFI_NM", unique = true)
@NotNull
@Size(max = 50)
private String nome;
protected UnidadeFederativaJpa() {
}
public UnidadeFederativaJpa(String identificador, String sigla, String nome) {
this.identificador = identificador;
this.sigla = sigla;
this.nome = nome;
}
public String getIdentificador() {
return identificador;
}
public String getSigla() {
return sigla;
}
public String getNome() {
return nome;
}
}
Repositório Spring Data
Em seguida, crie a interface Spring Data JPA que dá acesso à entidade. Além de estender JpaRepository, ela declara existePorSigla e existePorNome com consultas JPQL escritas explicitamente, já que essas verificações — que ignoram opcionalmente o identificador da própria unidade federativa — não são derivadas automaticamente do nome do método. existePorNome compara os nomes em minúsculas, de modo que "São Paulo" e "SÃO PAULO" sejam considerados o mesmo nome. Por fim, declara pesquisar, cuja consulta JPQL instancia a UnidadeFederativaProjecao diretamente através de uma expressão select new, filtrando pela sigla e pelo nome apenas quando o parâmetro correspondente não é nulo. Os filtros ignoram maiúsculas e minúsculas porque comparam a sigla e o nome em minúsculas com o padrão produzido por paraLike, já em minúsculas, declarando \ como caractere de escape.
package com.example.exemplo.infraestrutura.geografia.unidadefederativa;
import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import com.example.exemplo.aplicacao.geografia.unidadefederativa.UnidadeFederativaProjecao;
public interface IUnidadeFederativaRepositorioJpa extends JpaRepository<UnidadeFederativaJpa, String> {
@Query("""
select count(u) > 0
from UnidadeFederativaJpa u
where u.sigla = :sigla
and (:identificadorIgnorado is null or u.identificador <> :identificadorIgnorado)
""")
boolean existePorSigla(String sigla, @Nullable String identificadorIgnorado);
@Query("""
select count(u) > 0
from UnidadeFederativaJpa u
where lower(u.nome) = lower(:nome)
and (:identificadorIgnorado is null or u.identificador <> :identificadorIgnorado)
""")
boolean existePorNome(String nome, @Nullable String identificadorIgnorado);
@Query("""
select new com.example.exemplo.aplicacao.geografia.unidadefederativa.UnidadeFederativaProjecao(u.identificador, u.sigla, u.nome)
from UnidadeFederativaJpa u
where (:sigla is null or lower(u.sigla) like :sigla escape '\\')
and (:nome is null or lower(u.nome) like :nome escape '\\')
""")
Page<UnidadeFederativaProjecao> pesquisar(@Nullable String sigla, @Nullable String nome, Pageable pageable);
}
Implementação do repositório
Implemente IUnidadeFederativaRepositorio estendendo AgregadoRepositorioAbstrato, que já implementa incluir, buscarPorIdentificador, alterar, excluir e existePorIdentificador a partir dos métodos paraJpa, paraAgregado e paraIdentificadorJpa — os únicos que restam implementar. existePorSigla e existePorNome delegam para a interface Spring Data, convertendo o identificador a ignorar para o tipo usado pela entidade JPA.
package com.example.exemplo.infraestrutura.geografia.unidadefederativa;
import java.util.Objects;
import java.util.Optional;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativa;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaServico;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaSigla;
import com.example.exemplo.dominio.geografia.unidadefederativa.spi.IUnidadeFederativaRepositorio;
import dev.sauloaraujo.d5.framework.infraestrutura.AgregadoRepositorioAbstrato;
public class UnidadeFederativaRepositorio extends
AgregadoRepositorioAbstrato<UnidadeFederativaCodigoIbge, UnidadeFederativa, String, UnidadeFederativaJpa>
implements IUnidadeFederativaRepositorio {
private final IUnidadeFederativaRepositorioJpa repositorioJpa;
public UnidadeFederativaRepositorio(IUnidadeFederativaRepositorioJpa repositorioJpa) {
super(repositorioJpa, UnidadeFederativaServico.IDENTIFICADOR_INEXISTENTE_MODELO);
this.repositorioJpa = Objects.requireNonNull(repositorioJpa);
}
@Override
protected UnidadeFederativaJpa paraJpa(UnidadeFederativa agregado) {
Objects.requireNonNull(agregado);
var identificador = agregado.getIdentificador().orElseThrow().toString();
return new UnidadeFederativaJpa(identificador, agregado.getSigla().toString(), agregado.getNome());
}
@Override
protected UnidadeFederativa paraAgregado(UnidadeFederativaJpa agregadoJpa) {
Objects.requireNonNull(agregadoJpa);
return new UnidadeFederativa(new UnidadeFederativaCodigoIbge(agregadoJpa.getIdentificador()),
new UnidadeFederativaSigla(agregadoJpa.getSigla()), agregadoJpa.getNome());
}
@Override
protected String paraIdentificadorJpa(UnidadeFederativaCodigoIbge identificador) {
Objects.requireNonNull(identificador);
return identificador.toString();
}
@Override
public boolean existePorSigla(UnidadeFederativaSigla sigla,
Optional<UnidadeFederativaCodigoIbge> identificadorIgnorado) {
Objects.requireNonNull(sigla);
Objects.requireNonNull(identificadorIgnorado);
var identificadorIgnoradoJpa = identificadorIgnorado.map(UnidadeFederativaCodigoIbge::toString).orElse(null);
return repositorioJpa.existePorSigla(sigla.toString(), identificadorIgnoradoJpa);
}
@Override
public boolean existePorNome(String nome, Optional<UnidadeFederativaCodigoIbge> identificadorIgnorado) {
Objects.requireNonNull(nome);
Objects.requireNonNull(identificadorIgnorado);
var identificadorIgnoradoJpa = identificadorIgnorado.map(UnidadeFederativaCodigoIbge::toString).orElse(null);
return repositorioJpa.existePorNome(nome, identificadorIgnoradoJpa);
}
}
Implementação do repositório de aplicação
Implemente também IUnidadeFederativaAplicacaoRepositorio estendendo AgregadoAplicacaoRepositorioAbstrato. Ela já implementa pesquisar(UnidadeFederativaConsulta), convertendo a consulta em paginação e ordenação do Spring Data — através de paraPropriedadeOrdenacao, que mapeia cada valor de UnidadeFederativaOrdenacao para a propriedade correspondente — e delegando a pesquisar(UnidadeFederativaConsulta, Pageable), que resta implementar: ele converte a sigla e o nome da consulta em padrões like com paraLike — em minúsculas, escapado e envolvido por %, ou null na ausência de filtro — e delega à interface Spring Data.
package com.example.exemplo.infraestrutura.geografia.unidadefederativa;
import java.util.Objects;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import com.example.exemplo.aplicacao.geografia.unidadefederativa.UnidadeFederativaConsulta;
import com.example.exemplo.aplicacao.geografia.unidadefederativa.UnidadeFederativaOrdenacao;
import com.example.exemplo.aplicacao.geografia.unidadefederativa.UnidadeFederativaProjecao;
import com.example.exemplo.aplicacao.geografia.unidadefederativa.spi.IUnidadeFederativaAplicacaoRepositorio;
import dev.sauloaraujo.d5.framework.infraestrutura.AgregadoAplicacaoRepositorioAbstrato;
public class UnidadeFederativaAplicacaoRepositorio extends
AgregadoAplicacaoRepositorioAbstrato<UnidadeFederativaProjecao, UnidadeFederativaOrdenacao, UnidadeFederativaConsulta>
implements IUnidadeFederativaAplicacaoRepositorio {
private final IUnidadeFederativaRepositorioJpa repositorioJpa;
public UnidadeFederativaAplicacaoRepositorio(IUnidadeFederativaRepositorioJpa repositorioJpa) {
this.repositorioJpa = Objects.requireNonNull(repositorioJpa);
}
@Override
protected Page<UnidadeFederativaProjecao> pesquisar(UnidadeFederativaConsulta consulta, Pageable pageable) {
Objects.requireNonNull(consulta);
Objects.requireNonNull(pageable);
var sigla = paraLike(consulta.getSigla());
var nome = paraLike(consulta.getNome());
return repositorioJpa.pesquisar(sigla, nome, pageable);
}
@Override
protected String paraPropriedadeOrdenacao(UnidadeFederativaOrdenacao ordenacao) {
Objects.requireNonNull(ordenacao);
return switch (ordenacao) {
case IDENTIFICADOR -> "identificador";
case SIGLA -> "sigla";
case NOME -> "nome";
};
}
}
Testes do repositório
Diferente do serviço, cujo comportamento é especificado com cenários em Gherkin sobre dublês, os repositórios são testados por integração contra um banco H2 embarcado, usando @DataJpaTest. Esse tipo de teste exige uma classe de configuração do Spring Boot no classpath de teste — o gerador já criou uma mínima, anotada com @SpringBootApplication:
package com.example.exemplo.infraestrutura;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class InfraestruturaTesteAplicacao {
}
Escreva os testes cobrindo as operações do repositório; as duas variações de existePorSigla e existePorNome — considerando todas as unidades federativas e ignorando o identificador de uma delas —; e a comparação de nomes que ignora maiúsculas e minúsculas.
package com.example.exemplo.infraestrutura.geografia.unidadefederativa;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativa;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaSigla;
import dev.sauloaraujo.d5.framework.dominio.AgregadoInexistenteExcecao;
@DataJpaTest
class UnidadeFederativaRepositorioTest {
@Autowired
private IUnidadeFederativaRepositorioJpa repositorioJpa;
private UnidadeFederativaRepositorio repositorio;
@BeforeEach
void configurar() {
repositorio = new UnidadeFederativaRepositorio(repositorioJpa);
}
private static UnidadeFederativa criar(String codigoIbge, String sigla, String nome) {
return new UnidadeFederativa(new UnidadeFederativaCodigoIbge(codigoIbge), new UnidadeFederativaSigla(sigla),
nome);
}
@Test
void incluiEBuscaPorIdentificador() {
repositorio.incluir(criar("35", "SP", "São Paulo"));
var unidadeFederativa = repositorio.buscarPorIdentificador(new UnidadeFederativaCodigoIbge("35"));
assertEquals(new UnidadeFederativaSigla("SP"), unidadeFederativa.getSigla());
assertEquals("São Paulo", unidadeFederativa.getNome());
}
@Test
void buscaPorIdentificadorInexistenteLancaExcecao() {
assertThrows(AgregadoInexistenteExcecao.class,
() -> repositorio.buscarPorIdentificador(new UnidadeFederativaCodigoIbge("35")));
}
@Test
void existePorIdentificador() {
repositorio.incluir(criar("35", "SP", "São Paulo"));
assertTrue(repositorio.existePorIdentificador(new UnidadeFederativaCodigoIbge("35")));
assertFalse(repositorio.existePorIdentificador(new UnidadeFederativaCodigoIbge("33")));
}
@Test
void altera() {
repositorio.incluir(criar("35", "SP", "São Paulo"));
repositorio.alterar(criar("35", "SP", "São Paulo Alterado"));
var unidadeFederativa = repositorio.buscarPorIdentificador(new UnidadeFederativaCodigoIbge("35"));
assertEquals("São Paulo Alterado", unidadeFederativa.getNome());
}
@Test
void exclui() {
repositorio.incluir(criar("35", "SP", "São Paulo"));
repositorio.excluir(new UnidadeFederativaCodigoIbge("35"));
assertFalse(repositorio.existePorIdentificador(new UnidadeFederativaCodigoIbge("35")));
}
@Test
void existePorSiglaConsiderandoTodasAsUnidadesFederativas() {
repositorio.incluir(criar("35", "SP", "São Paulo"));
assertTrue(repositorio.existePorSigla(new UnidadeFederativaSigla("SP"), Optional.empty()));
assertFalse(repositorio.existePorSigla(new UnidadeFederativaSigla("RJ"), Optional.empty()));
}
@Test
void existePorSiglaIgnorandoOProprioIdentificador() {
repositorio.incluir(criar("35", "SP", "São Paulo"));
var identificador = Optional.of(new UnidadeFederativaCodigoIbge("35"));
assertFalse(repositorio.existePorSigla(new UnidadeFederativaSigla("SP"), identificador));
}
@Test
void existePorSiglaNaoIgnorandoUmaUnidadeFederativaDiferente() {
repositorio.incluir(criar("35", "SP", "São Paulo"));
repositorio.incluir(criar("33", "RJ", "Rio de Janeiro"));
var identificador = Optional.of(new UnidadeFederativaCodigoIbge("33"));
assertTrue(repositorio.existePorSigla(new UnidadeFederativaSigla("SP"), identificador));
}
@Test
void existePorNomeConsiderandoTodasAsUnidadesFederativas() {
repositorio.incluir(criar("35", "SP", "São Paulo"));
assertTrue(repositorio.existePorNome("São Paulo", Optional.empty()));
assertFalse(repositorio.existePorNome("Rio de Janeiro", Optional.empty()));
}
@Test
void existePorNomeIgnorandoOProprioIdentificador() {
repositorio.incluir(criar("35", "SP", "São Paulo"));
var identificador = Optional.of(new UnidadeFederativaCodigoIbge("35"));
assertFalse(repositorio.existePorNome("São Paulo", identificador));
}
@Test
void existePorNomeNaoIgnorandoUmaUnidadeFederativaDiferente() {
repositorio.incluir(criar("35", "SP", "São Paulo"));
repositorio.incluir(criar("33", "RJ", "Rio de Janeiro"));
var identificador = Optional.of(new UnidadeFederativaCodigoIbge("33"));
assertTrue(repositorio.existePorNome("São Paulo", identificador));
}
@Test
void existePorNomeIgnorandoMaiusculasEMinusculas() {
repositorio.incluir(criar("35", "SP", "São Paulo"));
assertTrue(repositorio.existePorNome("SÃO PAULO", Optional.empty()));
}
}
Escreva também os testes do repositório de aplicação, cobrindo a pesquisa sem filtro, sem resultados, filtrando por um trecho do nome e da sigla e ordenando pela sigla.
package com.example.exemplo.infraestrutura.geografia.unidadefederativa;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import com.example.exemplo.aplicacao.geografia.unidadefederativa.UnidadeFederativaConsulta;
import com.example.exemplo.aplicacao.geografia.unidadefederativa.UnidadeFederativaOrdenacao;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativa;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaSigla;
import dev.sauloaraujo.d5.framework.infraestrutura.AgregadoRepositorioAbstrato;
@DataJpaTest
class UnidadeFederativaAplicacaoRepositorioTest {
@Autowired
private IUnidadeFederativaRepositorioJpa repositorioJpa;
private AgregadoRepositorioAbstrato<UnidadeFederativaCodigoIbge, UnidadeFederativa, String, UnidadeFederativaJpa> repositorio;
private UnidadeFederativaAplicacaoRepositorio repositorioAplicacao;
@BeforeEach
void configurar() {
repositorio = new UnidadeFederativaRepositorio(repositorioJpa);
repositorioAplicacao = new UnidadeFederativaAplicacaoRepositorio(repositorioJpa);
}
private static UnidadeFederativa criar(String codigoIbge, String sigla, String nome) {
return new UnidadeFederativa(new UnidadeFederativaCodigoIbge(codigoIbge), new UnidadeFederativaSigla(sigla),
nome);
}
private static UnidadeFederativaConsulta consultar() {
return consultar(UnidadeFederativaOrdenacao.NOME, Optional.empty(), Optional.empty());
}
private static UnidadeFederativaConsulta consultar(UnidadeFederativaOrdenacao ordenacao, Optional<String> sigla,
Optional<String> nome) {
return new UnidadeFederativaConsulta(Optional.of(ordenacao), Optional.empty(), Optional.empty(),
Optional.empty(), sigla, nome);
}
@Test
void pesquisaRetornaProjecaoComIdentificadorSiglaENome() {
repositorio.incluir(criar("35", "SP", "São Paulo"));
repositorio.incluir(criar("33", "RJ", "Rio de Janeiro"));
var resultado = repositorioAplicacao.pesquisar(consultar());
assertEquals(2, resultado.getTotal());
assertEquals(2, resultado.getProjecoes().size());
var primeira = resultado.getProjecoes().get(0);
assertEquals(new UnidadeFederativaCodigoIbge("33"), primeira.getIdentificador());
assertEquals(new UnidadeFederativaSigla("RJ"), primeira.getSigla());
assertEquals("Rio de Janeiro", primeira.getNome());
var segunda = resultado.getProjecoes().get(1);
assertEquals(new UnidadeFederativaCodigoIbge("35"), segunda.getIdentificador());
assertEquals(new UnidadeFederativaSigla("SP"), segunda.getSigla());
assertEquals("São Paulo", segunda.getNome());
}
@Test
void pesquisaSemUnidadesFederativasRetornaResultadoVazio() {
var resultado = repositorioAplicacao.pesquisar(consultar());
assertEquals(0, resultado.getTotal());
assertEquals(0, resultado.getProjecoes().size());
}
@Test
void pesquisaFiltrandoPorTrechoDoNome() {
repositorio.incluir(criar("35", "SP", "São Paulo"));
repositorio.incluir(criar("33", "RJ", "Rio de Janeiro"));
var resultado = repositorioAplicacao
.pesquisar(consultar(UnidadeFederativaOrdenacao.NOME, Optional.empty(), Optional.of("paulo")));
assertEquals(1, resultado.getTotal());
assertEquals("São Paulo", resultado.getProjecoes().get(0).getNome());
}
@Test
void pesquisaFiltrandoPorTrechoDaSigla() {
repositorio.incluir(criar("35", "SP", "São Paulo"));
repositorio.incluir(criar("33", "RJ", "Rio de Janeiro"));
var resultado = repositorioAplicacao
.pesquisar(consultar(UnidadeFederativaOrdenacao.NOME, Optional.of("r"), Optional.empty()));
assertEquals(1, resultado.getTotal());
assertEquals("Rio de Janeiro", resultado.getProjecoes().get(0).getNome());
}
@Test
void pesquisaOrdenadaPorSigla() {
repositorio.incluir(criar("33", "RJ", "Rio de Janeiro"));
repositorio.incluir(criar("12", "AC", "Acre"));
repositorio.incluir(criar("27", "AL", "Alagoas"));
var resultado = repositorioAplicacao
.pesquisar(consultar(UnidadeFederativaOrdenacao.SIGLA, Optional.empty(), Optional.empty()));
assertEquals(new UnidadeFederativaSigla("AC"), resultado.getProjecoes().get(0).getSigla());
assertEquals(new UnidadeFederativaSigla("AL"), resultado.getProjecoes().get(1).getSigla());
assertEquals(new UnidadeFederativaSigla("RJ"), resultado.getProjecoes().get(2).getSigla());
}
}
Execute os testes da infraestrutura — com -am, o Maven executa também os cenários do domínio:
mvn -pl infraestrutura -am test
UI
Unidade federativa é exposta como uma API REST no módulo exemplo-ui-backend, pacote com.example.exemplo.ui.backend.geografia.unidadefederativa. Crie o package-info.java desse pacote, anotado com @NullMarked:
@org.jspecify.annotations.NullMarked
package com.example.exemplo.ui.backend.geografia.unidadefederativa;
Controlador
O controlador estende AgregadoControladorAbstrato, parametrizado pelo identificador, agregado, autorizações, projeção e consulta de unidade federativa, e recebe pelo construtor o UnidadeFederativaServico e o UnidadeFederativaAplicacaoServico. O @RequestMapping combina o prefixo Backend.PREFIXO com o caminho geografia/unidade-federativa. O parâmetro identificador dos endpoints é recebido diretamente como UnidadeFederativaCodigoIbge graças à serialização de objetos de valor, e as operações de escrita são executadas em uma transação graças ao gerenciamento de transações — que é também o motivo de o controlador, ao contrário das classes do domínio e da aplicação, não ser final.
package com.example.exemplo.ui.backend.geografia.unidadefederativa;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.exemplo.aplicacao.geografia.unidadefederativa.UnidadeFederativaAplicacaoServico;
import com.example.exemplo.aplicacao.geografia.unidadefederativa.UnidadeFederativaConsulta;
import com.example.exemplo.aplicacao.geografia.unidadefederativa.UnidadeFederativaProjecao;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativa;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaServico;
import dev.sauloaraujo.d5.framework.dominio.impl.AgregadoAutorizacoes;
import dev.sauloaraujo.d5.framework.ui.AgregadoControladorAbstrato;
import dev.sauloaraujo.d5.framework.ui.Backend;
@RestController
@RequestMapping(Backend.PREFIXO + "geografia/unidade-federativa")
public class UnidadeFederativaControlador extends
AgregadoControladorAbstrato<UnidadeFederativaCodigoIbge, UnidadeFederativa, AgregadoAutorizacoes<UnidadeFederativa>, UnidadeFederativaProjecao, UnidadeFederativaConsulta> {
public UnidadeFederativaControlador(UnidadeFederativaServico servico,
UnidadeFederativaAplicacaoServico servicoAplicacao) {
super(servico, servicoAplicacao);
}
}
Segurança
O gerador criou uma configuração de segurança que restringe o backend a usuários autenticados, no pacote com.example.exemplo.ui.backend.seguranca, que estende SegurancaConfiguracaoAbstrata: ela já protege /backend/** com autenticação HTTP Basic e CSRF baseado em cookie, restando declarar, em autorizar, quais endpoints liberar — sem nenhuma regra, todo endpoint sob /backend/** seria negado; aqui, todos passam a exigir apenas autenticação — e expor os usuários disponíveis através de um UserDetailsService. Ela declara dois usuários em memória: a/a, com a autorização ADMINISTRADOR, e u/u, com USUARIO. O pacote tem um package-info.java, anotado com @NullMarked:
@org.jspecify.annotations.NullMarked
package com.example.exemplo.ui.backend.seguranca;
package com.example.exemplo.ui.backend.seguranca;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import com.example.exemplo.dominio.Autorizacao;
import dev.sauloaraujo.d5.framework.ui.seguranca.SegurancaConfiguracaoAbstrata;
@Configuration
public class SegurancaConfiguracao extends SegurancaConfiguracaoAbstrata {
@Override
protected void autorizar(
AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry autorizacao) {
autorizacao.requestMatchers(BACKEND).authenticated();
}
@Bean
UserDetailsService usuarios(PasswordEncoder codificadorSenha) {
var administrador = User.withUsername("a").password(codificadorSenha.encode("a"))
.authorities(Autorizacao.ADMINISTRADOR.name()).build();
var usuario = User.withUsername("u").password(codificadorSenha.encode("u"))
.authorities(Autorizacao.USUARIO.name()).build();
return new InMemoryUserDetailsManager(administrador, usuario);
}
}
Autenticação
Para o cliente descobrir o usuário autenticado e o que ele pode fazer, o gerador criou um controlador que estende AutenticacaoControladorAbstrato, expondo o recurso de autenticação em Backend.PREFIXO + "autenticacao", e o próprio recurso, AutenticacaoRecurso, que estende AutenticacaoRecursoAbstrato. Gerados, eles trazem apenas o nome do usuário. Altere o recurso, acrescentando o atributo unidadeFederativa, com as autorizações do usuário sobre o agregado, e altere o controlador, para recebê-las do UnidadeFederativaServico: criarAutorizacoes é chamado sem identificador, pois essas autorizações valem para o agregado como um todo. Ao acrescentar outros agregados, repita o atributo para cada um que o cliente precisar conhecer, por exemplo, para exibir ou ocultar as opções de menu.
package com.example.exemplo.ui.backend.seguranca;
import java.util.Objects;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativa;
import dev.sauloaraujo.d5.framework.dominio.impl.AgregadoAutorizacoes;
import dev.sauloaraujo.d5.framework.ui.seguranca.AutenticacaoRecursoAbstrato;
public class AutenticacaoRecurso extends AutenticacaoRecursoAbstrato {
private final AgregadoAutorizacoes<UnidadeFederativa> unidadeFederativa;
public AutenticacaoRecurso(String usuario, AgregadoAutorizacoes<UnidadeFederativa> unidadeFederativa) {
super(usuario);
this.unidadeFederativa = Objects.requireNonNull(unidadeFederativa);
}
public AgregadoAutorizacoes<UnidadeFederativa> getUnidadeFederativa() {
return unidadeFederativa;
}
}
package com.example.exemplo.ui.backend.seguranca;
import java.util.Objects;
import java.util.Optional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaServico;
import dev.sauloaraujo.d5.framework.dominio.spi.ISegurancaServico;
import dev.sauloaraujo.d5.framework.ui.Backend;
import dev.sauloaraujo.d5.framework.ui.seguranca.AutenticacaoControladorAbstrato;
@RestController
@RequestMapping(Backend.PREFIXO + "autenticacao")
public class AutenticacaoControlador extends AutenticacaoControladorAbstrato {
private final UnidadeFederativaServico unidadeFederativaServico;
public AutenticacaoControlador(ISegurancaServico segurancaServico,
UnidadeFederativaServico unidadeFederativaServico) {
super(segurancaServico);
this.unidadeFederativaServico = Objects.requireNonNull(unidadeFederativaServico);
}
@Override
protected AutenticacaoRecurso recurso() {
var unidadeFederativa = unidadeFederativaServico.criarAutorizacoes(Optional.empty());
return new AutenticacaoRecurso(segurancaServico.getUsuario(), unidadeFederativa);
}
}
O JSON devolvido tem, além de usuario, o objeto unidadeFederativa com incluir, buscar, alterar e excluir.
Aplicação Spring Boot
Por fim, o gerador criou a classe com o main que sobe a aplicação, anotada com @SpringBootApplication, no pacote raiz com.example.exemplo. É a partir do pacote dela que o component scan encontra os controladores e a configuração de segurança, e que o registro de beans encontra os serviços e as implementações de repositório de todos os subdomínios. O pacote tem um package-info.java, anotado com @NullMarked:
@org.jspecify.annotations.NullMarked
package com.example.exemplo;
package com.example.exemplo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BackendAplicacao {
public static void main(String[] args) {
SpringApplication.run(BackendAplicacao.class, args);
}
}
Testando com o Swagger UI
Suba BackendAplicacao pela IDE ou, no diretório exemplo, executando:
mvn install
mvn -f ui-backend spring-boot:run
O mvn install é necessário porque spring-boot:run, executado apenas no módulo ui-backend, resolve os demais módulos do projeto a partir do repositório Maven local. A aplicação usa um banco H2 em memória, trazido por d5-framework-infraestrutura, cujas tabelas são criadas a partir das entidades JPA a cada inicialização — portanto, os dados incluídos se perdem quando a aplicação é encerrada.
Acesse http://localhost:8080/swagger-ui/index.html. Como d5-framework-springdoc está no classpath, o Swagger UI é gerado automaticamente a partir dos controladores do backend, listados sob a tag unidade-federativa-controlador:

Como todo endpoint sob /backend/** exige um usuário autenticado, e a aplicação não desafia o navegador com o diálogo nativo de autenticação básica, é preciso autenticar pelo próprio Swagger UI: clique em Authorize, no canto superior direito, e informe um dos usuários criados na configuração de segurança — a/a, administrador, ou u/u, usuário comum.

A partir daí, toda requisição disparada pelo Swagger UI leva o cabeçalho Authorization com essas credenciais e, junto, o cabeçalho X-XSRF-TOKEN, com o token de CSRF exigido pelas operações de escrita. O Swagger UI lê esse token, a cada requisição, do cookie XSRF-TOKEN, que o servidor envia já ao carregar a página; veja CSRF no Swagger UI. A primeira requisição autenticada, porém, renova o token: sua resposta apaga o cookie, e só a resposta seguinte traz um novo. Por isso, uma operação de escrita executada logo depois da primeira requisição autenticada vai sem o token e responde 403 com a mensagem Token CSRF ausente ou inválido. — basta executá-la de novo, já com o novo cookie.
Com a sessão autenticada, expanda cada operação e use Try it out:
Incluir
GET /backend/geografia/unidade-federativa/inclusao não recebe parâmetros e devolve um recurso vazio, usado apenas para montar o formulário de inclusão. POST /backend/geografia/unidade-federativa/incluir recebe a unidade federativa no corpo:
{
"identificador": "35",
"sigla": "SP",
"nome": "São Paulo"
}
O código IBGE e a sigla trafegam como strings simples graças à serialização de objetos de valor.


Pesquisar
GET /backend/geografia/unidade-federativa/pesquisa recebe os campos de UnidadeFederativaConsulta — sigla e nome (filtros parciais, opcionais), ordenacao, descendente, indicePagina e tamanhoPagina — como parâmetros de consulta individuais, e o Swagger UI, graças à expansão da consulta feita pelo d5-framework-springdoc, exibe cada um como um campo separado do formulário. Deixe em branco os campos que não quiser informar — eles assumem os padrões declarados em Consulta; deixe todos em branco para listar a primeira página, sem filtro. Valores inválidos, como tamanhoPagina igual a 0, resultam em 422, com a mensagem da validação; veja ExcecaoManipulador.


Alterar
GET /backend/geografia/unidade-federativa/{identificador}/alteracao recebe o código IBGE no parâmetro de caminho identificador — por exemplo, 35 — e devolve o agregado e as autorizações do usuário atual sobre ele:


POST /backend/geografia/unidade-federativa/{identificador}/alterar recebe o mesmo identificador no caminho e a unidade federativa alterada no corpo, repetindo o identificador:
{
"identificador": "35",
"sigla": "SP",
"nome": "São Paulo (alterado)"
}


Excluir
GET /backend/geografia/unidade-federativa/{identificador}/exclusao recebe o mesmo parâmetro identificador e devolve novamente o agregado e as autorizações, desta vez para confirmar a exclusão:


POST /backend/geografia/unidade-federativa/{identificador}/excluir recebe apenas o identificador no caminho, sem corpo:


Para trocar de usuário, clique em Authorize, em seguida em Logout e autentique-se novamente, desta vez como u/u. Como o usuário u não é administrador, repetir as operações de escrita autenticado como u deve falhar de acordo com as autorizações definidas em UnidadeFederativaServico — a pesquisa e os formulários de alteração e de exclusão, que dependem apenas da autorização de busca, são permitidos a qualquer usuário autenticado, mas as operações de escrita respondem 403.
Na lição 2, você vai acrescentar ao subdomínio Geografia o município, que referencia a unidade federativa.