001/**
002 * Copyright 2005-2016 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 */
016package org.kuali.rice.krad.uif.util;
017
018import org.apache.commons.lang.StringUtils;
019import org.kuali.rice.core.api.config.property.ConfigurationService;
020import org.kuali.rice.krad.datadictionary.validation.constraint.BaseConstraint;
021import org.kuali.rice.krad.datadictionary.validation.constraint.CaseConstraint;
022import org.kuali.rice.krad.datadictionary.validation.constraint.Constraint;
023import org.kuali.rice.krad.datadictionary.validation.constraint.MustOccurConstraint;
024import org.kuali.rice.krad.datadictionary.validation.constraint.PrerequisiteConstraint;
025import org.kuali.rice.krad.datadictionary.validation.constraint.SimpleConstraint;
026import org.kuali.rice.krad.datadictionary.validation.constraint.ValidCharactersConstraint;
027import org.kuali.rice.krad.datadictionary.validation.constraint.WhenConstraint;
028import org.kuali.rice.krad.service.KRADServiceLocator;
029import org.kuali.rice.krad.uif.UifConstants;
030import org.kuali.rice.krad.uif.field.InputField;
031import org.kuali.rice.krad.uif.view.FormView;
032import org.kuali.rice.krad.uif.view.View;
033import org.kuali.rice.krad.uif.control.TextControl;
034
035import java.text.MessageFormat;
036import java.util.ArrayList;
037import java.util.EnumSet;
038import 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 */
047public 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}