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 }