【Java】特定の文字だけを弾くCustom Validationを作る

文字をUnicodeに変換してEnumに持たせている対象文字のUnicodeと比べ、一致していたらValidationエラーにするという自作Valid用Annotationを作りました。

例では、「」(はしご高)が文字列に含まれていた場合に、Validationエラーとして弾いています。

ちなみに、この「」はIBM拡張漢字です。

実装

SpecificCharValid.java

/**
 * 文字列の中に、特定の文字が含まれていた場合はバリデーションエラーを返すAnnotation <br>
 * どの文字が対象となるかは{@link SpecificCharEnum}参照
 */
@Documented
@Constraint(validatedBy = {SpecificCharValidator.class})
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SpecificCharValid {

    String message() default "The characters that cannot be used are included.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    @Target({ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @interface List {
        SpecificCharValid[] value();
    }
}

これで、@SpecificCharValidアノテーションとして、フィールドに適用することができます。

@Target({ElementType.FIELD})でオブジェクトのフィールドに適用できるようにします。

ここでは、アノテーションとして宣言する文字列を設定する程度な記述です。
実際の処理は次のクラスで行います。

SpecificCharValidator.java

@Slf4j
public class SpecificCharValidator implements ConstraintValidator<SpecificCharValid, String> {

    @Override
    public void initialize(SpecificCharValid constraintAnnotation) {
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) {
            return true;
        }

        char[] chars = value.toCharArray();
        for (char c : chars) {
            String unicodeStr = Integer.toHexString(c);
            if (SpecificCharEnum.isSpecificChar(unicodeStr)) {
                String targetStr = ((ConstraintValidatorContextImpl) context)
                        .getConstraintViolationCreationContexts().get(0).getPath().toString();
                log.warn("検知対象の文字が含まれています。対象項目:{} 値:「{}」", targetStr, value);
                return false;
            }
        }
        return true;
    }
}

@SpecificCharValidアノテーションの実装クラスです。

ConstraintValidatorをimplementsしています。

isValid()をOverrideしていて、trueならエラー無し、falseならエラー有りとなります。

Enumが持っているUnicodeと、与えられた文字列に含まれる文字のUnicodeを一つずつ検証しています。

検証対象の文字列は引数のvalueとしてそのまま受け取るのですが、「そのvalueがどこのフィールドに入っていたのか」というフィールドの情報はConstraintValidatorContextに格納されており、上の実装のようにConstraintValidatorContextImplにキャストすることで取り出すことができます。

今回は、エラーとなった時にフィールド名も一緒にログで出力できるようにしました。

SpecificCharEnum.java

/**
 * 検知対象文字のEnum
 */
@Slf4j
public enum SpecificCharEnum {

    SPECIFIC_CHAR_ENUM_1("髙","9ad9");


    private final String specificChar;
    private final String unicodeStr;

    SpecificCharEnum(String specificChar, String unicodeStr) {
        this.specificChar = specificChar;
        this.unicodeStr = unicodeStr;
    }

    public String getUnicodeStr() {
        return unicodeStr;
    }

    public String getSpecificChar() {
        return specificChar;
    }

    public static boolean isSpecificChar(String targetUnicode){
        for (SpecificCharEnum specificCharEnum: SpecificCharEnum.values()){
            if (specificCharEnum.getUnicodeStr().equals(targetUnicode)){
                log.warn("検知対象の文字を検知しました。対象:{}",specificCharEnum.getSpecificChar());
                return true;
            }
        }
        return false;
    }
}

エラーで弾く対象となる文字をEnumで持っています。

Unicode情報を持っており、isSpecificCharで与えられたUnicodeと各EnumのUnicodeを比較しています。

テスト

SpecificCharValidatorTests.java

class SpecificCharValidatorTests {

    private Validator validator;

    @BeforeEach
    void setUp() {
        validator = Validation.buildDefaultValidatorFactory().getValidator();
    }

    @MethodSource("testStrList")
    @ParameterizedTest
    void specificCharTest(String targetStr, boolean expect) {
        TestBean testBean = new TestBean(targetStr);
        Set<ConstraintViolation<TestBean>> violations = validator.validate(testBean);
        assertThat(violations.isEmpty()).isEqualTo(expect);
    }

    static Stream<Arguments> testStrList() {
        return Stream.of(
                Arguments.arguments("", true),
                Arguments.arguments(null, true),
                Arguments.arguments("夏が近づき太平洋高気圧が勢力を増すようになると", true),
                Arguments.arguments("夏が近づき太平洋髙気圧が勢力を増すようになると", false),
                Arguments.arguments("髙等学校", false),
                Arguments.arguments("高等学校", true)
        );
    }

    private static class TestBean {
        @SpecificCharValid
        private String targetStr;

        TestBean(String targetStr) {
            this.targetStr = targetStr;
        }
    }
}

バリデーションエラーに引っ掛かった時のログとしてはこんな感じに出ます。

16:14:27.851 [main] WARN com.example.demo.validator.SpecificCharEnum - 制御対象の文字を検知しました。対象:髙
16:14:27.851 [main] WARN com.example.demo.validator.SpecificCharValidator - 検知対象の文字が含まれています。対象項目:targetStr 値:「髙等学校」

その他

文字のUnicodeを探すときに参考になるサイト

0g0.org

グリフウィキ

プログラム書いて出力してもらう

また、対象の文字が分かっているのであればプログラムでUnicodeを出力してしまうのもアリ。

上のEnumに追加する想定でUnicodeを出力するプログラムを書いてみる。

    @Disabled
    @Test
    void outputUnicode(){
        //検知対象にしたい文字をListに追加する。
        List<String> strList = List.of("髙", "あ", "い", "①");

        for(int i=0;i<strList.size(); i++){
            char[] charArray = strList.get(i).toCharArray();
            for (char c: charArray){
                System.out.println("SPECIFIC_CHAR_ENUM_"+
                        (i+1)+"(\""+strList.get(i)+"\",\""
                        +Integer.toHexString(c)+"\"),");
            }
        }
    }

出力結果

SPECIFIC_CHAR_ENUM_1("髙","9ad9"),
SPECIFIC_CHAR_ENUM_2("あ","3042"),
SPECIFIC_CHAR_ENUM_3("い","3044"),
SPECIFIC_CHAR_ENUM_4("①","2460"),

Process finished with exit code 0

プログラミングカテゴリの最新記事