Skip to content

HTTPS clone URL

Subversion checkout URL

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