SheetBindingErrors.java
package com.gh.mygreen.xlsmapper.validation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Stack;
import java.util.stream.Collectors;
import com.gh.mygreen.xlsmapper.util.PropertyTypeNavigator;
import com.gh.mygreen.xlsmapper.util.PropertyValueNavigator;
import com.gh.mygreen.xlsmapper.util.Utils;
import com.gh.mygreen.xlsmapper.validation.fieldvalidation.FieldFormatter;
import com.gh.mygreen.xlsmapper.validation.fieldvalidation.FieldFormatterRegistry;
import com.github.mygreen.cellformatter.lang.ArgUtils;
/**
* 1シート分のエラー情報を管理するクラス。
*
* @param <P> シートにマッピングするクラスタイプ
* @version 2.0
* @author T.TSUCHIE
*
*/
public class SheetBindingErrors<P> {
/** パスの区切り文字 */
public static final String PATH_SEPARATOR = ".";
/**
* 検証対象のオブジェクト。
* ・ルートオブジェクト
*/
private final P target;
/**
* オブジェクト名
*/
private final String objectName;
/**
* シート名
*/
private String sheetName;
/**
* シートのインデックス
*/
private int sheetIndex = -1;
/**
* 現在のパス。
* キャッシュ用。
*/
private String currentPath;
/**
* 検証対象のオブジェクトの現在のパス
*/
private Stack<String> nestedPathStack = new Stack<>();
/**
* エラーオブジェクト
*/
private final List<ObjectError> errors = new ArrayList<>();
/**
* フィールドの値のフォーマッタの管理クラス
*/
private FieldFormatterRegistry fieldFormatterRegistry = new FieldFormatterRegistry();
/** エラーコードの候補を生成するクラス */
private MessageCodeGenerator messageCodeGenerator = new MessageCodeGenerator();
/**
* プロパティ式から、値を取得する。
* ・private/protectedなどのフィールドにもアクセス可能にする。
*/
private final PropertyValueNavigator propertyValueNavigator = new PropertyValueNavigator();
{
propertyValueNavigator.setAllowPrivate(true);
propertyValueNavigator.setIgnoreNull(true);
propertyValueNavigator.setIgnoreNotFoundKey(true);
propertyValueNavigator.setCacheWithPath(true);
}
/**
* プロパティ式から、クラスタイプを取得する。
* ・private/protectedなどのフィールドにもアクセス可能にする。
*/
private final PropertyTypeNavigator propertyTypeNavigator = new PropertyTypeNavigator();
{
propertyTypeNavigator.setAllowPrivate(true);
propertyTypeNavigator.setIgnoreNotResolveType(true);
propertyTypeNavigator.setCacheWithPath(true);
}
/**
* オブジェクト名を指定しするコンストラクタ。
* <p>エラーメッセージを組み立てる際に、パスのルートとなる。
* @param target 検証対象のオブジェクト
* @param objectName オブジェクト名
*/
public SheetBindingErrors(final P target, final String objectName) {
this.target = target;
this.objectName = objectName;
this.fieldFormatterRegistry.init();
}
/**
* クラス名をオブジェクト名とするコンストラクタ。
* <p>オブジェクト名として、{@link Class#getCanonicalName()}を設定します。</p>
* @param target 検証対象のオブジェクト
* @throws IllegalArgumentException {@link target == null.}
*/
public SheetBindingErrors(final P target) {
this(target, target.getClass().getCanonicalName());
}
/**
* 検証対象のオブジェクトを取得する。
* @return
*/
public P getTarget() {
return target;
}
/**
* 現在のオブジェクト名称を取得する
* @return コンストラクタで設定したオブジェクト名称。
*/
public String getObjectName() {
return objectName;
}
/**
* 現在のシート名を取得する。
* @return シート名称
*/
public String getSheetName() {
return sheetName;
}
/**
* 現在のシート名を設定します。
* @param sheetName シートの名称
*/
public void setSheetName(final String sheetName) {
this.sheetName = sheetName;
}
/**
* シート番号を取得する
* @return 0から始まる。ただし、シートが設定されていない状態の時は、-1を返す。
*/
public int getSheetIndex() {
return sheetIndex;
}
/**
* シート番号を設定する
* @param sheetIndex シート番号(0から始まる)
*/
public void setSheetIndex(int sheetIndex) {
this.sheetIndex = sheetIndex;
}
/**
* 指定したパスのフィールドのクラスタイプを取得する。
* @since 2.0
* @param field フィールド名
* @return クラスタイプ。ただし、リストなどGenericsのタイプが指定されていない場合、クラスタイプもnullとなる。
*/
public Class<?> getFieldType(final String field) {
final String fieldPath = buildFieldPath(field);
Class<?> type = propertyTypeNavigator.getPropertyType(target.getClass(), fieldPath);
if(type != null) {
return type;
}
return getActualFieldType(fieldPath);
}
/**
* 指定したパスのフィールドのクラスタイプを取得する。
* <p>インスタンスを元に取得するため、サブクラスの可能性がある。</p>
* @since 2.0
* @param field フィールド名
* @return クラスタイプ。ただし、オブジェクトの値がnullの場合は、クラスタイプもnullとなる。
*/
public Class<?> getActualFieldType(final String field) {
final Object fieldValue = getFieldValue(field);
return fieldValue == null ? null : fieldValue.getClass();
}
/**
* 指定したパスのフィールドの値を取得する。
* <p>フィールドエラーにエラーが存在するときは、エラーオブジェクトから値を取得し、存在しない場合は、実際の値を取得する。</p>
* @param field フィールド名
* @return フィールドの値。
*/
public Object getFieldValue(final String field) {
final FieldError error = getFirstFieldError(field).orElse(null);
if(error != null && !error.isConversionFailure()) {
return error.getRejectedValue();
} else {
return getFieldActualValue(field);
}
}
/**
* 指定したパスのフィールドの値を取得する。
* @since 2.0
* @param field フィールド名
* @return フィールドの値。
*/
public Object getFieldActualValue(final String field) {
final String fieldPath = buildFieldPath(field);
return propertyValueNavigator.getProperty(target, fieldPath);
}
/**
* 現在のパス上のプロパティの値を取得します。
* <p>{@link #getTarget()}で取得できるルートオブジェクトに対して、{@link #getCurrentPath()}のパスで示された値を取得します。</p>
* @return 現在のパス上の値。
*/
public Object getValue() {
final String currentPath = getCurrentPath();
if(Utils.isEmpty(currentPath)) {
return target;
} else {
return propertyValueNavigator.getProperty(target, currentPath);
}
}
/**
* 指定したパスで現在のパスを初期化します。
* <p>nullまたは空文字を与えると、トップに移動します。
* @param nestedPath ネストするパス
*/
public void setNestedPath(final String nestedPath) {
final String canonicalPath = normalizePath(nestedPath);
this.nestedPathStack.clear();
if(canonicalPath.isEmpty()) {
this.currentPath = buildPath();
} else {
pushNestedPath(canonicalPath);
}
}
/**
* 現在のパスをルートに移動します。
*/
public void setRootPath() {
setNestedPath(null);
}
/**
* パスを正規化する。
* <ol>
* <li>トリムする。</li>
* <li>値がnullの場合は、空文字を返す。</li>
* <li>最後に'.'がついている場合、除去する。</li>
* </ol>
* @param subPath
* @return
*/
private String normalizePath(final String subPath) {
if(subPath == null) {
return "";
}
String value = subPath.trim();
if(value.isEmpty()) {
return value;
}
if(value.startsWith(PATH_SEPARATOR)) {
value = value.substring(1);
}
if(value.endsWith(PATH_SEPARATOR)) {
value = value.substring(0, value.length()-1);
}
return value;
}
/**
* パスを1つ下位に移動します。
* @param subPath ネストするパス
* @throws IllegalArgumentException subPath is empty.
*/
public void pushNestedPath(final String subPath) {
final String canonicalPath = normalizePath(subPath);
ArgUtils.notEmpty(canonicalPath, "canonicalPath");
this.nestedPathStack.push(canonicalPath);
this.currentPath = buildPath();
}
/**
* 配列やリストなどのインデックス付きのパスを1つ下位に移動します。
* @param subPath ネストするパス
* @param index インデックス番号(0から始まります。)
* @throws IllegalArgumentException {@literal subPath is empty or index < 0}
*/
public void pushNestedPath(final String subPath, final int index) {
final String canonicalPath = normalizePath(subPath);
ArgUtils.notEmpty(subPath, "subPath");
ArgUtils.notMin(index, -1, "index");
pushNestedPath(String.format("%s[%d]", canonicalPath, index));
}
/**
* マップなどのキー付きのパスを1つ下位に移動します。
* @param subPath ネストするパス
* @param key マップのキー
* @throws IllegalArgumentException {@literal subPath is empty or key is empty}
*/
public void pushNestedPath(final String subPath, final String key) {
final String canonicalPath = normalizePath(subPath);
ArgUtils.notEmpty(subPath, "subPath");
ArgUtils.notEmpty(key, "key");
pushNestedPath(String.format("%s[%s]", canonicalPath, key));
}
/**
* パスを1つ上位に移動します。
* @return 現在のパスを返しまます。
* @throws IllegalStateException {@literal パスがこれ以上移動できない場合}
*/
public String popNestedPath() {
if(nestedPathStack.isEmpty()) {
throw new IllegalStateException("Cannot pop nested path: no nested path on stack");
}
final String subPath = nestedPathStack.pop();
this.currentPath = buildPath();
return subPath;
}
/**
* 現在パスのスタックに積まれているパスを結合し、1つに組み立てる。
* <p>ルートの時は空文字を返します。</p>
* @return 結合したパス
*/
private String buildPath() {
return Utils.join(nestedPathStack, PATH_SEPARATOR);
}
/**
* 現在のパスを取得します。
* <p>ルートの時は空文字を返します。</p>
* @return 現在のパス
*/
public String getCurrentPath() {
return currentPath;
}
/**
* 現在のパスに引数で指定したフィールド名を追加した値を返す。
* <p>現在のパスが空の場合は、フィールド名を返す。</p>
* @param fieldName フィールド名
* @return フィールド名を追加したパス
*/
public String buildFieldPath(final String fieldName) {
if(Utils.isEmpty(getCurrentPath())) {
return fieldName;
} else {
return Utils.join(new String[]{getCurrentPath(), fieldName}, PATH_SEPARATOR);
}
}
/**
* 全てのエラーをクリアする。
* @since 0.5
*/
public void clearAllErrors() {
this.errors.clear();
}
/**
* エラー情報を追加する
* @param error エラー情報
* @throws IllegalArgumentException {@literal error == null.}
*/
public void addError(final ObjectError error) {
ArgUtils.notNull(error, "error");
this.errors.add(error);
}
/**
* エラー情報を全て追加する。
* @param errors エラー情報
* @throws IllegalArgumentException {@literal errors == null.}
*/
public void addAllErrors(final Collection<ObjectError> errors) {
ArgUtils.notNull(errors, "errors");
this.errors.addAll(errors);
}
/**
* 全てのエラー情報を取得する
* @return 全てのエラー情報
*/
public List<ObjectError> getAllErrors() {
return new ArrayList<>(errors);
}
/**
* エラーがあるか確かめる。
* @return true:エラーがある。
*/
public boolean hasErrors() {
return errors.size() > 0;
}
/**
* グローバルエラーを取得する
* @return エラーがない場合は空のリストを返す
*/
public List<ObjectError> getGlobalErrors() {
return errors.stream()
.filter(e -> !(e instanceof FieldError))
.collect(Collectors.toList());
}
/**
* 先頭のグローバルエラーを取得する。
* @return 存在しない場合は、空を返す。
*/
public Optional<ObjectError> getFirstGlobalError() {
return errors.stream()
.filter(e -> !(e instanceof FieldError))
.findFirst();
}
/**
* グローバルエラーがあるか確かめる。
* @return true:グローバルエラーがある。
*/
public boolean hasGlobalErrors() {
return getFirstGlobalError().isPresent();
}
/**
* グローバルエラーの件数を取得する
* @return エラーの件数
*/
public int getGlobalErrorCount() {
return getGlobalErrors().size();
}
/**
* フィールドエラーを取得する
* @return エラーがない場合は空のリストを返す
*/
public List<FieldError> getFieldErrors() {
return errors.stream()
.filter(e -> e instanceof FieldError)
.map(e -> (FieldError)e)
.collect(Collectors.toList());
}
/**
* 先頭のフィールドエラーを取得する
* @return エラーがない場合は空を返す
*/
public Optional<FieldError> getFirstFieldError() {
return errors.stream()
.filter(e -> e instanceof FieldError)
.map(e -> (FieldError)e)
.findFirst();
}
/**
* フィールドエラーが存在するか確かめる。
* @return true:フィールドエラーを持つ。
*/
public boolean hasFieldErrors() {
return getFirstFieldError().isPresent();
}
/**
* フィールドエラーの件数を取得する。
* @return フィールドエラーの件数
*/
public int getFieldErrorCount() {
return getFieldErrors().size();
}
/**
* パスを指定してフィールドエラーを取得する。
* <p>検索する際には、引数「path」に現在のパス({@link #getCurrentPath()})を付与して処理します。</p>
* @param path 最後に'*'を付けるとワイルドカードが指定可能。
* @return エラーがない場合は空のリストを返す
*/
public List<FieldError> getFieldErrors(final String path) {
final String fullPath = buildFieldPath(path);
return getFieldErrors().stream()
.filter(e -> isMatchingFieldError(fullPath, e))
.collect(Collectors.toList());
}
/**
* パスを指定して先頭のフィールドエラーを取得する。
* <p>検索する際には、引数「path」に現在のパス({@link #getCurrentPath()})を付与して処理します。</p>
* @param path 最後に'*'を付けるとワイルドカードが指定可能。
* @return エラーがない場合は空を返す
*/
public Optional<FieldError> getFirstFieldError(final String path) {
final String fullPath = buildFieldPath(path);
return getFieldErrors().stream()
.filter(e -> isMatchingFieldError(fullPath, e))
.findFirst();
}
/**
* 指定したパスのフィィールドエラーが存在するか確かめる。
* @param path 最後に'*'を付けるとワイルドカードが指定可能。
* @return true:エラーがある場合。
*/
public boolean hasFieldErrors(final String path) {
return getFirstFieldError(path).isPresent();
}
/**
* 指定したパスのフィィールドエラーの件数を取得する。
* @param path 最後に'*'を付けるとワイルドカードが指定可能。
* @return
*/
public int getFieldErrorCount(final String path) {
return getFieldErrors(path).size();
}
/**
* 指定したパスがフィールドエラーのパスと一致するかチェックするかどうか。
* @param path パス
* @param fieldError フィールドエラー
* @return true: 一致する場合。
*/
private boolean isMatchingFieldError(final String path, final FieldError fieldError) {
if (fieldError.getField().equals(path)) {
return true;
}
if(path.endsWith("*")) {
String subPath = path.substring(0, path.length()-1);
return fieldError.getField().startsWith(subPath);
}
return false;
}
/**
* グローバルエラーのビルダーを作成します。
* @param errorCode エラーコード
* @return {@link ObjectError}のインスタンスを組み立てるビルダクラス。
*/
public InternalObjectErrorBuilder createGlobalError(final String errorCode) {
return createGlobalError(new String[]{errorCode});
}
/**
* グローバルエラーのビルダーを作成します。
* @param errorCodes エラーコード。先頭の要素が優先されます。
* @return {@link ObjectError}のインスタンスを組み立てるビルダクラス。
*/
public InternalObjectErrorBuilder createGlobalError(final String[] errorCodes) {
String[] codes = new String[0];
for(String errorCode : errorCodes) {
codes = Utils.concat(codes, generateMessageCodes(errorCode));
}
return new InternalObjectErrorBuilder(this, getObjectName(), codes)
.sheetName(getSheetName());
}
/**
* フィールドエラーのビルダーを作成します。
* @param field フィールドパス。
* @param errorCode エラーコード
* @return {@link FieldError}のインスタンスを組み立てるビルダクラス。
*/
public InternalFieldErrorBuilder createFieldError(final String field, final String errorCode) {
return createFieldError(field, new String[]{errorCode});
}
/**
* フィールドエラーのビルダーを作成します。
* @param field フィールドパス。
* @param errorCodes エラーコード。先頭の要素が優先されます。
* @return {@link FieldError}のインスタンスを組み立てるビルダクラス。
*/
public InternalFieldErrorBuilder createFieldError(final String field, final String[] errorCodes) {
final String fieldPath = buildFieldPath(field);
final Class<?> fieldType = getFieldType(field);
final Object fieldValue = getFieldValue(field);
String[] codes = new String[0];
for(String errorCode : errorCodes) {
codes = Utils.concat(codes, generateMessageCodes(errorCode, fieldPath, fieldType));
}
return new InternalFieldErrorBuilder(this, getObjectName(), fieldPath, codes)
.sheetName(getSheetName())
.rejectedValue(fieldValue);
}
/**
* 型変換失敗時のフィールエラー用のビルダを作成します。
* @param field フィールドパス。
* @param fieldType フィールドのクラスタイプ
* @param rejectedValue 型変換に失敗した値
* @return {@link FieldError}のインスタンスを組み立てるビルダクラス。
*/
public InternalFieldErrorBuilder createFieldConversionError(final String field, final Class<?> fieldType, final Object rejectedValue) {
final String fieldPath = buildFieldPath(field);
final String[] codes = messageCodeGenerator.generateTypeMismatchCodes(getObjectName(), fieldPath, fieldType);
return new InternalFieldErrorBuilder(this, getObjectName(), fieldPath, codes)
.sheetName(getSheetName())
.rejectedValue(rejectedValue)
.conversionFailure(true);
}
/**
* フィールドに対するフォーマッタを登録する。
* @since 2.0
* @param field フィールド名
* @param fieldType フィールドのクラスタイプ
* @param formatter フォーマッタ
*/
public void registerFieldFormatter(final String field, final Class<?> fieldType, final FieldFormatter<?> formatter) {
registerFieldFormatter(field, fieldType, formatter, false);
}
/**
* フィールドに対するフォーマッタを登録する。
* @since 2.0
* @param field フィールド名
* @param fieldType フィールドのクラスタイプ
* @param formatter フォーマッタ
* @param strippedIndex 登録するときにフィールドパスから、インデックス情報を除去するかどうか。
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public void registerFieldFormatter(final String field, final Class<?> fieldType, final FieldFormatter<?> formatter,
final boolean strippedIndex) {
String fieldPath = buildFieldPath(field);
if(strippedIndex) {
// パスからインデックスやキーを削除する
List<String> strippedPaths = new ArrayList<>();
fieldFormatterRegistry.addStrippedPropertyPaths(strippedPaths, "", fieldPath);
if(strippedPaths.size() > 0) {
// 辞書順位並び変えて先頭に来るのが、インデックスを全て削除されたパス
Collections.sort(strippedPaths);
fieldPath = strippedPaths.get(0);
}
}
fieldFormatterRegistry.registerFormatter(fieldPath, (Class)fieldType, (FieldFormatter)formatter);
}
/**
* フィールドとクラスタイプを指定してフォーマッタを取得する。
* @since 2.0
* @param field フィールド名
* @param fieldType フィールドのクラスタイプ
* @return 見つからない場合は、nullを返す。
*/
public <T> FieldFormatter<T> findFieldFormatter(final String field, final Class<T> fieldType) {
String fieldPath = buildFieldPath(field);
return fieldFormatterRegistry.findFormatter(fieldPath, fieldType);
}
public String[] generateMessageCodes(final String code) {
return getMessageCodeGenerator().generateCodes(code, getObjectName());
}
public String[] generateMessageCodes(final String code, final String field) {
return getMessageCodeGenerator().generateCodes(code, getObjectName(), field, null);
}
public String[] generateMessageCodes(final String code, final String field, final Class<?> fieldType) {
return getMessageCodeGenerator().generateCodes(code, getObjectName(), field, fieldType);
}
public MessageCodeGenerator getMessageCodeGenerator() {
return messageCodeGenerator;
}
public void setMessageCodeGenerator(MessageCodeGenerator messageCodeGenerator) {
this.messageCodeGenerator = messageCodeGenerator;
}
/**
* フィールドのフォーマッタの管理クラスを取得する。
* @return
*/
public FieldFormatterRegistry getFieldFormatterRegistry() {
return fieldFormatterRegistry;
}
/**
* フィールドのフォーマッタクラスを設定する。
* @param fieldFormatterRegistry フィールドのフォーマッタの管理クラス
*/
public void setFieldFormatterRegistry(FieldFormatterRegistry fieldFormatterRegistry) {
this.fieldFormatterRegistry = fieldFormatterRegistry;
}
}