きり丸の技術日記

技術検証したり、資格等をここに残していきます。

【Java】自作アノテーションで特定の文字用の自作バリデーションを行う

きり丸アドベントカレンダー2020の13記事目です。だんだんToDoアプリケーションは関係なくなってきます。


対向システムに情報を渡すときに、データに取り扱い不可能な文字が紛れていることがあります。たとえば髙(はしごだか)であったり、ハイフンだったり、SJIS第一水準、第二水準のみ許容していたり。

今回の記事ではハイフンに似た文字のうち、全角の長音と全角のマイナスのみ許容するようにします。

ハイフンに似た記号


こうして目でみると、違いはわかりづらいですね…。

文字 UTF-8 Unicode 説明
- 2D U+002D ASCIIのハイフン
E383BC U+30FC 全角の長音
E28090 U+2010 別のハイフン
E28091 U+2011 改行しないハイフン
E28093 U+2013 ENダッシュ
E28094 U+2014 EMダッシュ
E28095 U+2015 全角のダッシュ
E28892 U+2212 全角のマイナス
EFBDB0 U+FF70 半角カナの長音

参考リンク
https://qiita.com/ryounagaoka/items/4cf5191d1a2763667add

ゴール

  • 特定の文字が含まれている場合は登録できないようにする

下記画像は「-bbbb」の「-」が含まれているからNGになっている。

f:id:nainaistar:20201129230646p:plain
追加ボタン押す前
f:id:nainaistar:20201129230701p:plain
追加ボタン押した後

環境

手順

自作アノテーションを作成する


アノテーションを自作します。作成したアノテーションを後で変数に付与します。

ファイル名:PrintableCharValid.java

@Documented
@Constraint(validatedBy = {PrintableValidator.class}) // 実際にValidationするクラス
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PrintableCharValid {
  String message() default "印刷不可能な文字が含まれています。";
  Class<?>[] groups() default {};
  Class<? extends Payload>[] payload() default {};

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

ログに出力されるメッセージ。

2020-11-29 22:20:22.305  WARN 2856 --- [nio-8080-exec-7] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.validation.BindException: org.spr
ingframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'todoDto' on field 'action': rejected value [‐aaaa]; codes [PrintableCharValid.todoDto.action,PrintableCharValid.action,PrintableCharValid.j
ava.lang.String,PrintableCharValid]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [todoDto.action,action]; arguments []; de
fault message [action]]; default message [印刷不可能な文字が含まれています。]]

自作アノテーションの項目は以前まとめたので、こちらの記事も観ていただけるとうれしいです。

【Spring】TestExecutionListnerを継承した自作アノテーションでSpringのテストセットアップを快適にする nainaistar.hatenablog.com

実際にバリデーションする処理を追加する


ConstraintValidatorを実装したクラスを作成します。

NG文字が含まれているかどうかは、一度に比較できないので文字を1つ1つ取り出してから比較する必要があります。ですので、toCharArray()メソッドでcharのリストにします。

Unicodeで比較する必要はありませんが、上記のハイフンに似た記号リストを見て分かるとおり、目では文字の違いが分かりづらいです。ですので、なぜNGだったのかを伝えるためにも、Unicodeの方が伝わりやすいです。

なお、1文字ずつ比較している関係で性能は線形探索になってしまうので、性能が気になったら別のロジックを使用してください。

ファイル名:PrintableValidator.java

public class PrintableValidator implements ConstraintValidator<PrintableCharValid, String> {
  @Override
  public void initialize(PrintableCharValid constraintAnnotation) {
  }

  @Override
  public boolean isValid(String value, ConstraintValidatorContext context) {
    if (value == null) {
      return true;
    }
    // 文字を1つ1つ取り出す
    for (char c : value.toCharArray()) {
      // 文言をunicodeに変換します。
      final String unicode = Integer.toHexString(c);
      // NGリストに実装しているメソッドでチェックする
      if (PrintableEnum.checkIncludeIllegalChar(unicode)){
        return false;
      }
    }
    return true;
  }
}

NG文字リストを作成する


NG文字リストを作成します。

ファイル名:PrintableEnum.java

@AllArgsConstructor
@Getter
public enum PrintableEnum {
  A("-", "2d"),
//  B("ー", "30fc"), // 長音は許容する
  C("‐", "2010"),
  D("‑", "2011"),
  E("–", "2013"),
  F("—", "2014"),
  G("―", "2015"),
//  H("−", "2212"), // 全角マイナスも許容する
  J("ー", "ff70");
  private final String character;
  private final String unicode;


  public static boolean checkIncludeIllegalChar(String unicode) {
    for (var value : PrintableEnum.values()) {
      if (value.unicode.equals(unicode)) {
        return true;
      }
    }
    return false;
  }
}

テストコードで確認する


実行のたびにValidatorを初期化します。

生成したValidatorにテスト用のクラスを渡すことで、結果を確認します。クラスの変数に今回生成した@PrintableCharValidを付与することでテストできます。

ファイル名:PrintableValidatorTests.java

class PrintableValidatorTests {

  private Validator validator;

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

  @ArgumentsSource(Target.class)
  @ParameterizedTest
  void printableTest(Target target) {
    TestBean testBean = new TestBean(target.string);
    Set<ConstraintViolation<TestBean>> violations = validator.validate(testBean);
    assertThat(violations.isEmpty()).isEqualTo(target.expected);
  }

  static class Target implements ArgumentsProvider {
    private String string;
    private boolean expected;
    Target(){
    }

    Target(String string, boolean expected){
      this.string = string;
      this.expected = expected;
    }

    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) throws Exception {
      return Stream.of(
          new Target("-", false),
          new Target("ー", true),
          new Target("‐", false),
          new Target("‑", false),
          new Target("–", false),
          new Target("—", false),
          new Target("―", false),
          new Target("−", true),
          new Target("ー", false)
      ).map(Arguments::of);
    }
  }

  private static class TestBean {
    @PrintableCharValid
    private String targetStr;

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

今回は似たような文言でテストをしたいので、ParameterizedTestを使用します。ParameterizedTestも以前に記事にまとめたので、下記記事も見ていただけるとうれしいです。 nainaistar.hatenablog.com

ソースコード

アドベントカレンダー13日目。
github.com

終わりに

自作アノテーション(カスタムアノテーション)を作れることでかなり便利になります。何が便利かというと、Dtoアノテーションを付与しているのでDtoを見るだけで仕様が分かるからです。これが別の箇所でバリデーションを行っていると、処理を追いづらくなってしまいます。

仕様書と実際の仕様が異なることはよくあるので、できるかぎり一致している状況を作れるということに価値があります。

ぜひ、自作アノテーションを使用することも考えてみてください。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

参考記事

敬称略。

Java】特定の文字だけを弾くCustom Validationを作る ito-u-oti.com

ハイフンに似てる文字の文字コード qiita.com

類似記事

きり丸アドベントカレンダー2020 adventar.org

きり丸のHerokuページ
https://kirimaru-todoapp.herokuapp.com/

【Spring】TestExecutionListnerを継承した自作アノテーションでSpringのテストセットアップを快適にする nainaistar.hatenablog.com

14日目のアドベントカレンダーの記事 https://nainaistar.hatenablog.com/entry/2020/12/14/083000nainaistar.hatenablog.com

f:id:nainaistar:20201013111905p:plain