001    /**
002     * Copyright 2005-2012 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.rice.krad.uif.util;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.kuali.rice.core.api.config.property.ConfigurationService;
020    import org.kuali.rice.krad.datadictionary.validation.constraint.BaseConstraint;
021    import org.kuali.rice.krad.datadictionary.validation.constraint.CaseConstraint;
022    import org.kuali.rice.krad.datadictionary.validation.constraint.Constraint;
023    import org.kuali.rice.krad.datadictionary.validation.constraint.MustOccurConstraint;
024    import org.kuali.rice.krad.datadictionary.validation.constraint.PrerequisiteConstraint;
025    import org.kuali.rice.krad.datadictionary.validation.constraint.SimpleConstraint;
026    import org.kuali.rice.krad.datadictionary.validation.constraint.ValidCharactersConstraint;
027    import org.kuali.rice.krad.datadictionary.validation.constraint.WhenConstraint;
028    import org.kuali.rice.krad.service.KRADServiceLocator;
029    import org.kuali.rice.krad.uif.UifConstants;
030    import org.kuali.rice.krad.uif.field.InputField;
031    import org.kuali.rice.krad.uif.view.FormView;
032    import org.kuali.rice.krad.uif.view.View;
033    import org.kuali.rice.krad.uif.control.TextControl;
034    
035    import java.text.MessageFormat;
036    import java.util.ArrayList;
037    import java.util.EnumSet;
038    import java.util.List;
039    
040    /**
041     * This class contains all the methods necessary for generating the js required to perform validation client side.
042     * The processAndApplyConstraints(InputField field, View view) is the key method of this class used by
043     * InputField to setup its client side validation mechanisms.
044     * 
045     * @author Kuali Rice Team (rice.collab@kuali.org)
046     */
047    public class ClientValidationUtils {
048            // used to give validation methods unique signatures
049            private static int methodKey = 0;
050            
051            // list used to temporarily store mustOccurs field names for the error
052            // message
053            private static List<List<String>> mustOccursPathNames;
054            
055            public static final String LABEL_KEY_SPLIT_PATTERN = ",";
056            
057            
058            public static final String PREREQ_MSG_KEY = "prerequisite";
059            public static final String POSTREQ_MSG_KEY = "postrequisite";
060            public static final String MUSTOCCURS_MSG_KEY = "mustOccurs";
061            public static final String GENERIC_FIELD_MSG_KEY = "general.genericFieldName";
062            
063            public static final String ALL_MSG_KEY = "general.all";
064            public static final String ATMOST_MSG_KEY = "general.atMost";
065            public static final String AND_MSG_KEY = "general.and";
066            public static final String OR_MSG_KEY = "general.or";
067            
068            private static ConfigurationService configService = KRADServiceLocator.getKualiConfigurationService();
069            
070            //Enum representing names of rules provided by the jQuery plugin
071            public static enum ValidationMessageKeys{
072                    REQUIRED("required"), 
073                    MIN_EXCLUSIVE("minExclusive"), 
074                    MAX_INCLUSIVE("maxInclusive"),
075                    MIN_LENGTH("minLengthConditional"),
076                    MAX_LENGTH("maxLengthConditional");
077                    
078                    private ValidationMessageKeys(String name) {
079                            this.name = name;
080                    }
081                    
082                    private final String name;
083                    
084                    @Override
085                    public String toString() {
086                            return name;
087                    }
088                    
089                    public static boolean contains(String name){
090                for (ValidationMessageKeys element : EnumSet.allOf(ValidationMessageKeys.class)) {
091                    if (element.toString().equalsIgnoreCase(name)) {
092                        return true;
093                    }
094                }
095                return false;
096                    }
097            }
098            
099            public static String generateMessageFromLabelKey(List<String> params, String labelKey){
100                    String message = "NO MESSAGE";
101                    if(StringUtils.isNotEmpty(labelKey)){
102                            message = configService.getPropertyValueAsString(labelKey);
103                            if(params != null && !params.isEmpty() && StringUtils.isNotEmpty(message)){
104                                message = MessageFormat.format(message, params.toArray());
105                            }
106                    }
107                    if(StringUtils.isEmpty(message)){
108                        message = labelKey;
109                    }
110                    //replace characters that might cause issues with their equivalent html codes
111                    if(message.contains("\"")){
112                        message = message.replace("\"", "&quot;");
113                    }
114                    if(message.contains("'")){
115                        message = message.replace("'", "&#39;");
116                    }
117                    if(message.contains("\\")){
118                        message = message.replace("\\", "&#92;");
119                    }
120                    return message;
121            }
122    
123            /**
124             * Generates the js object used to override all default messages for validator jquery plugin with custom
125             * messages derived from the configService.
126             * 
127             * @return
128             */
129            public static String generateValidatorMessagesOption(){
130                    String mOption = "";
131                    String keyValuePairs = "";
132                    for(ValidationMessageKeys element : EnumSet.allOf(ValidationMessageKeys.class)){
133                            String key = element.toString();
134                            String message = configService.getPropertyValueAsString(UifConstants.Messages.VALIDATION_MSG_KEY_PREFIX + key);
135                            if(StringUtils.isNotEmpty(message)){
136                                    keyValuePairs = keyValuePairs + "\n" + key + ": '"+ message + "',";
137                                    
138                            }
139                            
140                    }
141                    keyValuePairs = StringUtils.removeEnd(keyValuePairs, ",");
142                    if(StringUtils.isNotEmpty(keyValuePairs)){
143                            mOption="{" + keyValuePairs + "}";
144                    }
145                    
146                    return mOption;
147            }
148            
149            /**
150             * Returns the add method jquery validator call for the regular expression
151             * stored in validCharactersConstraint.
152             * 
153             * @param validCharactersConstraint
154             * @return js validator.addMethod script
155             */
156            public static String getRegexMethod(InputField field, ValidCharactersConstraint validCharactersConstraint) {
157                    String message = generateMessageFromLabelKey(validCharactersConstraint.getValidationMessageParams(), validCharactersConstraint.getLabelKey());
158                    String key = "validChar-" + field.getBindingInfo().getBindingPath() + methodKey;
159                    
160                    String regex = validCharactersConstraint.getValue();
161                    //replace characters known to cause issues if not escaped
162                    if(regex.contains("\\\\")){
163                        regex = regex.replaceAll("\\\\", "\\\\\\\\");
164                    }
165                    if(regex.contains("/")){
166                        regex = regex.replace("/", "\\/");
167                    }
168                    
169            return "\njQuery.validator.addMethod(\"" + ScriptUtils.escapeName(key)
170                    + "\", function(value, element) {\n "
171                    + "return this.optional(element) || /" + regex + "/.test(value);"
172                    + "}, \"" + message + "\");";
173            }
174            
175            /**
176         * Returns the add method jquery validator call for the regular expression
177         * stored in validCharactersConstraint that explicitly checks a boolean.  Needed because one method
178         * accepts params and the other doesn't.
179         * 
180         * @param validCharactersConstraint
181         * @return js validator.addMethod script
182         */
183        public static String getRegexMethodWithBooleanCheck(InputField field, ValidCharactersConstraint validCharactersConstraint) {
184            String message = generateMessageFromLabelKey(validCharactersConstraint.getValidationMessageParams(), validCharactersConstraint.getLabelKey());
185            String key = "validChar-" + field.getBindingInfo().getBindingPath() + methodKey;
186            
187            String regex = validCharactersConstraint.getValue();
188            //replace characters known to cause issues if not escaped
189            if(regex.contains("\\\\")){
190                regex = regex.replaceAll("\\\\", "\\\\\\\\");
191            }
192            if(regex.contains("/")){
193                        regex = regex.replace("/", "\\/");
194                    }
195            
196            return "\njQuery.validator.addMethod(\"" + ScriptUtils.escapeName(key)
197                    + "\", function(value, element, doCheck) {\n if(doCheck === false){return true;}else{"
198                    + "return this.optional(element) || /" + regex + "/.test(value);}"
199                    + "}, \"" + message + "\");";
200        }
201            
202    
203            /**
204             * This method processes a single CaseConstraint. Internally it makes calls
205             * to processWhenConstraint for each WhenConstraint that exists in this
206             * constraint. It adds a "dependsOn" css class to this field for the field
207             * which the CaseConstraint references.
208             * 
209             * @param view
210             * @param andedCase
211             *            the boolean logic to be anded when determining if this case is
212             *            satisfied (used for nested CaseConstraints)
213             */
214            public static void processCaseConstraint(InputField field, View view, CaseConstraint constraint, String andedCase) {
215                    if (constraint.getOperator() == null) {
216                            constraint.setOperator("equals");
217                    }
218    
219                    String operator = "==";
220                    if (constraint.getOperator().equalsIgnoreCase("not_equals") || constraint.getOperator().equalsIgnoreCase("not_equal")) {
221                            operator = "!=";
222                    }
223                    else if (constraint.getOperator().equalsIgnoreCase("greater_than_equal")) {
224                            operator = ">=";
225                    }
226                    else if (constraint.getOperator().equalsIgnoreCase("less_than_equal")) {
227                            operator = "<=";
228                    }
229                    else if (constraint.getOperator().equalsIgnoreCase("greater_than")) {
230                            operator = ">";
231                    }
232                    else if (constraint.getOperator().equalsIgnoreCase("less_than")) {
233                            operator = "<";
234                    }
235                    else if (constraint.getOperator().equalsIgnoreCase("has_value")) {
236                            operator = "";
237                    }
238                    // add more operator types here if more are supported later
239    
240                    field.getControl().addStyleClass("dependsOn-" + ScriptUtils.escapeName(constraint.getPropertyName()));
241    
242                    if (constraint.getWhenConstraint() != null && !constraint.getWhenConstraint().isEmpty()) {
243                            //String fieldPath = field.getBindingInfo().getBindingObjectPath() + "." + constraint.getPropertyName();
244                String fieldPath = constraint.getPropertyName();
245                        for (WhenConstraint wc : constraint.getWhenConstraint()) {
246                                    processWhenConstraint(field, view, constraint, wc, ScriptUtils.escapeName(fieldPath), operator, andedCase);
247                            }
248                    }
249            }
250            
251    
252    
253            /**
254             * This method processes the WhenConstraint passed in. The when constraint
255             * is used to create a boolean statement to determine if the constraint will
256             * be applied. The necessary rules/methods for applying this constraint are
257             * created in the createRule call. Note the use of the use of coerceValue js
258             * function call.
259             * 
260             * @param view
261             * @param wc
262             * @param fieldPath
263             * @param operator
264             * @param andedCase
265             */
266            private static void processWhenConstraint(InputField field, View view, CaseConstraint caseConstraint, WhenConstraint wc, String fieldPath,
267                            String operator, String andedCase) {
268                    String ruleString = "";
269                    // prerequisite constraint
270    
271                    String booleanStatement = "";
272                    if (wc.getValues() != null) {
273    
274                            String caseStr = "";
275                            if (!caseConstraint.isCaseSensitive()) {
276                                    caseStr = ".toUpperCase()";
277                            }
278                            for (int i = 0; i < wc.getValues().size(); i++) {
279                                    if (operator.isEmpty()) {
280                                            // has_value case
281                                            if (wc.getValues().get(i) instanceof String
282                                                            && ((String) wc.getValues().get(i)).equalsIgnoreCase("false")) {
283                                                    booleanStatement = booleanStatement + "(coerceValue('" + fieldPath + "') == '')";
284                                            }
285                                            else {
286                                                    booleanStatement = booleanStatement + "(coerceValue('" + fieldPath + "') != '')";
287                                            }
288                                    }
289                                    else {
290                                            // everything else
291                                            booleanStatement = booleanStatement + "(coerceValue('" + fieldPath + "')" + caseStr + " "
292                                                            + operator + " \"" + wc.getValues().get(i) + "\"" + caseStr + ")";
293                                    }
294                                    if ((i + 1) != wc.getValues().size()) {
295                                            booleanStatement = booleanStatement + " || ";
296                                    }
297                            }
298    
299                    }
300    
301                    if (andedCase != null) {
302                            booleanStatement = "(" + booleanStatement + ") && (" + andedCase + ")";
303                    }
304    
305                    if (wc.getConstraint() != null && StringUtils.isNotEmpty(booleanStatement)) {
306                            ruleString = createRule(field, wc.getConstraint(), booleanStatement, view);
307                    }
308    
309                    if (StringUtils.isNotEmpty(ruleString)) {
310                            addScriptToPage(view, field, ruleString);
311                    }
312            }
313            
314    
315    
316            /**
317             * Adds the script to the view to execute on a jQuery document ready event.
318             * 
319             * @param view
320             * @param script
321             */
322            public static void addScriptToPage(View view, InputField field, String script) {
323            String prefixScript = "";
324            
325            if (field.getOnDocumentReadyScript() != null) {
326                prefixScript = field.getOnDocumentReadyScript();
327            }
328            field.setOnDocumentReadyScript(prefixScript + "\n" + "runValidationScript(function(){" + script + "});");
329            }
330            
331            /**
332             * Determines which fields are being evaluated in a boolean statement, so handlers can be
333             * attached to them if needed, returns these names in a list.
334             * 
335             * @param statement
336             * @return
337             */
338            private static List<String> parseOutFields(String statement){
339                List<String> fieldNames = new ArrayList<String>();
340                String[] splits = StringUtils.splitByWholeSeparator(statement, "coerceValue('");
341                for(String s: splits){
342                    String fieldName = StringUtils.substringBefore(s, "'");
343                    fieldNames.add(fieldName);
344                }
345                return fieldNames;
346            }
347    
348            /**
349             * This method takes in a constraint to apply only when the passed in
350             * booleanStatement is valid. The method will create the necessary addMethod
351             * and addRule jquery validator calls for the rule to be applied to the
352             * field when the statement passed in evaluates to true during runtime and
353             * this field is being validated. Note the use of custom methods for min/max
354             * length/value.
355             * 
356             * @param field
357             *            the field to apply the generated methods and rules to
358             * @param constraint
359             *            the constraint to be applied when the booleanStatement
360             *            evaluates to true during validation
361             * @param booleanStatement
362             *            the booleanstatement in js - should return true when the
363             *            validation rule should be applied
364             * @param view
365             * @return
366             */
367            @SuppressWarnings("boxing")
368            private static String createRule(InputField field, Constraint constraint, String booleanStatement, View view) {
369                    String rule = "";
370                    int constraintCount = 0;
371                    if (constraint instanceof BaseConstraint && ((BaseConstraint) constraint).getApplyClientSide()) {
372                            if (constraint instanceof SimpleConstraint) {
373                                    if (((SimpleConstraint) constraint).getRequired() != null && ((SimpleConstraint) constraint).getRequired()) {
374                                            rule = rule + "required: function(element){\nreturn (" + booleanStatement + ");}";
375                                            //special requiredness indicator handling
376                                            String showIndicatorScript = "";
377                                            for(String checkedField: parseOutFields(booleanStatement)){
378                                                showIndicatorScript = showIndicatorScript + 
379                                                    "setupShowReqIndicatorCheck('"+ checkedField +"', '" + field.getBindingInfo().getBindingPath() + "', " + "function(){\nreturn (" + booleanStatement + ");});\n";
380                                            }
381                                            addScriptToPage(view, field, showIndicatorScript);
382                                            
383                                            constraintCount++;
384                                    }
385                                    if (((SimpleConstraint) constraint).getMinLength() != null) {
386                                            if (constraintCount > 0) {
387                                                    rule = rule + ",\n";
388                                            }
389                                            rule = rule + "minLengthConditional: [" + ((SimpleConstraint) constraint).getMinLength()
390                                                            + ", function(){return " + booleanStatement + ";}]";
391                                            constraintCount++;
392                                    }
393                                    if (((SimpleConstraint) constraint).getMaxLength() != null) {
394                                            if (constraintCount > 0) {
395                                                    rule = rule + ",\n";
396                                            }
397                                            rule = rule + "maxLengthConditional: [" + ((SimpleConstraint) constraint).getMaxLength()
398                                                            + ", function(){return " + booleanStatement + ";}]";
399                                            constraintCount++;
400                                    }
401    
402                                    if (((SimpleConstraint) constraint).getExclusiveMin() != null) {
403                                            if (constraintCount > 0) {
404                                                    rule = rule + ",\n";
405                                            }
406                                            rule = rule + "minExclusive: [" + ((SimpleConstraint) constraint).getExclusiveMin()
407                                                            + ", function(){return " + booleanStatement + ";}]";
408                                            constraintCount++;
409                                    }
410    
411                                    if (((SimpleConstraint) constraint).getInclusiveMax() != null) {
412                                            if (constraintCount > 0) {
413                                                    rule = rule + ",\n";
414                                            }
415                                            rule = rule + "maxInclusive: [" + ((SimpleConstraint) constraint).getInclusiveMax()
416                                                            + ", function(){return " + booleanStatement + ";}]";
417                                            constraintCount++;
418                                    }
419    
420                                    rule = "jq('[name=\"" + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath()) + "\"]').rules(\"add\", {" + rule + "\n});";
421                            }
422                            else if (constraint instanceof ValidCharactersConstraint) {
423                                    String regexMethod = "";
424                                    String methodName = "";
425                                    if(StringUtils.isNotEmpty(((ValidCharactersConstraint)constraint).getValue())) {
426                                            regexMethod = ClientValidationUtils.getRegexMethodWithBooleanCheck(field, (ValidCharactersConstraint) constraint) + "\n";
427                                            methodName = "validChar-" + field.getBindingInfo().getBindingPath() + methodKey;
428                                            methodKey++;
429                                    }
430                                    else {
431                                            if(StringUtils.isNotEmpty(((ValidCharactersConstraint)constraint).getLabelKey())){
432                                                    methodName = ((ValidCharactersConstraint)constraint).getLabelKey();
433                                            }
434                                    }
435                                    if (StringUtils.isNotEmpty(methodName)) {
436                                            rule = regexMethod + "jq('[name=\"" + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath()) + "\"]').rules(\"add\", {\n\"" + methodName
437                                                            + "\" : function(element){return (" + booleanStatement + ");}\n});";
438                                    }
439                            }
440                            else if (constraint instanceof PrerequisiteConstraint) {
441                                    processPrerequisiteConstraint(field, (PrerequisiteConstraint) constraint, view, booleanStatement);
442                            }
443                            else if (constraint instanceof CaseConstraint) {
444                                    processCaseConstraint(field, view, (CaseConstraint) constraint, booleanStatement);
445                            }
446                            else if (constraint instanceof MustOccurConstraint) {
447                                    processMustOccurConstraint(field, view, (MustOccurConstraint) constraint, booleanStatement);
448                            }
449                    }
450                    return rule;
451            }
452            
453    
454    
455            /**
456             * This method is a simpler version of processPrerequisiteConstraint
457             * 
458             * @see ClientValidationUtils#processPrerequisiteConstraint(org.kuali.rice.krad.uif.field.InputField, PrerequisiteConstraint, View, String)
459             * @param constraint
460             * @param view
461             */
462            public static void processPrerequisiteConstraint(InputField field, PrerequisiteConstraint constraint, View view) {
463                    processPrerequisiteConstraint(field, constraint, view, "true");
464            }
465    
466            /**
467             * This method processes a Prerequisite constraint that should be applied
468             * when the booleanStatement passed in evaluates to true.
469             * 
470             * @param constraint
471             *            prerequisiteConstraint
472             * @param view
473             * @param booleanStatement
474             *            the booleanstatement in js - should return true when the
475             *            validation rule should be applied
476             */
477            public static void processPrerequisiteConstraint(InputField field, PrerequisiteConstraint constraint, View view, String booleanStatement) {
478                    if (constraint != null && constraint.getApplyClientSide().booleanValue()) {
479                        String dependsClass = "dependsOn-" + ScriptUtils.escapeName(constraint.getPropertyName());
480                        String addClass = "jq('[name=\""+ ScriptUtils.escapeName(field.getBindingInfo().getBindingPath()) + "\"]').addClass('" + dependsClass + "');" +
481                            "jq('[name=\""+ ScriptUtils.escapeName(constraint.getPropertyName()) + "\"]').addClass('" + "dependsOn-" + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath()) + "');";
482                            addScriptToPage(view, field, addClass + getPrerequisiteStatement(field, view, constraint, booleanStatement)
483                                            + getPostrequisiteStatement(field, constraint, booleanStatement));
484                    //special requiredness indicator handling
485                    String showIndicatorScript = "setupShowReqIndicatorCheck('"+ ScriptUtils.escapeName(field.getBindingInfo().getBindingPath()) +"', '" + ScriptUtils.escapeName(constraint.getPropertyName()) + "', " + "function(){\nreturn (coerceValue('" + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath()) + "') && " + booleanStatement + ");});\n";
486                    addScriptToPage(view, field, showIndicatorScript);
487                    }
488            }
489    
490            /**
491             * This method creates the script necessary for executing a prerequisite
492             * rule in which this field occurs after the field specified in the
493             * prerequisite rule - since it requires a specific set of UI logic. Builds
494             * an if statement containing an addMethod jquery validator call. Adds a
495             * "dependsOn" css class to this field for the field specified.
496             * 
497             * @param constraint
498             *            prerequisiteConstraint
499             * @param booleanStatement
500             *            the booleanstatement in js - should return true when the
501             *            validation rule should be applied
502             * @return
503             */
504            private static String getPrerequisiteStatement(InputField field, View view, PrerequisiteConstraint constraint, String booleanStatement) {
505                    methodKey++;
506                    String message = "";
507                    if(StringUtils.isEmpty(constraint.getLabelKey())){
508                            message = configService.getPropertyValueAsString(UifConstants.Messages.VALIDATION_MSG_KEY_PREFIX + "prerequisite");
509                    }
510                    else{
511                            message = generateMessageFromLabelKey(constraint.getValidationMessageParams(), constraint.getLabelKey());
512                    }
513                    if(StringUtils.isEmpty(message)){
514                            message = "prerequisite - No message";
515                    }
516                    else{
517                            InputField requiredField = (InputField) view.getViewIndex().getDataFieldByPath(constraint.getPropertyName());
518                            if(requiredField != null && StringUtils.isNotEmpty(requiredField.getLabel())){
519                                    message = MessageFormat.format(message, requiredField.getLabel());
520                            }
521                            else{
522                                    message = MessageFormat.format(message, configService.getPropertyValueAsString(GENERIC_FIELD_MSG_KEY));
523                            }
524                    }
525                    
526                    // field occurs before case
527                    String methodName = "prConstraint-" + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath()) + methodKey;
528                    String addClass = "jq('[name=\""+ ScriptUtils.escapeName(field.getBindingInfo().getBindingPath()) + "\"]').addClass('" + methodName + "');\n";
529                    String method = "\njQuery.validator.addMethod(\""+ methodName +"\", function(value, element) {\n" +
530                            " if(" + booleanStatement + "){ return (this.optional(element) || (coerceValue('" + ScriptUtils.escapeName(constraint.getPropertyName()) + "')));}else{return true;} " +
531                            "}, \"" + message + "\");";
532                    
533                    String ifStatement = "if(occursBefore('" + ScriptUtils.escapeName(constraint.getPropertyName()) + "','" + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath()) +
534                    "')){" + addClass + method + "}";
535                    return ifStatement;
536            }
537    
538            /**
539             * This method creates the script necessary for executing a prerequisite
540             * rule in which this field occurs before the field specified in the
541             * prerequisite rule - since it requires a specific set of UI logic. Builds
542             * an if statement containing an addMethod jquery validator call.
543             * 
544             * @param constraint
545             *            prerequisiteConstraint
546             * @param booleanStatement
547             *            the booleanstatement in js - should return true when the
548             *            validation rule should be applied
549             * @return
550             */
551            private static String getPostrequisiteStatement(InputField field, PrerequisiteConstraint constraint, String booleanStatement) {
552                    // field occurs after case
553                    String message = "";
554                    if(StringUtils.isEmpty(constraint.getLabelKey())){
555                            message = configService.getPropertyValueAsString(UifConstants.Messages.VALIDATION_MSG_KEY_PREFIX + "postrequisite");
556                    }
557                    else{
558                            message = generateMessageFromLabelKey(constraint.getValidationMessageParams(), constraint.getLabelKey());
559                    }
560                    
561                    if(StringUtils.isEmpty(constraint.getLabelKey())){
562                            if(StringUtils.isNotEmpty(field.getLabel())){
563                                    message = MessageFormat.format(message, field.getLabel());
564                            }
565                            else{
566                                    message = MessageFormat.format(message, configService.getPropertyValueAsString(GENERIC_FIELD_MSG_KEY));
567                            }
568                            
569                    }
570                    
571                    String function = "function(element){\n" +
572                            "return (coerceValue('"+ ScriptUtils.escapeName(field.getBindingInfo().getBindingPath()) + "') && " + booleanStatement + ");}";
573                    String postStatement = "\nelse if(occursBefore('" + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath()) + "','" + ScriptUtils.escapeName(constraint.getPropertyName()) +
574                            "')){\njq('[name=\""+ ScriptUtils.escapeName(constraint.getPropertyName()) +
575                            "\"]').rules(\"add\", { required: \n" + function 
576                            + ", \nmessages: {\nrequired: \""+ message +"\"}});}\n";
577                    
578                    return postStatement;
579    
580            }
581    
582            /**
583             * This method processes the MustOccurConstraint. The constraint is only
584             * applied when the booleanStatement evaluates to true during validation.
585             * This method creates the addMethod and add rule calls for the jquery
586             * validation plugin necessary for applying this constraint to this field.
587             * 
588             * @param view
589             * @param mc
590             * @param booleanStatement
591             *            the booleanstatement in js - should return true when the
592             *            validation rule should be applied
593             */
594            public static void processMustOccurConstraint(InputField field, View view, MustOccurConstraint mc, String booleanStatement) {
595                    methodKey++;
596                    mustOccursPathNames = new ArrayList<List<String>>();
597                    // TODO make this show the fields its requiring
598                    String methodName = "moConstraint-" + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath()) + methodKey;
599                    String method = "\njQuery.validator.addMethod(\""+ methodName +"\", function(value, element) {\n" +
600                    " if(" + booleanStatement + "){return (this.optional(element) || ("+ getMustOccurStatement(field, mc) + "));}else{return true;}" +
601                    "}, \"" + getMustOccursMessage(view, mc) +"\");";
602                    String rule = method + "jq('[name=\""+ ScriptUtils.escapeName(field.getBindingInfo().getBindingPath()) + "\"]').rules(\"add\", {\n\"" + methodName + "\": function(element){return (" + booleanStatement + ");}\n});";
603                    addScriptToPage(view, field, rule);
604            }
605    
606            /**
607             * This method takes in a MustOccurConstraint and returns the statement used
608             * in determining if the must occurs constraint has been satisfied when this
609             * field is validated. Note the use of the mustOccurCheck method. Nested
610             * mustOccurConstraints are ored against the result of the mustOccurCheck by
611             * calling this method recursively.
612             * 
613             * @param constraint
614             * @return
615             */
616            @SuppressWarnings("boxing")
617            private static String getMustOccurStatement(InputField field, MustOccurConstraint constraint) {
618                    String statement = "";
619                    List<String> attributePaths = new ArrayList<String>();
620                    if (constraint != null && constraint.getApplyClientSide()) {
621                            String array = "[";
622                            if (constraint.getPrerequisiteConstraints() != null) {
623                                    for (int i = 0; i < constraint.getPrerequisiteConstraints().size(); i++) {
624                                            field.getControl().addStyleClass("dependsOn-"
625                                                            + constraint.getPrerequisiteConstraints().get(i).getPropertyName());
626                                            array = array + "'" + ScriptUtils.escapeName(constraint.getPrerequisiteConstraints().get(i).getPropertyName()) + "'";
627                                            attributePaths.add(constraint.getPrerequisiteConstraints().get(i).getPropertyName());
628                                            if (i + 1 != constraint.getPrerequisiteConstraints().size()) {
629                                                    array = array + ",";
630                                            }
631    
632                                    }
633                            }
634                            array = array + "]";
635                            statement = "mustOccurTotal(" + array + ", " + constraint.getMin() + ", " + constraint.getMax() + ")";
636                            //add min to string list
637                            if(constraint.getMin()!=null){
638                                    attributePaths.add(constraint.getMin().toString());
639                            }
640                            else{
641                                    attributePaths.add(null);
642                            }
643                            //add max to string list
644                            if(constraint.getMax()!=null){
645                                    attributePaths.add(constraint.getMax().toString());
646                            }
647                            else{
648                                    attributePaths.add(null);
649                            }
650                            
651                            mustOccursPathNames.add(attributePaths);
652                            if(StringUtils.isEmpty(statement)){
653                                    statement = "0";
654                            }
655                            if (constraint.getMustOccurConstraints() != null) {
656                                    for (MustOccurConstraint mc : constraint.getMustOccurConstraints()) {
657                                            statement = "mustOccurCheck(" + statement + " + " + getMustOccurStatement(field, mc) +
658                                                    ", " + constraint.getMin() + ", " + constraint.getMax() + ")";
659                                    }
660                            }
661                            else{
662                                    statement = "mustOccurCheck(" + statement +
663                                    ", " + constraint.getMin() + ", " + constraint.getMax() + ")";
664                            }
665                    }
666                    return statement;
667            }
668    
669            
670            /**
671             * Generates a message for the must occur constraint (if no label key is specified).  
672             * This message is most accurate when must occurs is a single
673             * or double level constraint.  Beyond that, the message will still be accurate but may be confusing for
674             * the user - this auto-generated message however will work in MOST use cases.
675             * 
676             * @param view
677             * @return
678             */
679            private static String getMustOccursMessage(View view, MustOccurConstraint constraint){
680                    String message = "";
681                    if(StringUtils.isNotEmpty(constraint.getLabelKey())){
682                            message = generateMessageFromLabelKey(constraint.getValidationMessageParams(), constraint.getLabelKey());
683                    }
684                    else{
685                            String and = configService.getPropertyValueAsString(AND_MSG_KEY);
686                            String or = configService.getPropertyValueAsString(OR_MSG_KEY);
687                            String all = configService.getPropertyValueAsString(ALL_MSG_KEY);
688                            String atMost = configService.getPropertyValueAsString(ATMOST_MSG_KEY);
689                            String genericLabel = configService.getPropertyValueAsString(GENERIC_FIELD_MSG_KEY);
690                            String mustOccursMsg = configService.getPropertyValueAsString(
691                                    UifConstants.Messages.VALIDATION_MSG_KEY_PREFIX + MUSTOCCURS_MSG_KEY);
692                            //String postfix = configService.getPropertyValueAsString(VALIDATION_MSG_KEY_PREFIX + MUSTOCCURS_POST_MSG_KEY);
693                            String statement="";
694                            for(int i=0; i< mustOccursPathNames.size(); i++){
695                                    String andedString = "";
696                                    
697                                    List<String> paths = mustOccursPathNames.get(i);
698                                    if(!paths.isEmpty()){
699                                            //note that the last 2 strings are min and max and rest are attribute paths
700                                            String min = paths.get(paths.size()-2);
701                                            String max = paths.get(paths.size()-1);
702                                            for(int j=0; j<paths.size()-2;j++){
703                                                    InputField field = (InputField) view.getViewIndex().getDataFieldByPath(paths.get(j).trim());
704                                                    String label = genericLabel;
705                                                    if(field != null && StringUtils.isNotEmpty(field.getLabel())){
706                                                            label = field.getLabel();
707                                                    }
708                                                    if(min.equals(max)){
709                                                            if(j==0){
710                                                                    andedString = label;
711                                                            }
712                                                            else if(j==paths.size()-3){
713                                                                    andedString = andedString + " " + and + " " + label;
714                                                            }
715                                                            else{
716                                                                    andedString = andedString + ", " + label;
717                                                            }
718                                                    }
719                                                    else{
720                                                            andedString = andedString + "<li>" + label + "</li>";
721                                                    }
722                                            }
723                                            if(min.equals(max)){
724                                                    andedString = "<li>" + andedString + "</li>";
725                                            }
726                                            andedString="<ul>" + andedString + "</ul>";
727                                    
728                                            if(StringUtils.isNotEmpty(min) && StringUtils.isNotEmpty(max) && !min.equals(max)){
729                                                    andedString = MessageFormat.format(mustOccursMsg, min + "-" + max) + "<br/>" +andedString;
730                                            }
731                                            else if(StringUtils.isNotEmpty(min) && StringUtils.isNotEmpty(max) && min.equals(max) && i==0){
732                                                    andedString = MessageFormat.format(mustOccursMsg, all) + "<br/>" +andedString;
733                                            }
734                                            else if(StringUtils.isNotEmpty(min) && StringUtils.isNotEmpty(max) && min.equals(max) && i!=0){
735                                                    //leave andedString as is
736                                            }
737                                            else if(StringUtils.isNotEmpty(min)){
738                                                    andedString = MessageFormat.format(mustOccursMsg, min) + "<br/>" +andedString;
739                                            }
740                                            else if(StringUtils.isNotEmpty(max)){
741                                                    andedString = MessageFormat.format(mustOccursMsg, atMost + " " + max) + "<br/>" +andedString;
742                                            }
743                                    }
744                                    if(StringUtils.isNotEmpty(andedString)){
745                                            if(i==0){
746                                                    statement = andedString;
747                                            }
748                                            else{
749                                                    statement = statement + or.toUpperCase() + andedString;
750                                            }
751                                    }
752                            }
753                            if(StringUtils.isNotEmpty(statement)){
754                                    message = statement;
755                            }
756                    }
757                    
758                    return message;
759            }
760    
761            /**
762             * This method processes all the constraints on the InputField passed in and adds all the necessary
763             * jQuery and js required (validator's rules, methods, and messages) to the View's onDocumentReady call.
764             * The result is js that will validate all the constraints contained on an InputField during user interaction
765             * with the field using the jQuery validation plugin and custom code.
766             * 
767             * @param field
768             */
769            @SuppressWarnings("boxing")
770            public static void processAndApplyConstraints(InputField field, View view) {
771                    methodKey = 0;
772            if (view instanceof FormView && ((FormView) view).isValidateClientSide()) {
773                if(field.getSimpleConstraint() != null && field.getSimpleConstraint().getApplyClientSide()){
774                    if ((field.getRequired() != null) && (field.getRequired().booleanValue())) {
775                        field.getControl().addStyleClass("required");
776                    }
777            
778                    if (field.getExclusiveMin() != null) {
779                        if (field.getControl() instanceof TextControl && ((TextControl) field.getControl()).getDatePicker() != null) {
780                            ((TextControl) field.getControl()).getDatePicker().getComponentOptions().put("minDate", field.getExclusiveMin());
781                        }
782                        else{
783                            String rule = "jq('[name=\""+ ScriptUtils.escapeName(field.getBindingInfo().getBindingPath()) + "\"]').rules(\"add\", {\n minExclusive: ["+ field.getExclusiveMin() + "]});";
784                            addScriptToPage(view, field, rule);
785                        }
786                    }
787            
788                    if (field.getInclusiveMax() != null) {
789                        if (field.getControl() instanceof TextControl && ((TextControl) field.getControl()).getDatePicker() != null) {
790                            ((TextControl) field.getControl()).getDatePicker().getComponentOptions().put("maxDate", field.getInclusiveMax());
791                        }
792                        else{
793                            String rule = "jq('[name=\""+ ScriptUtils.escapeName(field.getBindingInfo().getBindingPath()) + "\"]').rules(\"add\", {\n maxInclusive: ["+ field.getInclusiveMax() + "]});";
794                            addScriptToPage(view, field, rule);
795                        }
796                    }
797                }
798        
799                if (field.getValidCharactersConstraint() != null && field.getValidCharactersConstraint().getApplyClientSide()) {
800                    if(StringUtils.isNotEmpty(field.getValidCharactersConstraint().getValue())) {
801                        // set regex value takes precedence
802                        addScriptToPage(view, field, ClientValidationUtils.getRegexMethod(field, field.getValidCharactersConstraint()));
803                        field.getControl().addStyleClass("validChar-" + field.getBindingInfo().getBindingPath()+ methodKey);
804                        methodKey++;
805                    }
806                    else {
807                        //blindly assume that if there is no regex value defined that there must be a method by this name
808                        if(StringUtils.isNotEmpty(field.getValidCharactersConstraint().getLabelKey())){
809                            field.getControl().addStyleClass(field.getValidCharactersConstraint().getLabelKey());
810                        }
811                    }
812                }
813        
814                if (field.getCaseConstraint() != null && field.getCaseConstraint().getApplyClientSide()) {
815                    processCaseConstraint(field, view, field.getCaseConstraint(), null);
816                }
817        
818                if (field.getDependencyConstraints() != null) {
819                    for (PrerequisiteConstraint prc : field.getDependencyConstraints()) {
820                        processPrerequisiteConstraint(field, prc, view);
821                    }
822                }
823        
824                if (field.getMustOccurConstraints() != null) {
825                    for (MustOccurConstraint mc : field.getMustOccurConstraints()) {
826                        processMustOccurConstraint(field, view, mc, "true");
827                    }
828                }
829                
830            }
831        }
832    }