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