6 Eylül 2018 Perşembe

Bean Validation @Constraint Anotasyonu - Custom Validator İçindir

Giriş
Şu satırı dahil ederiz
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
Bu anotasyon @Repeatable ile kullanılabilir.

1. Kendi Anotasyonuma @Constraint Anotasyonu Eklenir
Bu anotasyon metod parametresinde @Valid varsa devreye girer. Jakarta standardı @Constraint anotasyonu üzerinde taşıyan custom anotasyon içinde 3 tane metod istiyor. Bunlar şöyle
// defines Jakarta groups that constraint applies to Class<?>[] groups() default {}; // defines an array of Payload obj associated with the constraint // defaults to [] Class<? extends Payload>[] payload() default {}; // default message that would be thrown in ConstraintValidationException String message() default "Error message";
2. @Constraint Anotasyonu İle Validator Belirtilir
Validator sınıfında iki tane metodu @Override ederek kodlamak gerekiyor. Bunlar
- initialize()
- isValid()
Validator sınıfları Spring tarafından bean olarak yüklenir. Açıklaması şöyle
NOTE: Spring scans for all the classes that extend from ConstraintValidator, and load them in the context as validators, also given this we can inject and access other beans as we normally access in other beans.
validatedBy Alanı
İmzası şöyle
@Target({ ANNOTATION_TYPE }) @Retention(RUNTIME) public @interface Constraint { Class<?extends ConstraintValidator<?, ?>>[] validatedBy(); }
Örnek - Parameter İçin Custom Constraint Anotasyonu
Elimizde şöyle bir kod olsun
@Target({ElementType.PARAMETER}) @Retention(RUNTIME) @Constraint(validatedBy = CountryValidator.class) @Documented public @interface ValidCountryCode { String COUNTRY_CODE_IS_NOT_VALID = "Country code is not valid"; String message() default COUNTRY_CODE_IS_NOT_VALID; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
Validator şöyle olsun
@Component public class CountryValidator implements ConstraintValidator<ValidCountryCode, String> { private static final COUNTRIES = Locale.getISOCountries(); @Override public boolean isValid(String value, ConstraintValidatorContext context) { return !COUNTRIES.stream().noneMatch(element -> element.equals(value)); } }

Örnek - Field İçin Custom Constraint Anotasyonu
Kendi anotasyonumuzu tanımlamak için şöyle yaparız.
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = NotContainsValidator.class) // 1.
public @interface NotContains {
  String[] notAllowValues() default {};
  String message() default "";
  Class<?>[] groups() default {}; // 2.
  Class<? extends Payload>[] payload() default {}; // 3.
}
Açıklaması şöyle
1. The validator used for this constraint, will perform the actual validation and do some additional work on the generation of the message if needed.
2. This is a required field, used to constraint grouping. This is not needed for this tutorial.
3. This is a required field, used to create the Payload and set the severity level for the constraint, this is not used by this validator.
Validator sınıfımız şöyle kodlarız
public class NotContainsValidator implements ConstraintValidator<NotContains,String> {// 1.
  private String[] notAllowValues;

  @Override
  public void initialize(NotContains constraintAnnotation) {. // 2.
    notAllowValues = constraintAnnotation.notAllowValues();
  }

  @Override
  public boolean isValid(String value, ConstraintValidatorContext context) { // 3.
    return !List.of(notAllowValues).contains(value);
  }
}
Açıklaması şöyle
1. The validator should extend from ConstraintValidator.
2. Initializes the validator with any required data set in the Constraint annotation. this method is guaranteed to be called before any method in the Validator class.
3. The method performs the actual validation, for sake of simplicity the isValid performs a simple validation checking the value of the field is not contained in the not notAllowValues attribute.
Daha sonra anotasyonu kendi kodumuza ekleriz
public class GreetingsRequest {

  @NotContains(
    notAllowValues = {"Michael","Bill"},
    message = "The first name is invalid.Following values are not allowed {notAllowValues}"
  )
  private String firstName;

  @NotContains(
    notAllowValues = {"Jackson","Gates"},
    message = "The last name is invalid. Following values are not allowed {notAllowValues}"
  )
  @Length(
    min = 1,
    max = 10,
    message = "The last name must have a length between {min} and {max}"
  )
  private String lastName;
  ...
}
Kullanmak için şöyle yaparız. Eğer validation başarısız olursa, Spring MethodArgumentNotValidException fırlatır. Bunu yakalayan bir exception handler da yazılabilir.
@Controller
@RequestMapping("/greetings")
public class GreetingsController {

  @PostMapping
  public ResponseEntity<String> greetings (@RequestBody @Valid GreetingsRequest request) {
    ...
  }
}
Örnek - Field İçin Custom Constraint Anotasyonu
Kendi anotasyonumuzu tanımlamak için şöyle yaparız. Burada @Constraint anotasyonu içinde kullanılacak validator olan UniqueUsernameValidator belirtiliyor.
@Documented
@Retention(RUNTIME)
@Target({FIELD, ANNOTATION_TYPE, PARAMETER})
@Constraint(validatedBy = UniqueUsernameValidator.class)
@interface UniqueUsername {
   String message() default "{com.domain.user.nonUniqueUsername}";
   Class<?>[] groups() default { };
   Class<? extends Payload>[] payload() default { };
}
Validator sınıfımızı şöyle kodlarız. Validator sınıfımız javax.validation.ConstraintValidator arayüzünden kalıtır.
@Component
public class UniqueUsernameValidator implements 
  ConstraintValidator<UniqueUsername, String> {

  @Autowired
  private UserRepository userRepository;

  @Override
  public boolean isValid(String username, ConstraintValidatorContext context) {
   // implements logic
  }
}
Kullanmak için şöyle yaparız.
@NotBlank
@Size(min = 2, max = 30)
@UniqueUsername
private String username;
Örnek - Class İçin Custom Constraint Anotasyonu
Kendi anotasyonumuzu tanımlamak için şöyle yaparız. Bu arayüzde value şeklinde bir dizi alanı var. Böylece @Repeatable kullanmak zorunda kalmıyoruz
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = NotNullIfAnotherFieldHasValueValidator.class)
@Documented
public @interface NotNullIfAnotherFieldHasValue {
  String fieldName();

  String fieldValue();

  String dependFieldName();

  String message();

  Class<?>[] groups() default {};

  Class<? extends Payload>[] payload() default {};

  @Target({TYPE, ANNOTATION_TYPE})
  @Retention(RUNTIME)
  @Documented
  @interface List {
    NotNullIfAnotherFieldHasValue[] value();
  }
}
Kullanmak için şöyle yaparız
@JsonDeserialize(as = IncomingRequestDto.class)
@NotNullIfAnotherFieldHasValue.List({

  @NotNullIfAnotherFieldHasValue(
    fieldName = "transactionType",
    fieldValue = "Sale",
    dependFieldName = "amountDto.value",
    message = " - amount is mandatory for Sale requests"),

  @NotNullIfAnotherFieldHasValue(
    fieldName = "transactionType",
    fieldValue = "VoidSale",
    dependFieldName = "reversalType",
    message = " - Reversal Type is mandatory for VoidSale requests"),
})
@JsonInclude(JsonInclude.Include.NON_NULL)
public class IncomingRequestDto {

  public TransactionType transactionType;
  public ReversalType reversalType;
  public String reversalId;
  public AmountDto amountDto;
}
Validator kodu şöyledir. Burada Object value sınıfı temsil eden nesnedir.
public class NotNullIfAnotherFieldHasValueValidator
  implements ConstraintValidator<NotNullIfAnotherFieldHasValue, Object> {
  
  private String fieldName;
  private String expectedFieldValue;
  private String dependFieldName;

  @Override
  public void initialize(NotNullIfAnotherFieldHasValue annotation) {
    fieldName = annotation.fieldName();
    expectedFieldValue = annotation.fieldValue();
    dependFieldName = annotation.dependFieldName();
  }

  @Override
  public boolean isValid(Object value, ConstraintValidatorContext ctx) {
    ...
  }
}

Hiç yorum yok:

Yorum Gönder