View Javadoc

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