chtefi
9/11/2016 - 11:35 AM

javax.validation JSR-303

javax.validation JSR-303

import java.io.StringReader
import java.util.Properties
import javax.validation.constraints.{Min, NotNull, Pattern, Size}

import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.{DeserializationFeature, ObjectMapper}
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import com.tsukaby.bean_validation_scala.ScalaValidatorFactory

import collection.JavaConversions._
import scala.annotation.meta.field
import scala.reflect.ClassTag

// the bean we are going to create from some simple textual properties
case class MyBean(
                   @(Pattern @field)(regexp = "[abc]+")
                   @JsonProperty("message")
                   msg: Option[String],

                   @(NotNull @field)
                   host: String,

                   @(Min @field)(10)
                   value: Int,

                   @(Size @field)(max = 2)
                   list: Option[List[Int]]
                 )

// OK, I had some fun and modularized a bit
trait ValueConverter[T] {
  def convert(value: String): T
}
class JacksonValueConverter[T : ClassTag]()(implicit mapper: ObjectMapper) extends ValueConverter[T] {
  override def convert(value: String): T = mapper.readValue(value, implicitly[ClassTag[T]].runtimeClass.asInstanceOf[Class[T]])
}

object Main extends App {
  implicit val mapper = new ObjectMapper
  mapper.registerModule(new DefaultScalaModule)
  mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)

  // we are going to convert the values from the raw properties using Jackson deserialization
  implicit val conv = new JacksonValueConverter[Object]

  // string to Map[String, String], just like that!
  implicit def toProps(s: String): Map[String, String] = { val p = new Properties(); p.load(new StringReader(s)); p.toMap }

  // filter Map keys, convert their corresponding value, and use them to create the given Class
  def make[T: ClassTag](prefix: String, p: Map[String, String])(implicit mapper: ObjectMapper, converter: ValueConverter[_]): T = {
    val map = p
        .filter { case (key, value) => key.startsWith(prefix + ".") }
        .map { case (key, value) => (key.substring((prefix + ".").length), converter.convert(p(key))) }

    val c: Class[T] = implicitly[ClassTag[T]].runtimeClass.asInstanceOf[Class[T]]
    mapper.convertValue(map, c)
  }

  // JSR-303
  def validate[T](bean: T) = {
    println("Validating " + bean)

    val validator = ScalaValidatorFactory.validator
    val violations = validator.validate(bean)
    if (violations.isEmpty)
      println("OK!")
    else
      println("KO!\n" + violations.map(x => x.getPropertyPath + " ==> " + x.getMessage).mkString("\n"))
  }

  validate(make[MyBean]("container", """|container.message="abcccc"
                                        |container.value=10
                                        |container.host="pluto"
                                        |ignore.me=1337
                                        |""".stripMargin))

  validate(make[MyBean]("abc", """|abc.message="abcdef"
                                  |abc.value=6
                                  |abc.list=[1, 3, 4]
                                  |""".stripMargin))

/*

Validating MyBean(Some(abcccc),pluto,10,None)
OK!

Validating MyBean(Some(abcdef),null,6,Some(List(1, 3, 4)))
KO!
host ==> may not be null
value ==> must be greater than or equal to 10
msg ==> must match "[abc]+"
list ==> size must be between 0 and 2
*/

}
// JSR-303 and Jackson could be mixed together to validate the deserialized entities

  val mapper = new ObjectMapper
  mapper.registerModule(new DefaultScalaModule)

  case class MyBean(
                     @(Pattern @field)(regexp = "[abc]+")
                     @JsonProperty("message")
                     msg: Option[String],
                   
                     @(NotNull @field)
                     host: String
                   )

  val value: MyBean = mapper.readValue("""{ "message": "abcdef" }""", classOf[MyBean])

  val validator = ScalaValidatorFactory.validator
  println(validator.validate(value))
  
  /* Set(
        ConstraintViolationImpl{interpolatedMessage='must match "[abc]+"', propertyPath=msg, rootBeanClass=class Main$MyBean, messageTemplate='{javax.validation.constraints.Pattern.message}'},
        ConstraintViolationImpl{interpolatedMessage='may not be null', propertyPath=host, rootBeanClass=class Main$MyBean, messageTemplate='{javax.validation.constraints.NotNull.message}'})
  */
// libraryDependencies += "com.wix" %% "accord-core" % "0.6"

import com.wix.accord.dsl._
import com.wix.accord._

object Main extends App {
  case class SubBean(l: List[Int])
  case class MyBean(msg: String, value: Int, sub: SubBean)

  implicit val mySubBeanValidator = validator[SubBean] { b =>
    b.l has size > 2
  }

  implicit val myBeanValidator = validator[MyBean] { b =>
    b.sub as "valid subs" is valid // compilation error if the implicit subbean validator is declared after
    b.msg as "message" is notEmpty
    b.msg as "starts" startsWith "abc"
    b.msg as "abc only" should matchRegexFully("[abc]+")
    b.value as "value" should between (0, 10).exclusive
  }

  println(validate(MyBean("abcabcabca", 5, SubBean(List(1, 2, 3)))))
  // Success
  println(validate(MyBean("abcdef", 5, SubBean(List(1, 2, 3)))))
  // Failure(Set(RuleViolation(abcdef,must fully match regular expression '[abc]+',Explicit(message))))
  println(validate(MyBean("abc", 10, SubBean(List(1, 2, 3)))))
  // Failure(Set(RuleViolation(10,got 10, expected between 0 and 10 (exclusively),Explicit(value))))
  println(validate(MyBean("abcabcabca", 5, SubBean(List(1, 2)))))
  // Failure(Set(GroupViolation(SubBean(List(1, 2)),is invalid,Set(RuleViolation(2,has size 2, expected more than 2,AccessChain(WrappedArray(l)))),Explicit(subs))))
}

/*
libraryDependencies += "javax.validation" % "validation-api" % "1.1.0.Final"
//libraryDependencies += "org.hibernate" % "hibernate-validator" % "5.2.4.Final"
libraryDependencies += "javax.el" % "javax.el-api" % "3.0.0"
libraryDependencies += "org.glassfish" % "javax.el" % "3.0.0"
libraryDependencies += "com.tsukaby" %% "bean-validation-scala" % "0.4.0"

For Scala stuff (Option, Seq, etc.), we need to use another Validator:
libraryDependencies += "com.tsukaby" %% "bean-validation-scala" % "0.4.0"

Otherwise, things like Option[String] can't be validated:
Exception in thread "main" javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint 'javax.validation.constraints.Size' validating type 'scala.Option<java.lang.String>'. Check configuration for 'msg'
With bean-validation-scala, it can!

*/

import javax.validation.constraints.{Pattern, Size}

import com.tsukaby.bean_validation_scala.ScalaValidatorFactory

import scala.annotation.meta.field

object Main extends App {
  case class Item(name: String)
  case class MyBean(@(Pattern @field)(regexp = "[abc]+")  @(Size @field)(min = 10) msg: Option[String])

  val validator = ScalaValidatorFactory.validator
  println(validator.validate(MyBean(Some("abcabcabca"))))
  // Set()
  println(validator.validate(MyBean(Some("abee"))))
  // Set(ConstraintViolationImpl{interpolatedMessage='must match "[abc]+"', propertyPath=msg, rootBeanClass=class Main$MyBean, messageTemplate='{javax.validation.constraints.Pattern.message}'}, ConstraintViolationImpl{interpolatedMessage='size must be between 10 and 2147483647', propertyPath=msg, rootBeanClass=class Main$MyBean, messageTemplate='{javax.validation.constraints.Size.message}'})
  println(validator.validate(MyBean(None)))
  // Set()
}

/*
libraryDependencies += "javax.validation" % "validation-api" % "1.1.0.Final"
libraryDependencies += "org.hibernate" % "hibernate-validator" % "5.2.4.Final"
libraryDependencies += "javax.el" % "javax.el-api" % "3.0.0"
libraryDependencies += "org.glassfish" % "javax.el" % "3.0.0"

On its own, the validation-api is useless, it does not contain any implementation (à la slf4j).
This is why hibernate-validator is necessary.
// Otherwise: Exception in thread "main" javax.validation.ValidationException: Unable to create a Configuration, because no Bean Validation provider could be found. Add a provider like Hibernate Validator (RI) to your classpath.

Then itself needs some EL language dependencies, javax.el (not in the JRE)
// Otherwise: Exception in thread "main" javax.validation.ValidationException: HV000183: Unable to load 'javax.el.ExpressionFactory'. Check that you have the EL dependencies on the classpath, or use ParameterMessageInterpolator instead

Then, for more complex annotations, javax.el can be necessary (not always, but if you use @CreditCardNumber, then it is)
// Otherwise: Exception in thread "main" java.lang.ExceptionInInitializerError ... Caused by: javax.el.ELException: Provider com.sun.el.ExpressionFactoryImpl not found 
*/

import javax.validation.Validation
import javax.validation.constraints.Size

import scala.annotation.meta.field

object Main extends App {
  case class MyBean(@(Size @field)(min = 10) msg: String) // Notice the meta annotation to define @Size on the field
  val validator = Validation.buildDefaultValidatorFactory().getValidator
  println(validator.validate(MyBean("toto")))
}

/*
sept. 11, 2016 1:29:26 PM org.hibernate.validator.internal.util.Version <clinit>
INFO: HV000001: Hibernate Validator 5.2.4.Final
[ConstraintViolationImpl{interpolatedMessage='size must be between 10 and 2147483647', propertyPath=msg, rootBeanClass=class Main$MyBean, messageTemplate='{javax.validation.constraints.Size.message}'}]
*/