ProductCreateValidator.java
package com.albertsilva.dev.asjcatalog.validation.product.validator;
import java.util.ArrayList;
import java.util.List;
import com.albertsilva.dev.asjcatalog.dto.product.request.ProductCreateRequest;
import com.albertsilva.dev.asjcatalog.repository.CategoryRepository;
import com.albertsilva.dev.asjcatalog.repository.ProductRepository;
import com.albertsilva.dev.asjcatalog.validation.product.annotation.ProductCreateValid;
import com.albertsilva.dev.asjcatalog.web.exception.response.FieldMessage;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
/**
* Validator de {@link ProductCreateValid}: duas verificações sobre
* {@code ProductCreateRequest}, sempre executadas (acumulam violações).
*
* <ol>
* <li><b>Nome único:</b> se o nome não for {@code null}/em branco, normaliza
* ({@code trim()} + {@code toLowerCase()}) e consulta
* {@code ProductRepository.existsByNameIgnoreCase}; violação em {@code name}
* com {@code {product.name.unique}}.</li>
* <li><b>Categorias existentes:</b> se {@code categoryIds} não for
* {@code null} nem vazio, chama {@code CategoryRepository.existsById} para
* cada id (uma consulta por id, interrompendo na primeira ausente); violação
* em {@code categoryIds} com {@code {product.categoryIds.invalid}}. Ids
* repetidos passam (cada um existe) e um elemento {@code null} na lista faria
* {@code existsById} lançar exceção (a confirmar em testes).</li>
* </ol>
*
* <p>
* Consulta o banco; não usa HTTP nem autenticação. Usa {@code FieldMessage}
* (pacote de exceções da camada web) como estrutura de acúmulo. A obrigação de
* {@code categoryIds} não vazio é de {@code @NotEmpty} no DTO.
* </p>
*/
public class ProductCreateValidator implements ConstraintValidator<ProductCreateValid, ProductCreateRequest> {
/** Repositório de produtos, usado na verificação de nome duplicado. */
private final ProductRepository productRepository;
/** Repositório de categorias, usado na verificação de existência dos ids. */
private final CategoryRepository categoryRepository;
/**
* Construtor utilizado para injetar as dependências
* necessárias para a validação.
*
* @param productRepository repositório de produtos, utilizado para
* verificar a existência de produtos com
* o mesmo nome
* @param categoryRepository repositório de categorias, utilizado para
* verificar a existência das categorias
* informadas no DTO
*/
public ProductCreateValidator(ProductRepository productRepository,
CategoryRepository categoryRepository) {
this.productRepository = productRepository;
this.categoryRepository = categoryRepository;
}
/**
* Verifica nome único e existência das categorias, acumulando as violações.
*
* @param dto dados de criação
* @param context contexto usado para registrar as violações nos campos
* {@code name} e {@code categoryIds}
* @return {@code true} se nenhuma violação for encontrada
*/
@Override
public boolean isValid(ProductCreateRequest dto, ConstraintValidatorContext context) {
List<FieldMessage> errors = new ArrayList<>();
validateUniqueName(dto, errors);
validateCategories(dto, errors);
addErrors(errors, context);
return errors.isEmpty();
}
/**
* Valida se já existe um produto cadastrado
* com o mesmo nome informado.
*
* <p>
* Antes da verificação, o nome é normalizado:
* <ul>
* <li>{@code trim()} (remove espaços apenas nas extremidades)</li>
* <li>Convertendo para letras minúsculas</li>
* </ul>
*
* <p>
* Caso o nome já exista, uma mensagem de erro
* é adicionada à lista de erros.
*
* @param dto objeto contendo os dados do produto
* @param errors lista responsável por armazenar
* os erros encontrados durante a validação
*/
private void validateUniqueName(ProductCreateRequest dto, List<FieldMessage> errors) {
if (dto.name() == null || dto.name().isBlank()) {
return;
}
String normalizedName = dto.name().trim().toLowerCase();
boolean productAlreadyExists = productRepository.existsByNameIgnoreCase(normalizedName);
if (productAlreadyExists) {
errors.add(new FieldMessage("name", "{product.name.unique}"));
}
}
/**
* Valida se todas as categorias informadas
* no produto existem na base de dados.
*
* <p>
* A validação percorre todos os identificadores
* de categorias informados e verifica individualmente
* sua existência.
*
* <p>
* Caso ao menos uma categoria não exista,
* uma mensagem de erro é adicionada à lista
* de erros.
*
* @param dto objeto contendo os dados do produto
* @param errors lista responsável por armazenar
* os erros encontrados durante a validação
*/
private void validateCategories(ProductCreateRequest dto, List<FieldMessage> errors) {
if (dto.categoryIds() == null || dto.categoryIds().isEmpty()) {
return;
}
boolean invalidCategory = dto.categoryIds().stream().anyMatch(id -> !categoryRepository.existsById(id));
if (invalidCategory) {
errors.add(new FieldMessage("categoryIds", "{product.categoryIds.invalid}"));
}
}
/**
* Adiciona ao contexto de validação todos os erros
* encontrados durante o processo de validação.
*
* <p>
* O método desabilita a mensagem padrão do Bean Validation
* para permitir o registro de mensagens customizadas
* associadas a campos específicos.
*
* @param errors lista contendo os erros encontrados
* @param context contexto utilizado para registrar
* as violações de validação
*/
private void addErrors(List<FieldMessage> errors, ConstraintValidatorContext context) {
if (errors.isEmpty()) {
return;
}
context.disableDefaultConstraintViolation();
for (FieldMessage error : errors) {
context.buildConstraintViolationWithTemplate(error.message()).addPropertyNode(error.fieldName())
.addConstraintViolation();
}
}
}