DSL Validations: The Whole Enchilada
As a conclusion to the series where we have learned to create the building blocks for DSL-based validations, let's put it all together for a complete solution.
Join the DZone community and get the full member experience.
Join For FreeThis is part 4 of a 4-part tutorial
- Part 1: DSL Validations: Properties
- Part 2: DSL Validations: Child Properties
- Part 3: DSL Validations: Operators
- Part 4: DSL Validations: The Whole Enchilada
In this final part of a four-part tutorial, after introducing the concept of property validators and operators, we can tie it all together by validating complete beans in a more reusable way.
PropertyBeanValidator
PropertyBeanValidator
is the worker class that evaluates a collection of PropertyValidator
s against a target object. The specific property validators are provided during construction and are AND'ed together as if wrapped by an AndOperator
(e.g., all property validators must pass for the entire bean to validate successfully).
open class PropertyBeanValidator<T> (
validators: Set<PropertyValidator<T>>) : DefaultBeanValidator() {
override fun <T> validate(
source: T,
vararg groups: Class<*>?): Set<ConstraintViolation<T>> {
// Place to catch all the constraint violations that
// occurred during this validation
val violations = mutableSetOf<ConstraintViolation<T>>()
// Call each individual validator to determine whether
// or not the bean validates correctly
validators
.parallelStream()
.forEach {
it as PropertyValidator<T>; it.validate(source, violations)
}
return violations
}
}
Putting It All Together
We'll reuse the Student
class defined in Part 2, "DSL Validations: Child Properties" (linked in the introduction).
data class Address(
val line1: String?,
val line2: String?
val city: String,
val state: String,
val zipCode: String
)
data class Student(
val studentId: String,
val firstName: String?,
val lastName: String?,
val emailAddress: String?,
val localAddress: Address
)
For this example, we have three business rules to apply against a Student
object:
firstName
andlastName
must both be present or missing;address.line2
presence requires thataddress.line1
is also present;address.zipCode
must be formatted correctly.
Ad-Hoc Bean Validator
Bean validators are created by instantiating PropertyBeanValidator
directly by a factory allowing callers to be provided an appropriate validator without actually knowing what needs to be validated. The factory determines the specific validations required - based on caller, data state, feature flags, etc. - and builds the validator on the fly.
val validators = setOf(
OrOperator(
"studentName",
listOf(
AndOperator(
"namePresent",
listOf(
NotBlankValidator("firstName", Student::firstName),
NotBlankValidator("lastName", Student::lastName)
)
),
AndOperator(
"nameNotPresent",
listOf(
NullOrBlankValidator("firstName", Student::firstName),
NullOrBlankValidator("lastName", Student::lastName)
),
)
"first/last name must both be present or null"
),
OrOperator(
"Line2RequiresLine1",
listOf(
ChildPropertyValidator(
"line1NotNull",
Student::localAddress,
NotBlankValidator("line1", Address::line1)),
ChildPropertyValidator(
"line2Null",
Student::localAddress,
NullOrBlankValidator("line2", Address::line2)),
),
"line2 requires line1."
),
ChildPropertyValidator(
"address.zipCode",
Student::localAddress,
ZipCodeFormatValidator("address", Address::zipCode)
)
)
val validator = PropertyBeanValidator(validators)
Class-Specific Bean Validator
A class-specific validator is useful when there is one and only one way to validate a class and consistent and correct usage across the code base. Here we extend PropertyBeanValidator
and pass in the validators via an alternative constructor.
class StudentBeanValidator (validators: Set<PropertyValidator<Student>>)
: PropertyBeanValidator<Student> (validators) {
constructor() : this(getValidators())
companion object {
fun getValidators() : Set<PropertyValidator<Student>> {
return setOf(
.
.
<same validations as above>
.
.
)
}
}
}
NOTE: It's a little more awkward in Kotlin, as you can't access data in the companion object
before the object is constructed, but calling a method is allowed. Statics in Java would allow the creation of an immutable set that could be used for any number of instantiations.
Validating
// Assume the student is created from a database entry
val myStudent = retrieveStudent("studentId")
// Validate the object
val violations = mutableSetOf<ConstraintViolation<T>>()
validator.validate(myStudent, violations)
// empty collection means successful validation
val successfullyValidated = violations.isEmpty()
Annotation-Based Validation
Jakarta's validation interface ConstraintValidator
declares an annotation-driven validation which, in turn, can be defined via the DSL.
First, implement the annotation that can be applied for validating students, in this example, limited to method parameters.
@Constraint(validatedBy = [StudentValidator::class])
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class ValidStudent(
val message: String = "Invalid Student record",
val groups: Array<KClass<*>> = [],
val payload: Array<KClass<out Payload>> = []
)
Next, implement the class-extending ConstraintValidator
that does the actual validation, using the StudentBeanValidation
implemented earlier.
class StudentValidator : ConstraintValidator<ValidStudent, Student> {
override fun isValid(student: Student,
context: ConstraintValidatorContext): Boolean {
val errors = StudentBeanValidator().validate(student)
return if (errors.isNotEmpty()) {
context.disableDefaultConstraintViolation()
context.buildConstraintViolationWithTemplate(
"Student validation failed with following errors :$errors")
.addConstraintViolation()
false
} else {
true
}
}
}
}
Here is the annotation in action:
fun registerStudentForClass(@ValidStudent student: Student): Student {
.
.
<do some work>
.
.
}
For those interested, this Baeldung tutorial dives deeper into validations than what I've covered.
Final Thoughts
DSL Validations is a language-independent way of checking for bean/object validity without writing ever more if-then-else statements that are uncommented, unclear, and unreadable, and are easy to extend and customize for whatever specific requirements your organization has.
Supporting Code
DefaultBeanValidator
open class DefaultBeanValidator : Validator {
override fun <T> validate(source: T,
vararg groups: Class<*>?)
: Set<ConstraintViolation<T>> {
throw UnsupportedOperationException (EXCEPTION_MESSAGE)
}
override fun <T> validateProperty(source: T,
propertyName: String?,
vararg groups: Class<*>?)
: Set<ConstraintViolation<T>> {
throw UnsupportedOperationException (EXCEPTION_MESSAGE)
}
override fun <T> validateValue(beanType: Class<T>?,
propertyName: String?,
value: Any?, vararg groups: Class<*>?)
: Set<ConstraintViolation<T>> {
throw UnsupportedOperationException (EXCEPTION_MESSAGE)
}
override fun getConstraintsForClass(clazz: Class<*>?): BeanDescriptor {
throw UnsupportedOperationException (EXCEPTION_MESSAGE)
}
override fun <T : Any?> unwrap(type: Class<T>?): T {
throw UnsupportedOperationException (EXCEPTION_MESSAGE)
}
override fun forExecutables(): ExecutableValidator {
throw UnsupportedOperationException (EXCEPTION_MESSAGE)
}
companion object {
const val EXCEPTION_MESSAGE = "Not yet implemented"
}
}
Published at DZone with permission of Scott Sosna. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments