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 }