きり丸アドベントカレンダー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になっている。
環境
- Java
- 15
手順
自作アノテーションを作成する
アノテーションを自作します。作成したアノテーションを後で変数に付与します。
ファイル名: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
類似記事
きり丸アドベントカレンダー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