CellField.java

package com.gh.mygreen.xlsmapper.validation.fieldvalidation;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import com.gh.mygreen.xlsmapper.fieldaccessor.LabelGetterFactory;
import com.gh.mygreen.xlsmapper.fieldaccessor.PositionGetterFactory;
import com.gh.mygreen.xlsmapper.util.ArgUtils;
import com.gh.mygreen.xlsmapper.util.CellPosition;
import com.gh.mygreen.xlsmapper.validation.FieldError;
import com.gh.mygreen.xlsmapper.validation.FieldErrorBuilder;
import com.gh.mygreen.xlsmapper.validation.SheetBindingErrors;


/**
 * 1つの項目(フィールド)に対する入力値チェックをするためのクラス。
 * <p>型変換などにおけるバインドエラーなどはシートの読み込み時に行われます。
 * <p>必須エラーのメッセージキーは、「fieldError.required」。
 *
 * @version 0.5
 * @author T.TSUCHIE
 * @param <T> チェック対象の値のタイプ
 *
 */
public class CellField<T> {

    private static final PositionGetterFactory positionGetterFactory = new PositionGetterFactory();
    private static final LabelGetterFactory labelGetterFactory = new LabelGetterFactory();

    private final SheetBindingErrors<?> errors;

    /**
     * フィールドの名称
     */
    private final String fieldName;

    /**
     * フィールドが定義されているBeanクラスのインスタンス
     */
    private Object beanObj;

    /**
     * フィールドの値
     */
    private T fieldValue;

    /**
     * フィールドのJavaBean上のパス
     */
    private String fieldPath;

    /**
     * セルの位置情報。
     * Beanに定義されたプロパティ情報から取得する。
     * 定義されていない場合は、nullが設定される。
     */
    private CellPosition position;

    /**
     * セルのラベル情報
     * Beanに定義されたプロパティ情報から取得する。
     * 定義されていない場合は、nullが設定される。
     */
    private String label;

    /**
     * 必須かどうか
     */
    private boolean required;

    private List<FieldValidator<T>> validators;

    private FieldFormatter<T> formatter;

    private Class<T> fieldType;

    /**
     * 指定されたフィールドの名称に対応するオブジェクトを構築します。
     * @param fieldName フィールドの名称。現在のBeanに対するフィールドの相対パスを指定します。
     * @param errors エラー情報
     */
    public CellField(final String fieldName, final SheetBindingErrors<?> errors) {

        ArgUtils.notEmpty(fieldName, "fieldName");
        ArgUtils.notNull(errors, "errors");

        this.fieldName = fieldName;
        this.errors = errors;

        init();
    }

    @SuppressWarnings("unchecked")
    private void init() {

        this.beanObj = errors.getValue();
        this.fieldValue = (T)errors.getFieldValue(fieldName);
        this.fieldType = (Class<T>)errors.getFieldType(fieldName);
        this.fieldPath = errors.buildFieldPath(fieldName);

        Optional<CellPosition> position = positionGetterFactory.create(beanObj.getClass(), fieldName)
                .map(getter -> getter.get(beanObj)).orElse(Optional.empty());
        position.ifPresent(p -> setPosition(p));

        Optional<String> label = labelGetterFactory.create(beanObj.getClass(), fieldName)
                .map(getter -> getter.get(beanObj)).orElse(Optional.empty());
        label.ifPresent(l -> setLabel(l));

        this.required = false;
        this.validators = new ArrayList<>();

        this.formatter = errors.findFieldFormatter(fieldName, fieldType);
    }

    /**
     * 値が必須かの設定を行う。
     * @param required 必須チェックを行いたい場合、「true」を設定する。
     * @return 自身のインスタンス。メソッドチェーンで記述する。
     */
    public CellField<T> setRequired(final boolean required) {
        this.required = required;
        return this;
    }

    /**
     * 値が必須かチェックを行うかどうか。
     * @return true: 必須入力チェックを行う。
     *               初期値は、非必須(オプション)です。
     */
    public boolean isRequired() {
        return required;
    }

    /**
     * {@link FieldValidator} を追加する。
     * @param validator validatorのインスタンス。
     * @return 自身のインスタンス。
     * @throws NullPointerException validator is null.
     */
    public CellField<T> add(final FieldValidator<T> validator) {
        ArgUtils.notNull(validator, "validator");
        this.validators.add(validator);

        return this;
    }

    /**
     * 複数の{@link FieldValidator} を追加する。
     * @param validators 複数のvalidatorのインスタンス。
     * @return 自身のインスタンス。
     * @throws NullPointerException validator is null.
     */
    public CellField<T> add(final List<FieldValidator<T>> validators) {

        if(validators.isEmpty()) {
            return this;
        }

        for(FieldValidator<T> validator : validators) {
            add(validator);
        }
        return this;
    }

    /**
     * 現在の{@link FieldValidator}を取得する。
     * @return 現在設定されている{@link FieldValidator}。
     */
    public List<FieldValidator<T>> getValidators() {
        return validators;
    }

    /**
     * グループなどのヒントを指定して、入力値の検証を行う。
     * <p>判定結果は、{@link #hasErrors()}で確認します。</p>
     * <p>型変換エラーなどが既に存在するときには、処理は終了します。</p>
     *
     * @param groups 検証するときのヒントとなるグループ。
     * @return 自身のインスタンス。
     */
    public CellField<T> validate(final Class<?>... groups) {

        // 既に型変換エラーなどがある場合、値が設定されていないため処理を終了します。
        if(hasErrors()) {
            return this;
        }

        // 必須チェック
        if(!validateForRequired()) {
            return this;
        }

        final List<Class<?>> hints = Arrays.asList(groups);

        if(getValidators() != null && !getValidators().isEmpty()) {
            for(FieldValidator<T> validator : getValidators()) {
                if(!validator.validate(this, hints)) {
                    return this;
                }
            }
        }

        return this;

    }

    /**
     * 必須エラーのメッセージキーを取得する。
     * <p>キー名は、「fieldError.required」。
     * @return
     */
    protected String getMessageKeyRequired() {
        return "cellFieldError.required";
    }

    /**
     * 必須チェックを行う。
     * @return trueの場合、必須エラーでない。
     */
    protected boolean validateForRequired() {

        if(isRequired() && isInputEmpty()) {
            errors.createFieldError(fieldName, getMessageKeyRequired())
                .address(getPosition())
                .label(getLabel())
                .variables("validatedValue", getValue())
                .buildAndAddError();
            return false;
        }

        return true;

    }

    /**
     * エラーを追加する。
     * @param errorCode エラーコード
     */
    public void rejectValue(final String errorCode) {
        rejectValue(errorCode, Collections.emptyMap());
    }

    /**
     * エラーを追加する
     * @param errorCode エラコード
     * @param variables エラーメッセージ中の変数
     */
    public void rejectValue(final String errorCode, final Map<String, Object> variables) {

        final String codes[] = errors.generateMessageCodes(errorCode, fieldPath, fieldType);

        final FieldError error = new FieldErrorBuilder(errors.getObjectName(), fieldPath, codes)
            .sheetName(errors.getSheetName())
            .rejectedValue(fieldValue)
            .variables(variables)
            .address(position)
            .label(label)
            .build();

        errors.addError(error);
    }

    /**
     * フィールドの値が空かどうか。
     * <p>値がnullまたは、文字列の場合空文字のとき、空と判定する。
     * @return trueの場合、値は空。
     */
    public boolean isInputEmpty() {
        if(fieldValue == null || fieldValue.toString().isEmpty()) {
            return true;
        }

        return false;
    }

    /**
     * フィールドに対してエラーが存在するかどうか。
     * @return trueの場合、エラーが存在する。
     */
    public boolean hasErrors() {
        return errors.hasFieldErrors(fieldName);
    }

    /**
     * フィールドに対してエラーが存在しなかどうか。
     * @return trueの場合、エラーが存在しない。
     */
    public boolean hasNotErrors() {
        return !hasErrors();
    }

    /**
     * エラー情報を取得する。
     * @return エラー情報
     */
    public SheetBindingErrors<?> getBindingErrors() {
        return errors;
    }

    /**
     * フィールドの名称を取得する
     * @return フィールドの名称
     */
    public String getFieldName() {
        return fieldName;
    }

    /**
     * フィールドの値を取得する。
     * @return フィールドの値。
     */
    public T getValue() {
        return fieldValue;
    }

    /**
     * フィールドのクラスタイプを取得する。
     * @return クラスタイプ
     */
    public Class<T> getType() {
        return fieldType;
    }

    /**
     * フィールドのJavaBean上のパスを取得する。
     * @return フィールドのJavaBean上のパス
     */
    public String getFieldPath() {
        return fieldPath;
    }

    /**
     * セルの位置情報を取得します。
     * <p>位置情報の取得用のフィールドやメソッドがbeanに定義されている場合は、コンストラクタの呼び出し時に設定されています。</p>
     * @return 自身のインスタンス。
     */
    public CellPosition getPosition() {
        return position;
    }

    /**
     * セルの位置情報を設定します。
     * @param position セルの位置情報
     */
    public void setPosition(CellPosition position) {
        this.position = position;
    }

    /**
     * セルのラベル情報を取得します。
     * <p>ラベル情報の取得用のフィールドやメソッドがbeanに定義されている場合は、コンストラクタの呼び出し時に設定されています。</p>
     * @return 自身のインスタンス。
     */
    public String getLabel() {
        return label;
    }

    /**
     * セルのラベル情報を設定します。
     * @param label セルのラベル情報
     */
    public void setLabel(String label) {
        this.label = label;
    }

    /**
     * フォーマッタを取得する。
     * @return フォーマッタ。
     *         デフォルトでは、フィールドのクラスタイプ、付与されたアノテーションを元にしたもの。
     *
     */
    public FieldFormatter<T> getFormatter() {
        return formatter;
    }

    /**
     * フォーマッタを設定する。
     * @param formatter
     */
    public void setFormatter(FieldFormatter<T> formatter) {
        this.formatter = formatter;
    }
}