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