001    /**
002     * Copyright 2005-2014 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.krad.datadictionary.state.StateMapping;
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.messages.MessageService;
029    import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
030    import org.kuali.rice.krad.uif.UifConstants;
031    import org.kuali.rice.krad.uif.control.TextControl;
032    import org.kuali.rice.krad.uif.field.InputField;
033    import org.kuali.rice.krad.uif.view.FormView;
034    import org.kuali.rice.krad.uif.view.View;
035    import org.kuali.rice.krad.uif.widget.DatePicker;
036    import org.kuali.rice.krad.util.KRADUtils;
037    
038    import java.text.MessageFormat;
039    import java.util.ArrayList;
040    import java.util.EnumSet;
041    import java.util.HashMap;
042    import java.util.List;
043    import java.util.Map;
044    
045    /**
046     * Contains all the methods necessary for generating the js required to perform validation client
047     * side. The processAndApplyConstraints(InputField field, View view) is the key method of this class
048     * used by InputField to setup its client side validation mechanisms.
049     * 
050     * Methods now take into account state based validation and states on constraints.
051     * 
052     * @author Kuali Rice Team (rice.collab@kuali.org)
053     */
054    public class ClientValidationUtils {
055        // used to give validation methods unique signatures
056        private static int methodKey = 0;
057    
058        // list used to temporarily store mustOccurs field names for the error
059        // message
060        private static List<List<String>> mustOccursPathNames;
061    
062        public static final String LABEL_KEY_SPLIT_PATTERN = ",";
063    
064        public static final String PREREQ_MSG_KEY = "prerequisite";
065        public static final String POSTREQ_MSG_KEY = "postrequisite";
066        public static final String MUSTOCCURS_MSG_KEY = "mustOccurs";
067        public static final String MUSTOCCURS_MSG_EQUAL_KEY = "mustOccursEqualMinMax";
068        public static final String GENERIC_FIELD_MSG_KEY = "general.genericFieldName";
069    
070        public static final String ALL_MSG_KEY = "general.all";
071        public static final String ATMOST_MSG_KEY = "general.atMost";
072        public static final String AND_MSG_KEY = "general.and";
073        public static final String OR_MSG_KEY = "general.or";
074    
075        // enum representing names of rules provided by the jQuery plugin
076        public static enum ValidationMessageKeys {
077            REQUIRED("required"),
078            MIN_EXCLUSIVE("minExclusive"),
079            MAX_INCLUSIVE("maxInclusive"),
080            MIN_LENGTH("minLengthConditional"),
081            MAX_LENGTH("maxLengthConditional");
082    
083            private ValidationMessageKeys(String name) {
084                this.name = name;
085            }
086    
087            private final String name;
088    
089            @Override
090            public String toString() {
091                return name;
092            }
093    
094            public static boolean contains(String name) {
095                for (ValidationMessageKeys element : EnumSet.allOf(ValidationMessageKeys.class)) {
096                    if (element.toString().equalsIgnoreCase(name)) {
097                        return true;
098                    }
099                }
100                return false;
101            }
102        }
103    
104        /**
105         * Returns formatted message text for the given message namespace, component, and key
106         * 
107         * @param namespace namespace code the message is associated with, if null the default namespace
108         *        will be used
109         * @param componentCode component code the message is associated with, if null default component
110         *        code is used
111         * @param messageKey key for the message to retrieve
112         * @param params list of parameters for the message text
113         * @return formatted message text
114         */
115        public static String generateMessageText(String namespace, String componentCode, String messageKey,
116                List<String> params) {
117            String message = "NO MESSAGE";
118            if (StringUtils.isNotEmpty(messageKey)) {
119                message = KRADServiceLocatorWeb.getMessageService().getMessageText(namespace, componentCode, messageKey);
120                if (params != null && !params.isEmpty() && StringUtils.isNotEmpty(message)) {
121                    message = MessageFormat.format(message, params.toArray());
122                    message = MessageStructureUtils.translateStringMessage(message);
123                }
124            }
125    
126            if (StringUtils.isEmpty(message)) {
127                message = messageKey;
128            }
129    
130            //replace characters that might cause issues with their equivalent html codes
131            message = KRADUtils.convertToHTMLAttributeSafeString(message);
132    
133            return message;
134        }
135    
136        /**
137         * Generates the js object used to override all default messages for validator jquery plugin
138         * with custom messages retrieved from the message service
139         * 
140         * @return script for message override
141         */
142        public static String generateValidatorMessagesOption() {
143            MessageService messageService = KRADServiceLocatorWeb.getMessageService();
144    
145            String mOption = "";
146            String keyValuePairs = "";
147            for (ValidationMessageKeys element : EnumSet.allOf(ValidationMessageKeys.class)) {
148                String key = element.toString();
149                String message = messageService.getMessageText(UifConstants.Messages.VALIDATION_MSG_KEY_PREFIX + key);
150    
151                if (StringUtils.isNotEmpty(message)) {
152                    message = MessageStructureUtils.translateStringMessage(message);
153                    keyValuePairs = keyValuePairs + "\n" + key + ": '" + message + "',";
154                }
155            }
156    
157            keyValuePairs = StringUtils.removeEnd(keyValuePairs, ",");
158            if (StringUtils.isNotEmpty(keyValuePairs)) {
159                mOption = "{" + keyValuePairs + "}";
160            }
161    
162            return mOption;
163        }
164    
165        /**
166         * Returns the add method jquery validator call for the regular expression stored in
167         * validCharactersConstraint.
168         * 
169         * @param validCharactersConstraint
170         * @return js validator.addMethod script
171         */
172        public static String getRegexMethod(InputField field, ValidCharactersConstraint validCharactersConstraint) {
173            String message = generateMessageText(validCharactersConstraint.getMessageNamespaceCode(),
174                    validCharactersConstraint.getMessageComponentCode(), validCharactersConstraint.getMessageKey(),
175                    validCharactersConstraint.getValidationMessageParams());
176            String key = "validChar-" + field.getBindingInfo().getBindingPath() + methodKey;
177    
178            // replace characters known to cause issues if not escaped
179            String regex = validCharactersConstraint.getValue();
180            if (regex.contains("\\\\")) {
181                regex = regex.replaceAll("\\\\", "\\\\\\\\");
182            }
183            if (regex.contains("/")) {
184                regex = regex.replace("/", "\\/");
185            }
186    
187            return "\njQuery.validator.addMethod(\""
188                    + ScriptUtils.escapeName(key)
189                    + "\", function(value, element) {\n "
190                    + "return this.optional(element) || /"
191                    + regex
192                    + "/.test(value);"
193                    + "}, \""
194                    + message
195                    + "\");";
196        }
197    
198        /**
199         * Returns the add method jquery validator call for the regular expression stored in
200         * validCharactersConstraint that explicitly checks a boolean. Needed because one method accepts
201         * params and the other doesn't.
202         * 
203         * @param validCharactersConstraint
204         * @return js validator.addMethod script
205         */
206        public static String getRegexMethodWithBooleanCheck(InputField field,
207                ValidCharactersConstraint validCharactersConstraint) {
208            String message = generateMessageText(validCharactersConstraint.getMessageNamespaceCode(),
209                    validCharactersConstraint.getMessageComponentCode(), validCharactersConstraint.getMessageKey(),
210                    validCharactersConstraint.getValidationMessageParams());
211            String key = "validChar-" + field.getBindingInfo().getBindingPath() + methodKey;
212    
213            // replace characters known to cause issues if not escaped
214            String regex = validCharactersConstraint.getValue();
215            if (regex.contains("\\\\")) {
216                regex = regex.replaceAll("\\\\", "\\\\\\\\");
217            }
218            if (regex.contains("/")) {
219                regex = regex.replace("/", "\\/");
220            }
221    
222            return "\njQuery.validator.addMethod(\""
223                    + ScriptUtils.escapeName(key)
224                    + "\", function(value, element, doCheck) {\n if(doCheck === false){return true;}else{"
225                    + "return this.optional(element) || /"
226                    + regex
227                    + "/.test(value);}"
228                    + "}, \""
229                    + message
230                    + "\");";
231        }
232    
233        /**
234         * This method processes a single CaseConstraint. Internally it makes calls to
235         * processWhenConstraint for each WhenConstraint that exists in this constraint. It adds a
236         * "dependsOn" css class to this field for the field which the CaseConstraint references.
237         * 
238         * @param view
239         * @param andedCase the boolean logic to be anded when determining if this case is satisfied
240         *        (used for nested CaseConstraints)
241         */
242        public static void processCaseConstraint(InputField field, View view, CaseConstraint constraint, String andedCase,
243                String validationState, StateMapping stateMapping) {
244            if (constraint.getOperator() == null) {
245                constraint.setOperator("equals");
246            }
247    
248            String operator = "==";
249            if (constraint.getOperator().equalsIgnoreCase("not_equals") || constraint.getOperator().equalsIgnoreCase(
250                    "not_equal")) {
251                operator = "!=";
252            } else if (constraint.getOperator().equalsIgnoreCase("greater_than_equal")) {
253                operator = ">=";
254            } else if (constraint.getOperator().equalsIgnoreCase("less_than_equal")) {
255                operator = "<=";
256            } else if (constraint.getOperator().equalsIgnoreCase("greater_than")) {
257                operator = ">";
258            } else if (constraint.getOperator().equalsIgnoreCase("less_than")) {
259                operator = "<";
260            } else if (constraint.getOperator().equalsIgnoreCase("has_value")) {
261                operator = "";
262            }
263            // add more operator types here if more are supported later
264    
265            field.getControl().addStyleClass("dependsOn-" + ScriptUtils.escapeName(constraint.getPropertyName()));
266    
267            if (constraint.getWhenConstraint() != null && !constraint.getWhenConstraint().isEmpty()) {
268                //String fieldPath = field.getBindingInfo().getBindingObjectPath() + "." + constraint.getPropertyName();
269                String fieldPath = constraint.getPropertyName();
270                for (WhenConstraint wc : constraint.getWhenConstraint()) {
271                    wc = ConstraintStateUtils.getApplicableConstraint(wc, validationState, stateMapping);
272                    if (wc != null) {
273                        processWhenConstraint(field, view, constraint, wc, ScriptUtils.escapeName(fieldPath), operator,
274                                andedCase, validationState, stateMapping);
275                    }
276                }
277            }
278        }
279    
280        /**
281         * This method processes the WhenConstraint passed in. The when constraint is used to create a
282         * boolean statement to determine if the constraint will be applied. The necessary rules/methods
283         * for applying this constraint are created in the createRule call. Note the use of the use of
284         * coerceValue js function call.
285         * 
286         * @param view
287         * @param wc
288         * @param fieldPath
289         * @param operator
290         * @param andedCase
291         */
292        private static void processWhenConstraint(InputField field, View view, CaseConstraint caseConstraint,
293                WhenConstraint wc, String fieldPath, String operator, String andedCase, String validationState,
294                StateMapping stateMapping) {
295            String ruleString = "";
296            // prerequisite constraint
297    
298            String booleanStatement = "";
299            if (wc.getValues() != null) {
300    
301                String caseStr = "";
302                if (!caseConstraint.isCaseSensitive()) {
303                    caseStr = ".toUpperCase()";
304                }
305                for (int i = 0; i < wc.getValues().size(); i++) {
306                    if (operator.isEmpty()) {
307                        // has_value case
308                        if (wc.getValues().get(i) instanceof String && ((String) wc.getValues().get(i)).equalsIgnoreCase(
309                                "false")) {
310                            booleanStatement = booleanStatement + "(coerceValue('" + fieldPath + "') == '')";
311                        } else {
312                            booleanStatement = booleanStatement + "(coerceValue('" + fieldPath + "') != '')";
313                        }
314                    } else {
315                        // everything else
316                        booleanStatement = booleanStatement
317                                + "(coerceValue('"
318                                + fieldPath
319                                + "')"
320                                + caseStr
321                                + " "
322                                + operator
323                                + " \""
324                                + wc.getValues().get(i)
325                                + "\""
326                                + caseStr
327                                + ")";
328                    }
329                    if ((i + 1) != wc.getValues().size()) {
330                        booleanStatement = booleanStatement + " || ";
331                    }
332                }
333    
334            }
335    
336            if (andedCase != null) {
337                booleanStatement = "(" + booleanStatement + ") && (" + andedCase + ")";
338            }
339    
340            if (wc.getConstraint() != null && StringUtils.isNotEmpty(booleanStatement)) {
341                Constraint constraint = ConstraintStateUtils.getApplicableConstraint(wc.getConstraint(), validationState,
342                        stateMapping);
343                if (constraint != null) {
344                    ruleString = createRule(field, constraint, booleanStatement, view, validationState, stateMapping);
345                }
346            }
347    
348            if (StringUtils.isNotEmpty(ruleString)) {
349                addScriptToPage(view, field, ruleString);
350            }
351        }
352    
353        /**
354         * Adds the script to the view to execute on a jQuery document ready event.
355         * 
356         * @param view
357         * @param script
358         */
359        public static void addScriptToPage(View view, InputField field, String script) {
360            String prefixScript = "";
361    
362            if (field.getOnDocumentReadyScript() != null) {
363                prefixScript = field.getOnDocumentReadyScript();
364            }
365            field.setOnDocumentReadyScript(prefixScript + "\n" + "runValidationScript(function(){" + script + "});");
366        }
367    
368        /**
369         * Determines which fields are being evaluated in a boolean statement, so handlers can be
370         * attached to them if needed, returns these names in a list.
371         * 
372         * @param statement
373         * @return
374         */
375        private static List<String> parseOutFields(String statement) {
376            List<String> fieldNames = new ArrayList<String>();
377            String[] splits = StringUtils.splitByWholeSeparator(statement, "coerceValue(");
378            for (String s : splits) {
379                //must be a coerceValue param and not preceding content from the split, always starts with "'"
380                if (!s.startsWith("'")) {
381                    continue;
382                }
383    
384                s = s.substring(1);
385                String fieldName = StringUtils.substringBefore(s, "'");
386                //Only add field name once for this condition check
387                if (fieldNames.contains(fieldName)) {
388                    fieldNames.add(fieldName);
389                }
390    
391            }
392            return fieldNames;
393        }
394    
395        /**
396         * This method takes in a constraint to apply only when the passed in booleanStatement is valid.
397         * The method will create the necessary addMethod and addRule jquery validator calls for the
398         * rule to be applied to the field when the statement passed in evaluates to true during runtime
399         * and this field is being validated. Note the use of custom methods for min/max length/value.
400         * 
401         * @param field the field to apply the generated methods and rules to
402         * @param constraint the constraint to be applied when the booleanStatement evaluates to true
403         *        during validation
404         * @param booleanStatement the booleanstatement in js - should return true when the validation
405         *        rule should be applied
406         * @param view
407         * @return
408         */
409        @SuppressWarnings("boxing")
410        private static String createRule(InputField field, Constraint constraint, String booleanStatement, View view,
411                String validationState, StateMapping stateMapping) {
412            String rule = "";
413            int constraintCount = 0;
414            if (constraint instanceof BaseConstraint && ((BaseConstraint) constraint).getApplyClientSide()) {
415                if (constraint instanceof SimpleConstraint) {
416                    if (((SimpleConstraint) constraint).getRequired() != null && ((SimpleConstraint) constraint)
417                            .getRequired()) {
418                        rule = rule + "required: function(element){\nreturn (" + booleanStatement + ");}";
419                        //special requiredness indicator handling
420                        String showIndicatorScript = "";
421                        for (String checkedField : parseOutFields(booleanStatement)) {
422                            showIndicatorScript = showIndicatorScript +
423                                    "setupShowReqIndicatorCheck('" + checkedField + "', '" + field.getBindingInfo()
424                                            .getBindingPath() + "', " + "function(){\nreturn (" + booleanStatement
425                                    + ");});\n";
426                        }
427                        addScriptToPage(view, field, showIndicatorScript);
428    
429                        constraintCount++;
430                    }
431    
432                    if (((SimpleConstraint) constraint).getMinLength() != null) {
433                        if (constraintCount > 0) {
434                            rule = rule + ",\n";
435                        }
436                        rule = rule
437                                + "minLengthConditional: ["
438                                + ((SimpleConstraint) constraint).getMinLength()
439                                + ", function(){return "
440                                + booleanStatement
441                                + ";}]";
442                        constraintCount++;
443                    }
444    
445                    if (((SimpleConstraint) constraint).getMaxLength() != null) {
446                        if (constraintCount > 0) {
447                            rule = rule + ",\n";
448                        }
449                        rule = rule
450                                + "maxLengthConditional: ["
451                                + ((SimpleConstraint) constraint).getMaxLength()
452                                + ", function(){return "
453                                + booleanStatement
454                                + ";}]";
455                        constraintCount++;
456                    }
457    
458                    if (((SimpleConstraint) constraint).getExclusiveMin() != null) {
459                        if (constraintCount > 0) {
460                            rule = rule + ",\n";
461                        }
462                        rule = rule
463                                + "minExclusive: ["
464                                + ((SimpleConstraint) constraint).getExclusiveMin()
465                                + ", function(){return "
466                                + booleanStatement
467                                + ";}]";
468                        constraintCount++;
469                    }
470    
471                    if (((SimpleConstraint) constraint).getInclusiveMax() != null) {
472                        if (constraintCount > 0) {
473                            rule = rule + ",\n";
474                        }
475                        rule = rule
476                                + "maxInclusive: ["
477                                + ((SimpleConstraint) constraint).getInclusiveMax()
478                                + ", function(){return "
479                                + booleanStatement
480                                + ";}]";
481                        constraintCount++;
482                    }
483    
484                    rule = "jQuery('[name=\""
485                            + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath())
486                            + "\"]').rules(\"add\", {"
487                            + rule
488                            + "\n});";
489                } else if (constraint instanceof ValidCharactersConstraint) {
490                    String regexMethod = "";
491                    String methodName = "";
492                    if (StringUtils.isNotEmpty(((ValidCharactersConstraint) constraint).getValue())) {
493                        regexMethod = ClientValidationUtils.getRegexMethodWithBooleanCheck(field,
494                                (ValidCharactersConstraint) constraint) + "\n";
495                        methodName = "validChar-" + field.getBindingInfo().getBindingPath() + methodKey;
496                        methodKey++;
497                    } else {
498                        if (StringUtils.isNotEmpty(((ValidCharactersConstraint) constraint).getMessageKey())) {
499                            methodName = ((ValidCharactersConstraint) constraint).getMessageKey();
500                        }
501                    }
502    
503                    if (StringUtils.isNotEmpty(methodName)) {
504                        rule = regexMethod
505                                + "jQuery('[name=\""
506                                + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath())
507                                + "\"]').rules(\"add\", {\n\""
508                                + methodName
509                                + "\" : function(element){return ("
510                                + booleanStatement
511                                + ");}\n});";
512                    }
513                } else if (constraint instanceof PrerequisiteConstraint) {
514                    processPrerequisiteConstraint(field, (PrerequisiteConstraint) constraint, view, booleanStatement);
515                } else if (constraint instanceof CaseConstraint) {
516                    processCaseConstraint(field, view, (CaseConstraint) constraint, booleanStatement, validationState,
517                            stateMapping);
518                } else if (constraint instanceof MustOccurConstraint) {
519                    processMustOccurConstraint(field, view, (MustOccurConstraint) constraint, booleanStatement);
520                }
521            }
522    
523            return rule;
524        }
525    
526        /**
527         * Simpler version of processPrerequisiteConstraint
528         * 
529         * @param constraint
530         * @param view
531         * @see ClientValidationUtils#processPrerequisiteConstraint(org.kuali.rice.krad.uif.field.InputField,
532         *      PrerequisiteConstraint, View, String)
533         */
534        public static void processPrerequisiteConstraint(InputField field, PrerequisiteConstraint constraint, View view) {
535            processPrerequisiteConstraint(field, constraint, view, "true");
536        }
537    
538        /**
539         * Processes a Prerequisite constraint that should be applied when the booleanStatement passed
540         * in evaluates to true.
541         * 
542         * @param constraint prerequisiteConstraint
543         * @param view
544         * @param booleanStatement the booleanstatement in js - should return true when the validation
545         *        rule should be applied
546         */
547        public static void processPrerequisiteConstraint(InputField field, PrerequisiteConstraint constraint, View view,
548                String booleanStatement) {
549            if (constraint != null && constraint.getApplyClientSide().booleanValue()) {
550                String dependsClass = "dependsOn-" + ScriptUtils.escapeName(constraint.getPropertyName());
551                String addClass = "jQuery('[name=\""
552                        + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath())
553                        + "\"]').addClass('"
554                        + dependsClass
555                        + "');"
556                        +
557                        "jQuery('[name=\""
558                        + ScriptUtils.escapeName(constraint.getPropertyName())
559                        + "\"]').addClass('"
560                        + "dependsOn-"
561                        + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath())
562                        + "');";
563    
564                addScriptToPage(view, field, addClass
565                        + getPrerequisiteStatement(field, view, constraint, booleanStatement)
566                        + getPostrequisiteStatement(field, constraint, booleanStatement));
567    
568                //special requiredness indicator handling
569                String showIndicatorScript = "setupShowReqIndicatorCheck('" + ScriptUtils.escapeName(
570                        field.getBindingInfo().getBindingPath()) + "', '" + ScriptUtils.escapeName(
571                        constraint.getPropertyName()) + "', " + "function(){\nreturn (coerceValue('" + ScriptUtils
572                        .escapeName(field.getBindingInfo().getBindingPath()) + "') && " + booleanStatement + ");});\n";
573    
574                addScriptToPage(view, field, showIndicatorScript);
575            }
576        }
577    
578        /**
579         * Creates the script necessary for executing a prerequisite rule in which this field occurs
580         * after the field specified in the prerequisite rule - since it requires a specific set of UI
581         * logic. Builds an if statement containing an addMethod jquery validator call. Adds a
582         * "dependsOn" css class to this field for the field specified.
583         * 
584         * @param constraint prerequisiteConstraint
585         * @param booleanStatement the booleanstatement in js - should return true when the validation
586         *        rule should be applied
587         * @return
588         */
589        private static String getPrerequisiteStatement(InputField field, View view, PrerequisiteConstraint constraint,
590                String booleanStatement) {
591            methodKey++;
592    
593            MessageService messageService = KRADServiceLocatorWeb.getMessageService();
594    
595            String message = "";
596            if (StringUtils.isEmpty(constraint.getMessageKey())) {
597                message = messageService.getMessageText(UifConstants.Messages.VALIDATION_MSG_KEY_PREFIX + "prerequisite");
598                message = MessageStructureUtils.translateStringMessage(message);
599            } else {
600                message = generateMessageText(constraint.getMessageNamespaceCode(),
601                        constraint.getMessageComponentCode(), constraint.getMessageKey(),
602                        constraint.getValidationMessageParams());
603            }
604    
605            if (StringUtils.isEmpty(message)) {
606                message = "prerequisite - No message";
607            } else {
608                InputField requiredField = (InputField) view.getViewIndex().getDataFieldByPath(
609                        constraint.getPropertyName());
610                if (requiredField != null && StringUtils.isNotEmpty(requiredField.getLabel())) {
611                    message = MessageFormat.format(message, requiredField.getLabel());
612                } else {
613                    String genericFieldLabel = messageService.getMessageText(GENERIC_FIELD_MSG_KEY);
614                    message = MessageFormat.format(message, genericFieldLabel);
615                }
616            }
617    
618            // field occurs before case
619            String methodName = "prConstraint-"
620                    + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath())
621                    + methodKey;
622    
623            String addClass = "jQuery('[name=\""
624                    + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath())
625                    + "\"]').addClass('"
626                    + methodName
627                    + "');\n";
628    
629            String method = "\njQuery.validator.addMethod(\"" + methodName + "\", function(value, element) {\n" +
630                    " if(" + booleanStatement + "){ return (this.optional(element) || (coerceValue('" + ScriptUtils
631                            .escapeName(constraint.getPropertyName()) + "')));}else{return true;} " +
632                    "}, \"" + message + "\");";
633    
634            String ifStatement = "if(occursBefore('"
635                    + ScriptUtils.escapeName(constraint.getPropertyName())
636                    + "','"
637                    + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath())
638                    +
639                    "')){"
640                    + addClass
641                    + method
642                    + "}";
643    
644            return ifStatement;
645        }
646    
647        /**
648         * This method creates the script necessary for executing a prerequisite rule in which this
649         * field occurs before the field specified in the prerequisite rule - since it requires a
650         * specific set of UI logic. Builds an if statement containing an addMethod jquery validator
651         * call.
652         * 
653         * @param constraint prerequisiteConstraint
654         * @param booleanStatement the booleanstatement in js - should return true when the validation
655         *        rule should be applied
656         * @return
657         */
658        private static String getPostrequisiteStatement(InputField field, PrerequisiteConstraint constraint,
659                String booleanStatement) {
660            MessageService messageService = KRADServiceLocatorWeb.getMessageService();
661    
662            // field occurs after case
663            String message = "";
664            if (StringUtils.isEmpty(constraint.getMessageKey())) {
665                message = messageService.getMessageText(UifConstants.Messages.VALIDATION_MSG_KEY_PREFIX + "postrequisite");
666                message = MessageStructureUtils.translateStringMessage(message);
667            } else {
668                message = generateMessageText(constraint.getMessageNamespaceCode(), constraint.getMessageComponentCode(),
669                        constraint.getMessageKey(), constraint.getValidationMessageParams());
670            }
671    
672            if (StringUtils.isEmpty(constraint.getMessageKey())) {
673                if (StringUtils.isNotEmpty(field.getLabel())) {
674                    message = MessageFormat.format(message, field.getLabel());
675                } else {
676                    String genericFieldLabel = messageService.getMessageText(GENERIC_FIELD_MSG_KEY);
677                    message = MessageFormat.format(message, genericFieldLabel);
678                }
679            }
680    
681            String function = "function(element){\n" +
682                    "return (coerceValue('"
683                    + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath())
684                    + "') && "
685                    + booleanStatement
686                    + ");}";
687            String postStatement = "\nelse if(occursBefore('"
688                    + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath())
689                    + "','"
690                    + ScriptUtils.escapeName(constraint.getPropertyName())
691                    +
692                    "')){\njQuery('[name=\""
693                    + ScriptUtils.escapeName(constraint.getPropertyName())
694                    +
695                    "\"]').rules(\"add\", { required: \n"
696                    + function
697                    + ", \nmessages: {\nrequired: \""
698                    + message
699                    + "\"}});}\n";
700    
701            return postStatement;
702    
703        }
704    
705        /**
706         * This method processes the MustOccurConstraint. The constraint is only applied when the
707         * booleanStatement evaluates to true during validation. This method creates the addMethod and
708         * add rule calls for the jquery validation plugin necessary for applying this constraint to
709         * this field.
710         * 
711         * @param view
712         * @param mc
713         * @param booleanStatement the booleanstatement in js - should return true when the validation
714         *        rule should be applied
715         */
716        public static void processMustOccurConstraint(InputField field, View view, MustOccurConstraint mc,
717                String booleanStatement) {
718            methodKey++;
719            mustOccursPathNames = new ArrayList<List<String>>();
720            // TODO make this show the fields its requiring
721            String methodName = "moConstraint-"
722                    + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath())
723                    + methodKey;
724            String method = "\njQuery.validator.addMethod(\"" + methodName + "\", function(value, element) {\n" +
725                    " if("
726                    + booleanStatement
727                    + "){return (this.optional(element) || ("
728                    + getMustOccurStatement(field, mc)
729                    + "));}else{return true;}"
730                    +
731                    "}, \""
732                    + getMustOccursMessage(view, mc)
733                    + "\");";
734            String rule = method
735                    + "jQuery('[name=\""
736                    + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath())
737                    + "\"]').rules(\"add\", {\n\""
738                    + methodName
739                    + "\": function(element){return ("
740                    + booleanStatement
741                    + ");}\n});";
742            addScriptToPage(view, field, rule);
743        }
744    
745        /**
746         * This method takes in a MustOccurConstraint and returns the statement used in determining if
747         * the must occurs constraint has been satisfied when this field is validated. Note the use of
748         * the mustOccurCheck method. Nested mustOccurConstraints are ored against the result of the
749         * mustOccurCheck by calling this method recursively.
750         * 
751         * @param constraint
752         * @return
753         */
754        @SuppressWarnings("boxing")
755        private static String getMustOccurStatement(InputField field, MustOccurConstraint constraint) {
756            String statement = "";
757            List<String> attributePaths = new ArrayList<String>();
758            if (constraint != null && constraint.getApplyClientSide()) {
759                String array = "[";
760                if (constraint.getPrerequisiteConstraints() != null) {
761                    for (int i = 0; i < constraint.getPrerequisiteConstraints().size(); i++) {
762                        field.getControl().addStyleClass("dependsOn-" + constraint.getPrerequisiteConstraints().get(i)
763                                .getPropertyName());
764                        array = array + "'" + ScriptUtils.escapeName(constraint.getPrerequisiteConstraints().get(i)
765                                .getPropertyName()) + "'";
766                        attributePaths.add(constraint.getPrerequisiteConstraints().get(i).getPropertyName());
767                        if (i + 1 != constraint.getPrerequisiteConstraints().size()) {
768                            array = array + ",";
769                        }
770    
771                    }
772                }
773                array = array + "]";
774                statement = "mustOccurTotal(" + array + ", " + constraint.getMin() + ", " + constraint.getMax() + ")";
775                //add min to string list
776                if (constraint.getMin() != null) {
777                    attributePaths.add(constraint.getMin().toString());
778                } else {
779                    attributePaths.add(null);
780                }
781                //add max to string list
782                if (constraint.getMax() != null) {
783                    attributePaths.add(constraint.getMax().toString());
784                } else {
785                    attributePaths.add(null);
786                }
787    
788                mustOccursPathNames.add(attributePaths);
789                if (StringUtils.isEmpty(statement)) {
790                    statement = "0";
791                }
792                if (constraint.getMustOccurConstraints() != null) {
793                    for (MustOccurConstraint mc : constraint.getMustOccurConstraints()) {
794                        statement = "mustOccurCheck(" + statement + " + " + getMustOccurStatement(field, mc) +
795                                ", " + constraint.getMin() + ", " + constraint.getMax() + ")";
796                    }
797                } else {
798                    statement = "mustOccurCheck(" + statement +
799                            ", " + constraint.getMin() + ", " + constraint.getMax() + ")";
800                }
801            }
802            return statement;
803        }
804    
805        /**
806         * Generates a message for the must occur constraint (if no label key is specified). This
807         * message is most accurate when must occurs is a single or double level constraint. Beyond
808         * that, the message will still be accurate but may be confusing for the user - this
809         * auto-generated message however will work in MOST use cases.
810         * 
811         * @param view
812         * @return
813         */
814        private static String getMustOccursMessage(View view, MustOccurConstraint constraint) {
815            MessageService messageService = KRADServiceLocatorWeb.getMessageService();
816    
817            String message = "";
818            if (StringUtils.isNotEmpty(constraint.getMessageKey())) {
819                message = generateMessageText(constraint.getMessageNamespaceCode(), constraint.getMessageComponentCode(),
820                        constraint.getMessageKey(), constraint.getValidationMessageParams());
821            } else {
822                String and = messageService.getMessageText(AND_MSG_KEY);
823                String or = messageService.getMessageText(OR_MSG_KEY);
824                String mustOccursMsgEqualMinMax = messageService.getMessageText(
825                        UifConstants.Messages.VALIDATION_MSG_KEY_PREFIX + MUSTOCCURS_MSG_EQUAL_KEY);
826                String atMost = messageService.getMessageText(ATMOST_MSG_KEY);
827                String genericLabel = messageService.getMessageText(GENERIC_FIELD_MSG_KEY);
828                String mustOccursMsg = messageService.getMessageText(
829                        UifConstants.Messages.VALIDATION_MSG_KEY_PREFIX + MUSTOCCURS_MSG_KEY);
830    
831                String statement = "";
832                for (int i = 0; i < mustOccursPathNames.size(); i++) {
833                    String andedString = "";
834    
835                    List<String> paths = mustOccursPathNames.get(i);
836                    if (!paths.isEmpty()) {
837                        //note that the last 2 strings are min and max and rest are attribute paths
838                        String min = paths.get(paths.size() - 2);
839                        String max = paths.get(paths.size() - 1);
840                        for (int j = 0; j < paths.size() - 2; j++) {
841                            InputField field = (InputField) view.getViewIndex().getDataFieldByPath(paths.get(j).trim());
842                            String label = genericLabel;
843                            if (field != null && StringUtils.isNotEmpty(field.getLabel())) {
844                                label = field.getLabel();
845                            }
846                            if (min.equals(max)) {
847                                if (j == 0) {
848                                    andedString = label;
849                                } else if (j == paths.size() - 3) {
850                                    andedString = andedString + " " + and + " " + label;
851                                } else {
852                                    andedString = andedString + ", " + label;
853                                }
854                            } else {
855                                andedString = andedString + "(" + label + ")";
856                            }
857                        }
858                        if (min.equals(max)) {
859                            andedString = "(" + andedString + ")";
860                        }
861    
862                        if (StringUtils.isNotBlank(andedString) && !andedString.equals("()")) {
863                            if (StringUtils.isNotEmpty(min) && StringUtils.isNotEmpty(max) && !min.equals(max)) {
864                                andedString = MessageFormat.format(mustOccursMsg, min + "-" + max) + " " + andedString;
865                            } else if (StringUtils.isNotEmpty(min)
866                                    && StringUtils.isNotEmpty(max)
867                                    && min.equals(max)
868                                    && i == 0) {
869                                andedString = mustOccursMsgEqualMinMax + " " + andedString;
870                            } else if (StringUtils.isNotEmpty(min)
871                                    && StringUtils.isNotEmpty(max)
872                                    && min.equals(max)
873                                    && i != 0) {
874                                //leave andedString as is
875                            } else if (StringUtils.isNotEmpty(min)) {
876                                andedString = MessageFormat.format(mustOccursMsg, min) + " " + andedString;
877                            } else if (StringUtils.isNotEmpty(max)) {
878                                andedString = MessageFormat.format(mustOccursMsg, atMost + " " + max) + " " + andedString;
879                            }
880                        }
881                    }
882                    if (StringUtils.isNotEmpty(andedString)) {
883                        if (StringUtils.isNotBlank(statement)) {
884                            statement = statement + " " + or.toUpperCase() + " " + andedString;
885                        } else {
886                            statement = andedString;
887                        }
888                    }
889                }
890                if (StringUtils.isNotEmpty(statement)) {
891                    message = statement;
892                    message = message.replace(")(", " " + or + " ");
893                }
894            }
895    
896            return message;
897        }
898    
899        /**
900         * This method processes all the constraints on the InputField passed in and adds all the
901         * necessary jQuery and js required (validator's rules, methods, and messages) to the View's
902         * onDocumentReady call. The result is js that will validate all the constraints contained on an
903         * InputField during user interaction with the field using the jQuery validation plugin and
904         * custom code.
905         * 
906         * @param field
907         */
908        @SuppressWarnings("boxing")
909        public static void processAndApplyConstraints(InputField field, View view, Object model) {
910            methodKey = 0;
911            String validationState = ConstraintStateUtils.getClientViewValidationState(model, view);
912            StateMapping stateMapping = view.getStateMapping();
913    
914            if (view instanceof FormView && ((FormView) view).isValidateClientSide()) {
915                SimpleConstraint simpleConstraint = ConstraintStateUtils.getApplicableConstraint(
916                        field.getSimpleConstraint(), validationState, stateMapping);
917                if (simpleConstraint != null && simpleConstraint.getApplyClientSide()) {
918    
919                    if ((simpleConstraint.getRequired() != null) && (simpleConstraint.getRequired().booleanValue())) {
920                        field.getControl().addStyleClass("required");
921                    }
922    
923                    if (simpleConstraint.getExclusiveMin() != null) {
924                        if (field.getControl() instanceof TextControl
925                                && ((TextControl) field.getControl()).getDatePicker() != null) {
926                            DatePicker datePicker = ((TextControl) field.getControl()).getDatePicker();
927                            Map<String, String> dpTemplateOptions = datePicker.getTemplateOptions();
928    
929                            if (dpTemplateOptions == null) {
930                                datePicker.setTemplateOptions(dpTemplateOptions = new HashMap<String, String>());
931                            }
932    
933                            dpTemplateOptions.put("minDate",
934                                    simpleConstraint.getExclusiveMin());
935                        } else {
936                            String rule = "jQuery('[name=\""
937                                    + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath())
938                                    + "\"]').rules(\"add\", {\n minExclusive: ["
939                                    + simpleConstraint.getExclusiveMin()
940                                    + "]});";
941                            addScriptToPage(view, field, rule);
942                        }
943                    }
944    
945                    if (simpleConstraint.getInclusiveMax() != null) {
946                        if (field.getControl() instanceof TextControl
947                                && ((TextControl) field.getControl()).getDatePicker() != null) {
948                            ((TextControl) field.getControl()).getDatePicker().getTemplateOptions().put("maxDate",
949                                    simpleConstraint.getInclusiveMax());
950                        } else {
951                            String rule = "jQuery('[name=\""
952                                    + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath())
953                                    + "\"]').rules(\"add\", {\n maxInclusive: ["
954                                    + simpleConstraint.getInclusiveMax()
955                                    + "]});";
956                            addScriptToPage(view, field, rule);
957                        }
958                    }
959                }
960    
961                ValidCharactersConstraint validCharactersConstraint = ConstraintStateUtils.getApplicableConstraint(
962                        field.getValidCharactersConstraint(), validationState, stateMapping);
963    
964                if (validCharactersConstraint != null && validCharactersConstraint.getApplyClientSide()) {
965                    if (StringUtils.isNotEmpty(validCharactersConstraint.getValue())) {
966                        // set regex value takes precedence
967                        addScriptToPage(view, field, ClientValidationUtils.getRegexMethod(field,
968                                validCharactersConstraint));
969                        field.getControl().addStyleClass(
970                                "validChar-" + field.getBindingInfo().getBindingPath() + methodKey);
971                        methodKey++;
972                    } else {
973                        //blindly assume that if there is no regex value defined that there must be a method by this name
974                        if (StringUtils.isNotEmpty(validCharactersConstraint.getMessageKey())) {
975                            field.getControl().addStyleClass(validCharactersConstraint.getMessageKey());
976                        }
977                    }
978                }
979    
980                CaseConstraint caseConstraint = ConstraintStateUtils.getApplicableConstraint(field.getCaseConstraint(),
981                        validationState, stateMapping);
982                if (caseConstraint != null && caseConstraint.getApplyClientSide()) {
983                    processCaseConstraint(field, view, caseConstraint, null, validationState, stateMapping);
984                }
985    
986                if (field.getDependencyConstraints() != null) {
987                    for (PrerequisiteConstraint prc : field.getDependencyConstraints()) {
988                        prc = ConstraintStateUtils.getApplicableConstraint(prc, validationState, stateMapping);
989                        if (prc != null) {
990                            processPrerequisiteConstraint(field, prc, view);
991                        }
992                    }
993                }
994    
995                if (field.getMustOccurConstraints() != null) {
996                    for (MustOccurConstraint mc : field.getMustOccurConstraints()) {
997                        mc = ConstraintStateUtils.getApplicableConstraint(mc, validationState, stateMapping);
998                        if (mc != null) {
999                            processMustOccurConstraint(field, view, mc, "true");
1000                        }
1001                    }
1002                }
1003    
1004            }
1005        }
1006    
1007    }