BaseValidationErrorMapper.java
package com.github.jenkaby.bikerental.shared.web.advice;
import com.github.jenkaby.bikerental.shared.web.response.ValidationError;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Path;
import jakarta.validation.metadata.ConstraintDescriptor;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.stereotype.Component;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static com.github.jenkaby.bikerental.shared.web.advice.ErrorCodes.VALIDATION_ERROR;
@Component
public class BaseValidationErrorMapper implements ValidationErrorMapper {
private static final Set<String> INTERNAL_ATTRIBUTES = Set.of("message", "groups", "payload");
private final static Pattern CODE_PATTERN = Pattern.compile("([a-z])([A-Z])");
@Override
public ValidationError toValidationError(ConstraintViolation<?> violation) {
String field = extractFieldName(violation);
String code = mapConstraintCode(violation);
Map<String, Object> params = extractConstraintParams(violation.getConstraintDescriptor());
return new ValidationError(field, code, params);
}
@Override
public ValidationError mapFieldError(FieldError fieldError) {
var field = fieldError.getField();
var code = extractConstraint(fieldError.getCodes());
var params = extractParams(fieldError);
return new ValidationError(field, code, params);
}
@Override
public ValidationError mapObjectError(ObjectError objectError) {
var code = extractConstraint(objectError.getCodes());
return new ValidationError(null, code, null);
}
@Override
public ValidationError mapResolvableError(MessageSourceResolvable resolvable) {
var code = extractConstraint(resolvable.getCodes());
var params = extractParams(resolvable.getArguments());
return new ValidationError(null, prefixCode(code), params);
}
private String mapConstraintCode(ConstraintViolation<?> violation) {
var simpleConstraintName = violation.getConstraintDescriptor()
.getAnnotation()
.annotationType()
.getSimpleName();
return prefixCode(normalizeCode(simpleConstraintName));
}
private String normalizeCode(String rawCode) {
return CODE_PATTERN.matcher(rawCode).replaceAll("$1_$2").toLowerCase();
}
private String prefixCode(String code) {
return "validation." + code;
}
private Map<String, Object> extractParams(FieldError fieldError) {
var args = fieldError.getArguments();
if (args == null) {
return null;
}
return Arrays.stream(args)
.filter(arg -> arg instanceof ConstraintDescriptor<?>)
.map(arg -> (ConstraintDescriptor<?>) arg)
.findFirst()
.map(this::extractConstraintParams)
.orElse(null);
}
private Map<String, Object> extractConstraintParams(ConstraintDescriptor<?> descriptor) {
Map<String, Object> attributes = descriptor.getAttributes();
return attributes.entrySet()
.stream()
.filter(e -> !isInternalAttribute(e.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
private Map<String, Object> extractParams(Object[] arguments) {
if (arguments == null) {
return null;
}
for (Object arg : arguments) {
if (arg instanceof ConstraintDescriptor<?> descriptor) {
return this.extractConstraintParams(descriptor);
}
}
return null;
}
private boolean isInternalAttribute(String key) {
return INTERNAL_ATTRIBUTES.contains(key);
}
private String extractFieldName(ConstraintViolation<?> violation) {
StringBuilder path = new StringBuilder();
for (Path.Node node : violation.getPropertyPath()) {
if (node.getName() != null) {
if (!path.isEmpty()) {
path.append(".");
}
path.append(node.getName());
}
if (node.getIndex() != null) {
path.append("[").append(node.getIndex()).append("]");
}
if (node.getKey() != null) {
path.append("[").append(node.getKey()).append("]");
}
}
return path.toString();
}
private String extractConstraint(String[] codes) {
if (codes == null || codes.length == 0) {
return VALIDATION_ERROR;
}
return prefixCode(normalizeCode(codes[codes.length - 1]));
}
}