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