PageRenderTime 44ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/README.markdown

http://github.com/geofflane/grails-constraints
Markdown | 316 lines | 230 code | 86 blank | 0 comment | 0 complexity | dc54d6a1a239c03c20108e176e938649 MD5 | raw file
Possible License(s): Apache-2.0
  1. # Custom Constraints #
  2. This Grails plugin allows you to create custom domain Constraints for validating Domain objects.
  3. Without this plugin, if you have a custom validation that you want to perform on
  4. a Domain object, you have to use a generic *validator* constraint and define it inline.
  5. With this plugin, you can create reusable, shareable constraints that you can use on
  6. multiple Domain objects. You can then package Constraints in plugins of their own and reuse them across
  7. projects as well.
  8. ### Please Note: ###
  9. Plugins are not loaded during Unit Tests, so you cannot test constraints in your unit tests. They should work during
  10. integration tests though, so you can test them there.
  11. ## Get Started ##
  12. 1. Create a groovy file in */grails-app/utils/* called *Constraint.groovy
  13. 2. Implement a validate closure
  14. 3. Add appropriate messages to */grails-app/i18n/messages.properties*
  15. 4. Apply the validation to a Domain class
  16. ## Create a Constraint with a validate closure ##
  17. Under */grails-app/utils/*:
  18. class UsPhoneConstraint {
  19. def validate = { val ->
  20. return val ==~ /^[01]?[- .]?(\([2-9]\d{2}\)|[2-9]\d{2})[- .]?\d{3}[- .]?\d{4}$/
  21. }
  22. }
  23. ## Add messages to messages.properties ##
  24. Unless you set the *defaultMessage* static property, then it is a good idea to add an entry to messages.properties
  25. with a default format string to show the user if validation fails.
  26. The default format is: default.invalid.constraintName.message
  27. ## Apply the Constraint to a domain class ##
  28. class Person {
  29. String phone
  30. static constraints = {
  31. phone(usPhone: true)
  32. }
  33. }
  34. ## Details
  35. ### Constraint parameters ###
  36. Any parameters passed to the constraint will be available in your Constraint object via the *params*
  37. property.
  38. e.g.
  39. class FooDomain {
  40. String prop
  41. static constraints = {
  42. prop(someConstraint: ['a':1, 'b':2])
  43. }
  44. }
  45. def validate = { val ->
  46. def a = params.a
  47. def b = params.b
  48. return val == a + b
  49. }
  50. ### validate closure (required) ###
  51. The validate closure is the main part of the algorithm where validation is performed. It should return a value to indicate
  52. if the validation was successful.
  53. Successful validation is indicated by the return of:
  54. 1. true
  55. 2. null
  56. An unsuccessful validation is indicated by the return of:
  57. 1. false
  58. 2. A String which is used as the error message to show the user
  59. 3. A Collection with first element being message code, and following elements being message parameters
  60. 4. An Array with first element being message code, and following elements being message parameters
  61. The validate closure takes up to 3 parameters:
  62. 1. The value to be validated
  63. 2. The target object being validated
  64. 3. The validation errors collection
  65. e.g.
  66. def validate = { thePropertyValue, theTargetObject, errorsListYouProbablyWontEverNeed ->
  67. return null != thePropertyValue && theTargetObject.rocks()
  68. }
  69. ### supports closure (optional) ###
  70. Your Constraint can optionally implement a *supports* closure that will allow you to restrict the types
  71. of the properties that the Constraint can be applied to. This closure will be passed a single argument, a Class that
  72. represents the type of the property that the constraint was applied to.
  73. e.g.:
  74. class Foo {
  75. Integer bar
  76. static constraints = {
  77. bar(custom: true)
  78. }
  79. }
  80. The CustomConstraint will get an Integer class passed to its *supports* closure to check.
  81. ### name property (optional) ###
  82. The default name of the Constraint to use in your Domain object is the name of the class, camelCased,
  83. without the tailing Constraint.
  84. e.g.:
  85. 1. MyConstraint -> my
  86. 2. UsPhoneConstraint -> usPhone
  87. You can override this by providing a static name variable in your constraint definition:
  88. static name = "customName"
  89. ### defaultMessageCode property (optional) ###
  90. The defaultMessageCode property defines the default key that will be used to look up the error message
  91. in the *grails-app/i18n/messages.properties* files.
  92. The default value is *default.$name.invalid.message*
  93. You can override this by providing a static variable:
  94. static defaultMessageCode = "default.something.unset.message"
  95. ### failureCode property (optional) ###
  96. The failureCode property defines a key that can be used to lookup error messages in the *grails-app/i18n/messages.properties* files.
  97. The value of this property is appended to the end of the Class.property name that the Constraint is applied to.
  98. The default value is *invalid.$name*
  99. e.g.:
  100. With a CustomConstraint defined the default entry in messages.properties will be something like:
  101. Person.firstName.custom.invalid
  102. You can override this by providing a static variable:
  103. static failureCode = "unset.constraint"
  104. ### defaultMessage property (optional) ###
  105. If no value is found in *messages.properties* for the defaultMessageCode or the failureCode then this message will be
  106. used if it is supplied.
  107. ### expectsParams property (optional) ###
  108. The expectsParams static property allows you to define the required parameters for the Constraint.
  109. The expectsParams can be one of:
  110. 1. Boolean true, saying a parameter is expected
  111. 2. A List of the named parameters that are expected in a map
  112. 3. A Closure allowing you to validate the parameters yourself
  113. e.g.:
  114. static expectsParams = ['start', 'end']
  115. static expectsParams = true
  116. static expectsParams = { parameters -> // ... do something }
  117. ### persistent property (optional) ###
  118. If you need access to the database to perform your validation, you can make your Constraint a persistent constraint by
  119. setting the static property *persist = true* in your Constraint class.
  120. This will make a *hibernateTemplate* property available to your Constraint that you can use to access the database.
  121. Generally these will be more complicated to write because they require knowledge of the details of the Domain
  122. Set this property in your Constraint class with:
  123. static persistent = true
  124. > ### Note ###
  125. > Persistent constraints are only supported when using the Hibernate plugin.
  126. ## Simple Example ##
  127. class SsnConstraint {
  128. static name = "social"
  129. static defaultMessageCode = "default.not.social.message"
  130. def supports = { type ->
  131. return type!= null && String.class.isAssignableFrom(type);
  132. }
  133. def validate = { propertyValue ->
  134. return propertyValue ==~ /\d{3}(-)?\d{2}(-)?\d{4}/
  135. }
  136. }
  137. class Person {
  138. String ssn
  139. static constraints = {
  140. ssn(social: true)
  141. }
  142. }
  143. ## Example With Params ##
  144. class StartsAndEndsWithConstraint {
  145. static expectsParams = ['start', 'end']
  146. def validate = { propertyValue, target ->
  147. return propertyValue[0] == params.start && propertyValue[-1] == params.end
  148. }
  149. }
  150. class MyDomain {
  151. String foo
  152. static constraints = {
  153. foo(startsAndEndsWith: [start: 'G', end: 'f'])
  154. }
  155. }
  156. ## Example Persistent Constraint ##
  157. import org.codehaus.groovy.grails.commons.DomainClassArtefactHandler;
  158. import org.hibernate.Criteria;
  159. import org.hibernate.FlushMode;
  160. import org.hibernate.HibernateException;
  161. import org.hibernate.Session;
  162. import org.hibernate.criterion.Restrictions;
  163. import org.springframework.orm.hibernate3.HibernateCallback;
  164. import java.util.ArrayList;
  165. import java.util.Collections;
  166. import java.util.Iterator;
  167. import java.util.List;
  168. class UniqueEgConstraint {
  169. static persistent = true
  170. def dbCall = { propertyValue, Session session ->
  171. session.setFlushMode(FlushMode.MANUAL);
  172. try {
  173. boolean shouldValidate = true;
  174. if(propertyValue != null && DomainClassArtefactHandler.isDomainClass(propertyValue.getClass())) {
  175. shouldValidate = session.contains(propertyValue)
  176. }
  177. if(shouldValidate) {
  178. Criteria criteria = session.createCriteria( constraintOwningClass )
  179. .add(Restrictions.eq( constraintPropertyName, propertyValue ))
  180. return criteria.list()
  181. } else {
  182. return null
  183. }
  184. } finally {
  185. session.setFlushMode(FlushMode.AUTO)
  186. }
  187. }
  188. def validate = { propertyValue ->
  189. dbCall.delegate = delegate
  190. def _v = dbCall.curry(propertyValue) as HibernateCallback
  191. def result = hibernateTemplate.executeFind(_v)
  192. return result ? false : true // If we find a result, then non-unique
  193. }
  194. }
  195. ## Notes ###
  196. ### Dependency Injection ###
  197. Constraints are standard Grails Artefacts which means that standard things like dependency injection are supported.
  198. You can inject a service or other Spring managed beans into your Constraint class if you need to use it.
  199. e.g.
  200. class MyCustomConstraint {
  201. def someService
  202. def validate = { val ->
  203. return someService.someMethod(val)
  204. }
  205. }
  206. ### Logging ###
  207. Like dependency injection, your constraints classes will have access to the *log* property if you want to do logging
  208. in them.
  209. e.g.
  210. class MyCustomConstraint {
  211. def validate = { val ->
  212. log.debug "Calling MyCustomConstraint with value [${val}]"
  213. // ...
  214. }
  215. }
  216. ### Testing ###
  217. @TestMixin support has been added to make Constraints easy to test using Unit tests.
  218. e.g.
  219. @TestMixin(ConstraintUnitTestMixin)
  220. class UsPhoneConstraintTest {
  221. @Test
  222. void testUsPhoneValidation() {
  223. def constraint = testFor(UsPhoneConstraint)
  224. // Params are automatically mixed in to the test class and exposed
  225. // to the constraint with the call above.
  226. params = true
  227. assertTrue constraint.validate("5135551212")
  228. assertFalse constraint.validate("bad")
  229. }
  230. }