From b2e1bbdceac4a9704e2e9e73c019573ced82f1c6 Mon Sep 17 00:00:00 2001 From: Jon Oler Date: Fri, 29 Mar 2013 00:47:42 -0600 Subject: [PATCH 1/2] Issue #17: Wire spring dependencies in newly created domain/command object in JQueryRemoteValidationController. --- .../validation/ui/JQueryRemoteValidatorController.groovy | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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..729d7be 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,7 +55,9 @@ 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}" From e4a87179744ae63ad979c2cec22f86e6a21422f6 Mon Sep 17 00:00:00 2001 From: Jon Oler Date: Mon, 8 Apr 2013 12:15:29 -0600 Subject: [PATCH 2/2] Internationalization improvements: * New algorithm for determining the error code to use for client-side error messages. Instead of approximating the same error codes that grails uses, we now use the same codes as grails. This allows code that uses server-side validation using messages in the message bundle to use the same codes for messages on the client and the server. For example, previously, a message with the code className.propertyName.minSize.notmet would be successfully retrieved for a constraint violation of the minSize constraint on the server, but would not be used for client-side validation. The useLegacyMessageCodes setting can be used to get the old behavior. * For remote validation, there was a buggy attempt to avoid sending the error message if it was "known". The problem was that just the default message code was checked for the constraint name instead of the message code actually used. We now just always lookup the message rather than attempting to optimize. * Allow custom validators to have their client-side javascript validation function name differ from the server-side constraint name as is the case with the standard grails constraints. * Use the default error code, default error message, and failure code when generating error messages for custom constraints. --- .../ui/JQueryRemoteValidatorController.groovy | 8 +- .../ui/JqueryValidationService.groovy | 185 +++++++++++++----- src/docs/guide/usage/features.gdoc | 59 +++++- .../ui/JqueryValidationServiceSpec.groovy | 6 +- 4 files changed, 197 insertions(+), 61 deletions(-) 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 729d7be..d6678ad 100644 --- a/grails-app/controllers/org/grails/jquery/validation/ui/JQueryRemoteValidatorController.groovy +++ b/grails-app/controllers/org/grails/jquery/validation/ui/JQueryRemoteValidatorController.groovy @@ -62,12 +62,6 @@ class JQueryRemoteValidatorController { // 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\\'" }