View Javadoc

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