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