diff --git a/grails-app/controllers/org/grails/jquery/validation/ui/JQueryRemoteValidatorController.groovy b/grails-app/controllers/org/grails/jquery/validation/ui/JQueryRemoteValidatorController.groovy index d325a48..d6678ad 100644 --- a/grails-app/controllers/org/grails/jquery/validation/ui/JQueryRemoteValidatorController.groovy +++ b/grails-app/controllers/org/grails/jquery/validation/ui/JQueryRemoteValidatorController.groovy @@ -15,6 +15,7 @@ package org.grails.jquery.validation.ui import org.codehaus.groovy.grails.validation.ConstrainedPropertyBuilder +import org.springframework.beans.factory.config.AutowireCapableBeanFactory import org.springframework.validation.BeanPropertyBindingResult /** @@ -34,6 +35,9 @@ class JQueryRemoteValidatorController { def validatableInstance if (!params.id || params.id.equals("0")) { validatableInstance = validatableClass.newInstance() + // Wire in spring dependencies... + applicationContext.autowireCapableBeanFactory?.autowireBeanProperties( + validatableInstance, AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, false) } else { validatableInstance = validatableClass.get(params.id.toLong()) } @@ -51,17 +55,13 @@ class JQueryRemoteValidatorController { } constrainedProperty.validate(validatableInstance, propertyValue, errors) - if(validatableInstance.isAttached()) validatableInstance.discard() + if (grailsApplication.isDomainClass(validatableInstance.getClass()) && validatableInstance.isAttached()) { + validatableInstance.discard() + } def fieldError = errors.getFieldError(params.property) // println "fieldError = ${fieldError}, code = ${fieldError?.code}, params.constraint = ${params.constraint}" response.setContentType("text/json;charset=UTF-8") - if (fieldError && fieldError.code.indexOf(params.constraint) > -1) { - // if constraint is known then render false (use default message), - // otherwise render custom message. - render params.constraint ? "false" : """{"message":"${message(error: errors.getFieldError(params.property))}"}""" - } else { - render "true" - } + render fieldError ? """{"message":"${message(error: fieldError)}"}""" : "true" } } diff --git a/grails-app/services/org/grails/jquery/validation/ui/JqueryValidationService.groovy b/grails-app/services/org/grails/jquery/validation/ui/JqueryValidationService.groovy index d4231a1..145529a 100644 --- a/grails-app/services/org/grails/jquery/validation/ui/JqueryValidationService.groovy +++ b/grails-app/services/org/grails/jquery/validation/ui/JqueryValidationService.groovy @@ -18,7 +18,9 @@ import org.codehaus.groovy.grails.validation.ConstrainedPropertyBuilder import org.codehaus.groovy.grails.commons.DefaultGrailsDomainClass import org.codehaus.groovy.grails.web.pages.FastStringWriter import grails.util.GrailsNameUtils +import grails.validation.ValidationErrors import org.springframework.web.context.request.RequestContextHolder +import net.zorched.grails.plugins.validation.CustomConstraintFactory /** * @@ -53,7 +55,27 @@ class JqueryValidationService { nullable: "default.null.message", validator: "default.invalid.validator.message", unique: "default.not.unique.message" - ] + ] + + static final GRAILS_CONSTRAINT_FAILURE_CODES_MAP = [ + blank:'blank', + creditCard:'creditCard.invalid', + email:'email.invalid', + inList:'not.inList', + matches:'matches.invalid', + max:'max.exceeded', + maxSize:'maxSize.exceeded', + min:'min.notmet', + minSize:'minSize.notmet', + notEqual:'notEqual', + nullable:'nullable', + range:null, + size:null, + unique:'unique', + url:'url.invalid', + validator:'validator.invalid' + ] + static transactional = false def grailsApplication def messageSource @@ -158,12 +180,13 @@ class JqueryValidationService { return constraintsMap } - private List getConstraintNames(def constrainedProperty) { - def constraintNames = constrainedProperty.appliedConstraints.collect { return it.name } - if (constraintNames.contains("blank") && constraintNames.contains("nullable")) { - constraintNames.remove("nullable") // blank constraint take precedence + private List getConstraints(def constrainedProperty) { + def constraints = [] + constraints.addAll(constrainedProperty.appliedConstraints) + if (constraints.find{it.name == "blank"} && constraints.find {it.name == "nullable"}) { + constraints.removeAll {it.name == "nullable"} // blank constraint take precedence } - return constraintNames + return constraints } private String createRemoteJavaScriptConstraints(String contextPath, String constraintName, String validatableClassName, String propertyName) { @@ -212,25 +235,65 @@ class JqueryValidationService { return message.encodeAsJavaScript() } - private String getMessage(Class validatableClass, String propertyName, def args, String constraintName, Locale locale) { - def code = "${validatableClass.name}.${propertyName}.${constraintName}" - def defaultMessage = "Error message for ${code} undefined." - def message = messageSource.getMessage(code, args == null ? null : args.toArray(), null, locale) - - ERROR_CODE_SUFFIXES.each { errorSuffix -> - message = message?:messageSource.getMessage("${code}.${errorSuffix}", args == null ? null : args.toArray(), null, locale) - } - if (!message) { - code = "${GrailsNameUtils.getPropertyName(validatableClass)}.${propertyName}.${constraintName}" + private String getMessage(def constraint, Class validatableClass, def args, Locale locale) { + def argArray = args == null ? null : args.toArray() + String defaultMessageCode + String message + // The preferred way to do things is to create an object to validate and pretend that a validation error occurred + // so that we simulate as closely as possible all of the error codes that grails generates on the server side. + // Another approach would be to copy the code from AbstractConstraint that does all the work of generating all of + // the message code possibilities and sticks them into the Errors object here. That would allow us to eliminate the + // else case below. It would come at the cost of having to make sure it was kept in sync with the grails source from + // which is was copied. It would be nice if grails would refactor that bit of code into a utility class that we + // could call... + if (!grailsApplication.config.jqueryValidationUi.useLegacyMessageCodes && constraint.respondsTo('rejectValue')) { + String failureCode + // Handle custom constraints correctly by getting the default message code, default message and the + // failure code to use. It would be nice if the standard grails constraints also worked this way, but + // unfortunately we have to just hard code in the values for those constraints based on the grails + // documentation... + if (constraint instanceof CustomConstraintFactory.CustomConstraintClass) { + defaultMessageCode = constraint.constraint.getDefaultMessageCode() + failureCode = constraint.constraint.getFailureCode() + } else { + failureCode = GRAILS_CONSTRAINT_FAILURE_CODES_MAP[constraint.name] + } + // Fake a call to reject the constraint which should result in our validationErrors object having a single + // FieldError in it which will contain a list of the standard grails message codes for validation failure + // in the correct search order... + def targetObject = validatableClass.newInstance() + def validationErrors = new ValidationErrors(targetObject, constraint.propertyName) + // Casts to string needed here to avoid ambiguous method overloading exceptions since sometimes the values of + // defaultMessageCode and failureCode are null... + constraint.rejectValue(targetObject, validationErrors, (String)defaultMessageCode, (String)failureCode, argArray) + message = validationErrors.fieldErrors[0].codes.findResult {messageSource.getMessage(it, argArray, null, locale)} + } else { + // This is not the common case, but could happen if someone implemented Constraint without subclassing + // AbstractConstraint. This is a rather inaccurate attempt at trying to find a message from a series of message code + // possibilities. It is inaccurate because the name of the constraint often can't be generated by tacking on '.error' + // or '.invalid' to the end of ${classname}.${constraint.propertyName}.${constraint.name}. Also, for custom constraints, the user + // can define whatever message code they want, so we're not respecting that at all here. Finally, for custom + // constraints, the default message code and default message can be defined, but there is no attempt to even try to + // find a default value for a custom constraint here... + def code = "${validatableClass.name}.${constraint.propertyName}.${constraint.name}" message = messageSource.getMessage(code, args == null ? null : args.toArray(), null, locale) - } + + ERROR_CODE_SUFFIXES.each { errorSuffix -> + message = message?:messageSource.getMessage("${code}.${errorSuffix}", argArray, null, locale) + } + if (!message) { + code = "${GrailsNameUtils.getPropertyName(validatableClass)}.${constraint.propertyName}.${constraint.name}" + message = messageSource.getMessage(code, argArray, null, locale) + } - ERROR_CODE_SUFFIXES.each { errorSuffix -> - message = message?:messageSource.getMessage("${code}.${errorSuffix}", args == null ? null : args.toArray(), null, locale) + ERROR_CODE_SUFFIXES.each { errorSuffix -> + message = message?:messageSource.getMessage("${code}.${errorSuffix}", argArray, null, locale) + } } if (!message) { - code = DEFAULT_ERROR_MESSAGE_CODES_MAP[constraintName] - message = messageSource.getMessage(code, args == null ? null : args.toArray(), defaultMessage, locale) + String defaultMessage = "Property [{0}] of class [{1}] with value [{2}] is not valid" + defaultMessageCode = defaultMessageCode ?: DEFAULT_ERROR_MESSAGE_CODES_MAP[constraint.name] + message = messageSource.getMessage(defaultMessageCode, argArray, defaultMessage, locale) } return message.encodeAsJavaScript() } @@ -243,7 +306,7 @@ class JqueryValidationService { javaScriptConstraints << "{ " constraintsMap = getConstraintsMap(constrainedProperty.propertyType) - def constraintNames = getConstraintNames(constrainedProperty) + def constraints = getConstraints(constrainedProperty) switch (constrainedProperty.propertyType) { case Date: @@ -264,17 +327,17 @@ class JqueryValidationService { if (javaScriptConstraintCode) { javaScriptConstraints << javaScriptConstraintCode - if (constraintNames.size() > 0) { + if (constraints.size() > 0) { javaScriptConstraints << ", " } else { javaScriptConstraints << " " } } - constraintNames.eachWithIndex { constraintName, i -> - javaScriptConstraint = constraintsMap[constraintName] + constraints.eachWithIndex { constraint, i -> + javaScriptConstraint = constraintsMap[constraint.name] javaScriptConstraintCode = null if (javaScriptConstraint) { - switch (constraintName) { + switch (constraint.name) { case "nullable": if (!constrainedProperty.isNullable()) { javaScriptConstraintCode = "${javaScriptConstraint}: true" @@ -347,21 +410,35 @@ class JqueryValidationService { break case "unique": case "validator": - javaScriptConstraintCode = createRemoteJavaScriptConstraints(RequestContextHolder.requestAttributes.contextPath, constraintName, constrainedProperty.owningClass.name, constrainedProperty.propertyName) + javaScriptConstraintCode = createRemoteJavaScriptConstraints(RequestContextHolder.requestAttributes.contextPath, constraint.name, constrainedProperty.owningClass.name, constrainedProperty.propertyName) + break + default: + // custom constraint... + def customConstraintsMap = grailsApplication.config.jqueryValidationUi.CustomConstraintsMap + if (customConstraintsMap && customConstraintsMap[constraint.name]) { + javaScriptConstraintCode = "${javaScriptConstraint}: ${customConstraintsMap[constraint.name]}" + } else { + log.error "Failed to generate javascript validation rule for constraint '${constraint.name}' " + + "with javascript constraint '${javaScriptConstraint}': missing custom constraints map " + + "entry. Ignoring this constraint and moving on." + } break } } else { + // Old way of generating the custom constraint javascript rule is to assume the javascript constraint validation method is + // the same name as the grails constraint. The preferred way to do things now is to create a map entry in the appropriate + // map (string, number, date, etc.) for the custom constraint as is done for the built-in grails constraints... def customConstraintsMap = grailsApplication.config.jqueryValidationUi.CustomConstraintsMap - if (customConstraintsMap && customConstraintsMap[constraintName]) { - javaScriptConstraintCode = "$constraintName: ${customConstraintsMap[constraintName]}" + if (customConstraintsMap && customConstraintsMap[constraint.name]) { + javaScriptConstraintCode = "${constraint.name}: ${customConstraintsMap[constraint.name]}" } else { - log.info "${constraintName} constraint not found even in the CustomConstraintsMap, use custom constraint and remote validation" + log.info "${constraint.name} constraint not found even in the CustomConstraintsMap, use custom constraint and remote validation" javaScriptConstraintCode = createRemoteJavaScriptConstraints(RequestContextHolder.requestAttributes.contextPath, constraintName, constrainedProperty.owningClass.name, constrainedProperty.propertyName) } } if (javaScriptConstraintCode) { javaScriptConstraints << javaScriptConstraintCode - if (i < constraintNames.size() - 1) { + if (i < constraints.size() - 1) { javaScriptConstraints << ", " } else { javaScriptConstraints << " " @@ -382,13 +459,13 @@ private String _createJavaScriptMessages(def constrainedProperty, Locale locale, def args = [] FastStringWriter javaScriptMessages = new FastStringWriter(VALIDATION_MESSAGE_LENGTH) String javaScriptConstraint -def constraintNames +def constraints String javaScriptMessageCode def constraintsMap = getConstraintsMap(constrainedProperty.propertyType) javaScriptMessages << "{ " -constraintNames = getConstraintNames(constrainedProperty) +constraints = getConstraints(constrainedProperty) javaScriptMessageCode = null switch (constrainedProperty.propertyType) { case Date: @@ -412,27 +489,27 @@ case BigDecimal: if (javaScriptMessageCode) { javaScriptMessages << javaScriptMessageCode -if (constraintNames.size() > 0) { +if (constraints.size() > 0) { javaScriptMessages << ", " } else { javaScriptMessages << " " } } -constraintNames.eachWithIndex { constraintName, i -> -javaScriptConstraint = constraintsMap[constraintName] +constraints.eachWithIndex { constraint, i -> +javaScriptConstraint = constraintsMap[constraint.name] javaScriptMessageCode = null args.clear() args = [constrainedProperty.propertyName, constrainedProperty.owningClass] if (javaScriptConstraint) { - switch (constraintName) { + switch (constraint.name) { case "nullable": if (!constrainedProperty.isNullable()) { - javaScriptMessageCode = "${javaScriptConstraint}: '${getMessage(constrainedProperty.owningClass, constrainedProperty.propertyName, args, constraintName, locale)}'" + javaScriptMessageCode = "${javaScriptConstraint}: '${getMessage(constraint, constrainedProperty.owningClass, args, locale)}'" } case "blank": if (!constrainedProperty.isBlank()) { - javaScriptMessageCode = "${javaScriptConstraint}: '${getMessage(constrainedProperty.owningClass, constrainedProperty.propertyName, args, constraintName, locale)}'" + javaScriptMessageCode = "${javaScriptConstraint}: '${getMessage(constraint, constrainedProperty.owningClass, args, locale)}'" } break case "creditCard": @@ -440,7 +517,7 @@ if (javaScriptConstraint) { case "url": if (constrainedProperty.isCreditCard() || constrainedProperty.isEmail() || constrainedProperty.isUrl()) { args << VALUE_PLACEHOLDER - javaScriptMessageCode = "${javaScriptConstraint}: function() { return '${getMessage(constrainedProperty.owningClass, constrainedProperty.propertyName, args, constraintName, locale)}'; }".replace( + javaScriptMessageCode = "${javaScriptConstraint}: function() { return '${getMessage(constraint, constrainedProperty.owningClass, args, locale)}'; }".replace( VALUE_PLACEHOLDER, "' + \$('#${constrainedProperty.propertyName}').val() + '"); } break @@ -452,39 +529,51 @@ if (javaScriptConstraint) { case "minSize": case "notEqual": args << VALUE_PLACEHOLDER - args << constrainedProperty."${constraintName}" - javaScriptMessageCode = "${javaScriptConstraint}: function() { return '${getMessage(constrainedProperty.owningClass, constrainedProperty.propertyName, args, constraintName, locale)}'; }".replace( + args << constrainedProperty."${constraint.name}" + javaScriptMessageCode = "${javaScriptConstraint}: function() { return '${getMessage(constraint, constrainedProperty.owningClass, args, locale)}'; }".replace( VALUE_PLACEHOLDER, "' + \$('#${constrainedProperty.propertyName}').val() + '"); break case "range": case "size": args << VALUE_PLACEHOLDER - def range = constrainedProperty."${constraintName}" + def range = constrainedProperty."${constraint.name}" args << range.from args << range.to - javaScriptMessageCode = "${javaScriptConstraint}: function() { return '${getMessage(constrainedProperty.owningClass, constrainedProperty.propertyName, args, constraintName, locale)}'; }".replace( + javaScriptMessageCode = "${javaScriptConstraint}: function() { return '${getMessage(constraint, constrainedProperty.owningClass, args, locale)}'; }".replace( VALUE_PLACEHOLDER, "' + \$('#${constrainedProperty.propertyName}').val() + '"); break case "unique": case "validator": args << VALUE_PLACEHOLDER - javaScriptMessageCode = "${javaScriptConstraint}: function() { return '${getMessage(constrainedProperty.owningClass, constrainedProperty.propertyName, args, constraintName, locale)}'; }".replace( + javaScriptMessageCode = "${javaScriptConstraint}: function() { return '${getMessage(constraint, constrainedProperty.owningClass, args, locale)}'; }".replace( VALUE_PLACEHOLDER, "' + \$('#${constrainedProperty.propertyName}').val() + '"); break + default: + // custom constraint... + def customConstraintsMap = grailsApplication.config.jqueryValidationUi.CustomConstraintsMap + if (customConstraintsMap && customConstraintsMap[constraint.name]) { + args << VALUE_PLACEHOLDER + javaScriptMessageCode = "${javaScriptConstraint}: function() { return '${getMessage(constraint, constrainedProperty.owningClass, args, locale)}'; }".replace( + VALUE_PLACEHOLDER, "' + \$('#${constrainedProperty.propertyName}').val() + '"); + } + break } } else { + // Old way of generating the custom constraint javascript message is to assume the javascript constraint validation method is + // the same name as the grails constraint. The preferred way to do things now is to create a map entry in the appropriate + // map (string, number, date, etc.) for the custom constraint as is done for the built-in grails constraints... def customConstraintsMap = grailsApplication.config.jqueryValidationUi.CustomConstraintsMap - if (customConstraintsMap && customConstraintsMap[constraintName]) { + if (customConstraintsMap && customConstraintsMap[constraint.name]) { args << VALUE_PLACEHOLDER - javaScriptMessageCode = "${constraintName}: function() { return '${getMessage(constrainedProperty.owningClass, constrainedProperty.propertyName, args, constraintName, locale)}'; }".replace( + javaScriptMessageCode = "${constraint.name}: function() { return '${getMessage(constraint, constrainedProperty.owningClass, args, locale)}'; }".replace( VALUE_PLACEHOLDER, "' + \$('#${constrainedProperty.propertyName}').val() + '"); } // else remote validation, using remote message. } if (javaScriptMessageCode) { javaScriptMessages << javaScriptMessageCode - if (i < constraintNames.size() - 1) { + if (i < constraints.size() - 1) { javaScriptMessages << ", " } else { javaScriptMessages << " " diff --git a/src/docs/guide/usage/features.gdoc b/src/docs/guide/usage/features.gdoc index a08e451..45b0e87 100644 --- a/src/docs/guide/usage/features.gdoc +++ b/src/docs/guide/usage/features.gdoc @@ -29,8 +29,16 @@ h3. Extensibility Together with the [custom constraints|http://github.com/geofflane/grails-constraints] plugin, the plugin is fully extensible with your own custom validation logic. -The plugin come with 2 custom constraints, phone and phoneUS (International and US phone number validation) which enabled by the following configuration: +The plugin comes with 2 custom constraints, phone and phoneUS (International and US phone number validation) which are enabled by the following configuration: {code} +StringConstraintsMap = [ + blank:'required', // inverse: blank=false, required=true + creditCard:'creditcard', + ..., + phone:'phone', + phoneUS:'phoneUS', +] + CustomConstraintsMap = [ phone:'true', phoneUS:'true' @@ -39,13 +47,25 @@ CustomConstraintsMap = [ If you implement new custom constraints (both server-side and Javascript), you can enable it by adding the constraints to the @CustomConstraintsMap@, for example: {code} +StringConstraintsMap = [ + blank:'required', // inverse: blank=false, required=true + creditCard:'creditcard', + ..., + phone:'phone', + phoneUS:'phoneUS', + yourCustomConstraint:'jqueryValidationPluginCustomConstraint' +] + CustomConstraintsMap = [ phone:'true', phoneUS:'true', - yourCustomConstraint:'Javascript Code' + yourCustomConstraint:'jqueryValidationPluginCustomConstraintParamGeneratorJavascript' ] {code} -The @'Javascript Code'@ is Javascript code specific to your custom constraints. This will be rendered by @@ tag. Please refer to +The @'jqueryValidationPluginCustomConstraint'@ is the name of the custom jquery validation plugin method to use for client-side validation. +The @'jqueryValidationPluginCustomConstraintParamGeneratorJavascript'@ is javascript that will be used to supply the parameter to +the jqueryValidationPluginCustomConstraint. +These values will be used by the @@ tag. Please refer to the source code of the server-side implementation of phone constraint [here|http://github.com/limcheekin/jquery-validation-ui/blob/master/test/unit/org/grails/jquery/validation/ui/PhoneConstraintTests.groovy] and the client-side implementation @@ -54,7 +74,7 @@ and the client-side implementation h3. Internationalization Support All client-side validation messages retrieve from messages.properties. So, both client-side and server-side validation using the same message bundle. -The plugin retrieve the validation message with following codes from top to bottom: +Prior to version 1.4.5, the plugin retrieved the validation message by trying the following codes in this order: {code} classFullName.property.constraint @@ -65,7 +85,36 @@ className.property.constraint.error className.property.constraint.invalid {code} -If it is not found, it will use the default message. +As of version 1.4.5, the plugin now searches for an error message using the same error codes used by the grails server-side validation process. It also +respects the @defaultMessageCode@, @defaultMessage@, and @failureCode@ settings on custom constraints when searching for an error message. This means that +it should now be much easier to know which message codes you should use in your message bundles since the server-side and client-side now use the same +codes and search order. The one exception to this is if a grails constraint returns different message codes depending on the kind +of validation failure. For example, the grails size constraint returns @className.propertyName.size.toosmall@ or @className.propertyName.size.toobig@ +depending on whether the field value was too large or too small to satisfy the constraint. Since the plugin is simply generating the validation code, +but doesn't yet know what the value will be, it has no way of knowing which validation message it should insert into the javascript. It may be possible +to generate javascript that would handle both cases on the client in the future. However, for now the workaround is to use the message code +@className.propertyName.size.error@ (or other standard grails message code for the size constraint that doesn't have 'toobig' or 'toosmall' in it) with a +message that will work for when the value is too large or too small. Another option for the size constraint is to use the @minSize@ and @maxSize@ constraints +instead of the @size@ constraint. + +Most pre-1.4.5 applications will be mostly compatible with the new behavior as most of the error codes used by the old behavior are also used by the standard +grails validation process. The one difference is that in some cases the error codes with the suffix of '.invalid' or the error codes with no '.error' or +'.invalid' suffix that were used by the plugin prior to 1.4.5 are not used by the standard grails validation process. If you have these message codes in your +message bundles, you will need to update them to use a standard grails message code instead. For example, the message code @className.property.blank.invalid@ +is not a standard grails message code, but it was searched for by this plugin prior to version 1.4.5. You would need to change your message bundles to use a +standard grails message code instead. For example, the codes @className.property.blank.error@ or @className.property.blank@ are standard grails message codes for +the blank constraint and would be valid replacements for the @className.property.blank.invalid message@ code that worked prior to grails 1.4.5. For backwards +compatibility, you can add the following line: + +{code} +useLegacyMessageCodes=false +{code} + +to the @jqueryValidationUi@ section of your @Config.groovy@ to use the pre-1.4.5 message codes. However, please be aware that the pre-1.4.5 behavior is +deprecated, so this option is only made available to allow users to upgrade the plugin and have time to convert their message codes to standard grails +codes. The useLegacyMessageCodes option will be removed in a future release. + +If a message cannot be found from the message codes, the default message will be used. h3. Type Validation Support The plugin supports type validation for @Date@, @Long@, @Integer@, @Short@, @BigInteger@, @Float@, @Double@, and @BigDecimal@ and retrieves the corresponding diff --git a/test/unit/org/grails/jquery/validation/ui/JqueryValidationServiceSpec.groovy b/test/unit/org/grails/jquery/validation/ui/JqueryValidationServiceSpec.groovy index a492375..4410146 100644 --- a/test/unit/org/grails/jquery/validation/ui/JqueryValidationServiceSpec.groovy +++ b/test/unit/org/grails/jquery/validation/ui/JqueryValidationServiceSpec.groovy @@ -2,6 +2,7 @@ package org.grails.jquery.validation.ui import spock.lang.* import grails.test.mixin.* +import org.codehaus.groovy.grails.validation.Constraint import org.springframework.context.MessageSource @TestFor(JqueryValidationService) @@ -16,13 +17,16 @@ public class JqueryValidationServiceSpec extends Specification { def "Escaped messages"() { given: MessageSource messageSource = Mock() + Constraint constraint = Mock() service.messageSource = messageSource when: - def message = service.getMessage(this.class, "prop", null, "max", null) + def message = service.getMessage(constraint, this.class, null, null) then: 1 * messageSource.getMessage("${this.class.name}.prop.max", null, null, null) >> "my 'message'" + constraint.name >> 'max' + constraint.propertyName >> 'prop' 0 * _._ message=="my \\'message\\'" }