CustomMessageInterpolator.java
package com.github.mygreen.messageformatter.beanvalidation;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import javax.validation.Path;
import javax.validation.metadata.ConstraintDescriptor;
import org.hibernate.validator.internal.engine.MessageInterpolatorContext;
import org.springframework.context.MessageSource;
import org.springframework.context.NoSuchMessageException;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.util.StringUtils;
import org.springframework.validation.DefaultMessageCodesResolver;
import org.springframework.validation.MessageCodesResolver;
import com.github.mygreen.messageformatter.MessageInterpolator;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
/**
* {@link MessageInterpolator}とBeanValidationの{@link javax.validation.MessageInterpolator}をブリッジする。
* <p>BeanValidationのメッセージ処理をカスタマイズするために利用する。</p>
* <p>BeanValidationのHibernateValidatorの実装と比べ、追加で以下の変数がメッセージ中で利用が可能。</p>
* <ul>
* <li>{@code propertyPath} : エラーとなったプロパティのパス。ネストしている場合は、{@code address.tel}のようにドットで区切られる。</li>
* <li>{@code propertyName} : エラーとなったプロパティの名称。メッセージソースからプロパティ名を取得したもの。メッセージソースに定義されていなければ値は{@code null}となる。</li>
* </ul>
*
* @author T.TSUCHIE
*
*/
@RequiredArgsConstructor
public class CustomMessageInterpolator implements javax.validation.MessageInterpolator {
/**
* メッセージソース。
*/
@Getter
private final MessageSource messageSource;
/**
* パラメータ付きのメッセージのフォーマッタ。
*/
@Getter
private final MessageInterpolator messageInterpolator;
/**
* プロパティ名のメッセージコードの候補を生成する。
*/
@Setter
@Getter
private MessageCodesResolver messageCodeResolver = new DefaultMessageCodesResolver();
/**
* プロパティ名のメッセージコードの候補をを生成するときのコード名。
* デフォルトは、{@code propertyName} です。
* {@code null} を設定すると、コード名は付与されません。
*/
@Getter
@Setter
private String propertyNameCode = "propertyName";
@Override
public String interpolate(final String messageTemplate, final Context context) {
return messageInterpolator.interpolate(messageTemplate,
createMessageVariables(context, Locale.getDefault()),
0, new MessageSourceAccessor(messageSource));
}
@Override
public String interpolate(final String messageTemplate, final Context context, final Locale locale) {
return messageInterpolator.interpolate(messageTemplate,
createMessageVariables(context, locale),
0, new MessageSourceAccessor(messageSource, locale));
}
/**
* メッセージ中で利用可能な変数を作成する
* @param context コンテキスト
* @param locale ロケール
* @return メッセージ変数のマップ
*/
protected Map<String, Object> createMessageVariables(final Context context, final Locale locale) {
final Map<String, Object> vars = new HashMap<>();
if(context instanceof MessageInterpolatorContext) {
MessageInterpolatorContext mic = (MessageInterpolatorContext)context;
vars.putAll(mic.getMessageParameters());
// プロパティの情報を設定する
Path path = mic.getPropertyPath();
vars.put("propertyPath", path.toString());
resolvePropertyName(mic, locale).ifPresent(name -> vars.put("propertyName", name));
}
final ConstraintDescriptor<?> descriptor = context.getConstraintDescriptor();
for(Map.Entry<String, Object> entry : descriptor.getAttributes().entrySet()) {
final String attrName = entry.getKey();
final Object attrValue = entry.getValue();
vars.put(attrName, attrValue);
}
// 検証対象の値
vars.computeIfAbsent("validatedValue", key -> context.getValidatedValue());
// デフォルトのメッセージ
final String defaultCode = String.format("%s.message", descriptor.getAnnotation().annotationType().getCanonicalName());
final String defaultMessage = messageSource.getMessage(defaultCode, null, locale);
vars.put(defaultCode, defaultMessage);
return vars;
}
/**
* プロパティの名称をメッセージソースから解決する。
* <p>{@link MessageCodesResolver} で生成したコードを元に取得する。
* @param context コンテキスト
* @param locale ロケール
* @return プロパティの名称。解決できない場合は空を返す。
*/
protected Optional<String> resolvePropertyName(final MessageInterpolatorContext context, final Locale locale) {
final String objectName = context.getRootBeanType().getSimpleName();
final String field = context.getPropertyPath().toString();
final Class<?> fieldType = context.getValidatedValue() != null ? context.getValidatedValue().getClass() : null;
String[] codes = messageCodeResolver.resolveMessageCodes(propertyNameCode, objectName, field, fieldType);
for(String code : codes) {
try {
String result = messageSource.getMessage(code, null, locale);
if(StringUtils.hasLength(result)) {
return Optional.of(result);
}
} catch(NoSuchMessageException e) {
/*
* MessageSource#setUseCodeAsDefaultMessageの設定によって、
* 対応するコードが見つからない場合、例外が発生するか戻り値がnullになったりする。
*/
continue;
}
}
return Optional.empty();
}
}