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