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