JakartaSheetBeanValidator.java
package com.gh.mygreen.xlsmapper.validation.beanvalidation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import org.hibernate.validator.internal.engine.path.NodeImpl;
import org.hibernate.validator.internal.engine.path.PathImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gh.mygreen.xlsmapper.fieldaccessor.LabelGetterFactory;
import com.gh.mygreen.xlsmapper.fieldaccessor.PositionGetterFactory;
import com.gh.mygreen.xlsmapper.localization.MessageInterpolator;
import com.gh.mygreen.xlsmapper.localization.ResourceBundleMessageResolver;
import com.gh.mygreen.xlsmapper.util.ArgUtils;
import com.gh.mygreen.xlsmapper.util.CellPosition;
import com.gh.mygreen.xlsmapper.util.Utils;
import com.gh.mygreen.xlsmapper.validation.FieldError;
import com.gh.mygreen.xlsmapper.validation.ObjectValidator;
import com.gh.mygreen.xlsmapper.validation.SheetBindingErrors;
import com.gh.mygreen.xlsmapper.validation.fieldvalidation.FieldFormatter;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Path;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import jakarta.validation.metadata.ConstraintDescriptor;
/**
* Jakarta Bean Validaion 3.0/3.1 を利用したValidator.
*
* @since 2.3
* @author T.TSUCHIE
*
*/
public class JakartaSheetBeanValidator implements ObjectValidator<Object> {
private static final Logger logger = LoggerFactory.getLogger(JakartaSheetBeanValidator.class);
/**
* BeanValidationのアノテーションの属性で、メッセージ中の変数から除外するもの。
* <p>メッセージの再構築を行う際に必要
*/
private static final Set<String> EXCLUDE_MESSAGE_ANNOTATION_ATTRIBUTES;
static {
Set<String> set = new HashSet<String>(3);
set.add("message");
set.add("groups");
set.add("payload");
EXCLUDE_MESSAGE_ANNOTATION_ATTRIBUTES = Collections.unmodifiableSet(set);
}
private final Validator targetValidator;
public JakartaSheetBeanValidator(final Validator targetValidator) {
ArgUtils.notNull(targetValidator, "targetValidator");
this.targetValidator = targetValidator;
}
public JakartaSheetBeanValidator() {
this.targetValidator = createDefaultValidator();
}
/**
* Bean Validaion のデフォルトのインスタンスを作成します。
* @return Validatorのインスタンス。
*/
protected Validator createDefaultValidator() {
final ValidatorFactory validatorFactory = Validation.byDefaultProvider().configure()
.messageInterpolator(new JakartaMessageInterpolatorAdapter(new ResourceBundleMessageResolver(), new MessageInterpolator()))
.buildValidatorFactory();
final Validator validator = validatorFactory.usingContext()
.getValidator();
return validator;
}
/**
* Bean ValidationのValidatorを取得する。
* @return Validatorのインスタンス。
*/
public Validator getTargetValidator() {
return targetValidator;
}
/**
* グループを指定して検証を実行する。
* @param targetObj 検証対象のオブジェクト。
* @param errors エラーオブジェクト
* @param groups BeanValiationのグループのクラス
*/
@Override
public void validate(final Object targetObj, final SheetBindingErrors<?> errors, final Class<?>... groups) {
ArgUtils.notNull(targetObj, "targetObj");
ArgUtils.notNull(errors, "errors");
processConstraintViolation(getTargetValidator().validate(targetObj, groups), errors);
}
/**
* BeanValidationの検証結果をSheet用のエラーに変換する
* @param violations BeanValidationの検証結果
* @param errors シートのエラー
*/
protected void processConstraintViolation(final Set<ConstraintViolation<Object>> violations,
final SheetBindingErrors<?> errors) {
for(ConstraintViolation<Object> violation : violations) {
final String fieldName = violation.getPropertyPath().toString();
final Optional<FieldError> fieldError = errors.getFirstFieldError(fieldName);
if(fieldError.isPresent() && fieldError.get().isConversionFailure()) {
// 型変換エラーが既存のエラーにある場合は、処理をスキップする。
continue;
}
final ConstraintDescriptor<?> cd = violation.getConstraintDescriptor();
final String[] errorCodes = determineErrorCode(cd);
final Map<String, Object> errorVars = createVariableForConstraint(cd);
final String nestedPath = errors.buildFieldPath(fieldName);
if(Utils.isEmpty(nestedPath)) {
// オブジェクトエラーの場合
errors.createGlobalError(errorCodes)
.variables(errorVars)
.defaultMessage(violation.getMessageTemplate())
.buildAndAddError();
} else {
// フィールドエラーの場合
// 親のオブジェクトから、セルの座標を取得する
final Object parentObj = violation.getLeafBean();
final Path path = violation.getPropertyPath();
Optional<CellPosition> cellAddress = Optional.empty();
Optional<String> label = Optional.empty();
if(Path.class.isAssignableFrom(PathImpl.class)) {
final String pathNodeName = getPathNodeName(path);
cellAddress = new PositionGetterFactory().create(parentObj.getClass(), pathNodeName)
.map(getter -> getter.get(parentObj)).orElse(Optional.empty());
label = new LabelGetterFactory().create(parentObj.getClass(), pathNodeName)
.map(getter -> getter.get(parentObj)).orElse(Optional.empty());
}
// フィールドフォーマッタ
Class<?> fieldType = errors.getFieldType(nestedPath);
if(fieldType != null) {
FieldFormatter<?> fieldFormatter = errors.findFieldFormatter(nestedPath, fieldType);
if(fieldFormatter != null) {
errorVars.putIfAbsent("fieldFormatter", fieldFormatter);
}
}
// 実際の値を取得する
errorVars.putIfAbsent("validatedValue", violation.getInvalidValue());
errors.createFieldError(fieldName, errorCodes)
.variables(errorVars)
.address(cellAddress)
.label(label)
.defaultMessage(violation.getMessageTemplate())
.buildAndAddError();
}
}
}
/**
* BeanValidationのPathの名称を取得する。
* <p>Hibernateのバージョンにより、パッケージが異なるのでリフレクションで取得する。
*
* @param path パス
* @return 名称
*/
private String getPathNodeName(final Path path) {
try {
Method getLeafNodeMethod = PathImpl.class.getMethod("getLeafNode");
Object leafNodeObj = getLeafNodeMethod.invoke(path);
if(leafNodeObj == null) {
return null;
}
Method getNodeNameMethod = NodeImpl.class.getMethod("getName");
Object nodeName = getNodeNameMethod.invoke(leafNodeObj);
return nodeName != null ? nodeName.toString() : null;
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new RuntimeException("fail PathImple.getLeafNode().getName()", e);
}
}
/**
* エラーコードを決定する。
* <p>※ユーザ指定メッセージの場合はエラーコードは空。</p>
*
* @since 2.3
* @param descriptor フィールド情報
* @return エラーコード
*/
protected String[] determineErrorCode(final ConstraintDescriptor<?> descriptor) {
// バリデーション用アノテーションから属性「message」のでデフォルト値を取得し、変更されているかどう比較する。
String defaultMessage = null;
try {
Method messageMethod = descriptor.getAnnotation().annotationType().getMethod("message");
messageMethod.setAccessible(true);
defaultMessage = Objects.toString(messageMethod.getDefaultValue(), null);
} catch (NoSuchMethodException | SecurityException e) {
logger.warn("Fail getting annotation's attribute 'message' for " + descriptor.getAnnotation().annotationType().getSimpleName() , e);
}
if(!descriptor.getMessageTemplate().equals(defaultMessage)) {
/*
* アノテーション属性「message」の値がデフォルト値から変更されている場合は、
* ユーザー指定メッセージとして判断し、エラーコードは空にしてユーザー指定メッセージを優先させる。
*/
return new String[]{};
} else {
// アノテーションのクラス名をもとに生成する。
return new String[]{
descriptor.getAnnotation().annotationType().getSimpleName(),
descriptor.getAnnotation().annotationType().getCanonicalName(),
descriptor.getAnnotation().annotationType().getCanonicalName() + ".message"
};
}
}
/**
* BeanValidationのアノテーションの値を元に、メッセージ変数を作成する。
* @param descriptor
* @return メッセージ変数
*/
protected Map<String, Object> createVariableForConstraint(final ConstraintDescriptor<?> descriptor) {
final Map<String, Object> vars = new HashMap<String, Object>();
for(Map.Entry<String, Object> entry : descriptor.getAttributes().entrySet()) {
final String attrName = entry.getKey();
final Object attrValue = entry.getValue();
// メッセージ変数で必要ないものを除外する
if(EXCLUDE_MESSAGE_ANNOTATION_ATTRIBUTES.contains(attrName)) {
continue;
}
vars.put(attrName, attrValue);
}
return vars;
}
}