001 /**
002 * Copyright 2010 The Kuali Foundation Licensed under the Educational Community License, Version 2.0 (the "License"); you may
003 * not use this file except in compliance with the License. You may obtain a copy of the License at
004 * http://www.osedu.org/licenses/ECL-2.0 Unless required by applicable law or agreed to in writing, software distributed
005 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
006 * implied. See the License for the specific language governing permissions and limitations under the License.
007 */
008
009 package org.kuali.student.r2.common.validator;
010
011 import java.lang.reflect.InvocationTargetException;
012 import java.util.ArrayList;
013 import java.util.Collection;
014 import java.util.Date;
015 import java.util.HashMap;
016 import java.util.Iterator;
017 import java.util.List;
018 import java.util.Map;
019 import java.util.Stack;
020
021 import org.apache.commons.beanutils.PropertyUtils;
022 import org.apache.log4j.Logger;
023 import org.kuali.student.common.util.MessageUtils;
024 import org.kuali.student.r1.common.dictionary.dto.CaseConstraint;
025 import org.kuali.student.r1.common.dictionary.dto.CommonLookupParam;
026 import org.kuali.student.r1.common.dictionary.dto.Constraint;
027 import org.kuali.student.r1.common.dictionary.dto.DataType;
028 import org.kuali.student.r1.common.dictionary.dto.FieldDefinition;
029 import org.kuali.student.r1.common.dictionary.dto.LookupConstraint;
030 import org.kuali.student.r1.common.dictionary.dto.MustOccurConstraint;
031 import org.kuali.student.r1.common.dictionary.dto.ObjectStructureDefinition;
032 import org.kuali.student.r1.common.dictionary.dto.RequiredConstraint;
033 import org.kuali.student.r1.common.dictionary.dto.ValidCharsConstraint;
034 import org.kuali.student.r1.common.dictionary.dto.WhenConstraint;
035 import org.kuali.student.r2.core.search.dto.SearchParamInfo;
036 import org.kuali.student.r2.core.search.dto.SearchRequestInfo;
037 import org.kuali.student.r2.core.search.dto.SearchResultInfo;
038 import org.kuali.student.r2.core.search.service.SearchService;
039 import org.kuali.student.r1.common.validator.BeanConstraintDataProvider;
040 import org.kuali.student.r1.common.validator.ConstraintDataProvider;
041 import org.kuali.student.r1.common.validator.DateParser;
042 import org.kuali.student.r1.common.validator.ServerDateParser;
043 import org.kuali.student.r1.common.validator.ValidatorUtils;
044 import org.kuali.student.r2.common.dto.AttributeInfo;
045 import org.kuali.student.r2.common.dto.ContextInfo;
046 import org.kuali.student.r2.common.dto.LocaleInfo;
047 import org.kuali.student.r2.common.dto.ValidationResultInfo;
048 import org.kuali.student.r2.common.exceptions.DoesNotExistException;
049 import org.kuali.student.r2.common.exceptions.InvalidParameterException;
050 import org.kuali.student.r2.common.exceptions.MissingParameterException;
051 import org.kuali.student.r2.common.exceptions.OperationFailedException;
052 import org.kuali.student.r2.common.exceptions.PermissionDeniedException;
053 import org.kuali.student.r2.common.infc.ValidationResult.ErrorLevel;
054 import org.kuali.student.r2.common.messages.dto.MessageInfo;
055 import org.kuali.student.r2.common.messages.service.MessageService;
056 import org.springframework.beans.BeanUtils;
057
058 // This class is a special case, this class/equivelent doesn't exist in R2
059 // packages and is a common and has methods used in both R1 and R2 packages,
060 // this class was duplicated to R2 and modified to work with R2 services
061 // BaseAbstractValidator, BaseAbstractValidator, Validator, ValidatorFactory
062
063 public class DefaultValidatorImpl extends BaseAbstractValidator {
064 final static Logger LOG = Logger.getLogger(DefaultValidatorImpl.class);
065
066 private MessageService messageService = null;
067
068 private SearchService searchDispatcher;
069
070 private String messageLocaleKey = "en";
071
072 private String messageGroupKey = "validation";
073
074 private DateParser dateParser = new ServerDateParser();
075
076 private boolean serverSide = true;
077
078 public MessageService getMessageService() {
079 return messageService;
080 }
081
082 public void setMessageService(MessageService messageService) {
083 this.messageService = messageService;
084 }
085
086 public String getMessageLocaleKey() {
087 return messageLocaleKey;
088 }
089
090 public void setMessageLocaleKey(String messageLocaleKey) {
091 this.messageLocaleKey = messageLocaleKey;
092 }
093
094 public String getMessageGroupKey() {
095 return messageGroupKey;
096 }
097
098 public void setMessageGroupKey(String messageGroupKey) {
099 this.messageGroupKey = messageGroupKey;
100 }
101
102 public void setDateParser(DateParser dateParser) {
103 this.dateParser = dateParser;
104 }
105
106 /**
107 * @return the serverSide
108 */
109 public boolean isServerSide() {
110 return serverSide;
111 }
112
113 /**
114 * @param serverSide
115 * the serverSide to set
116 */
117 public void setServerSide(boolean serverSide) {
118 this.serverSide = serverSide;
119 }
120
121 /**
122 * @return the dateParser
123 */
124 public DateParser getDateParser() {
125 return dateParser;
126 }
127
128 public List<ValidationResultInfo> validateObject(Object data, ObjectStructureDefinition objStructure, ContextInfo contextInfo) {
129
130 ObjectStructureHierarchy objectStructureHierarchy = new ObjectStructureHierarchy();
131 objectStructureHierarchy.setObjectStructure(objStructure);
132 return validateObject(data, objectStructureHierarchy, contextInfo);
133 }
134
135 /**
136 * Validate Object and all its nested child objects for given type and state
137 *
138 * @param data
139 * @param objStructure
140 * @return
141 */
142 public List<ValidationResultInfo> validateObject(Object data, ObjectStructureHierarchy objStructure, ContextInfo contextInfo) {
143
144 List<ValidationResultInfo> results = new ArrayList<ValidationResultInfo>();
145 Stack<String> elementStack = new Stack<String>();
146
147 validateObject(results, data, objStructure, elementStack, data, objStructure.getObjectStructure(), true, null, contextInfo);
148
149 return results;
150 }
151
152 private void validateObject(List<ValidationResultInfo> results, Object data, ObjectStructureHierarchy objStructure, Stack<String> elementStack, Object rootData,
153 ObjectStructureDefinition rootObjStructure, boolean isRoot, ConstraintDataProvider parentDataProvider, ContextInfo contextInfo) {
154
155 ConstraintDataProvider dataProvider = new BeanConstraintDataProvider();
156 dataProvider.initialize(data);
157 if (parentDataProvider != null){
158 dataProvider.setParent(parentDataProvider);
159 }
160
161 // Push object structure to the top of the stack
162 StringBuilder objXPathElement = new StringBuilder(dataProvider.getPath());
163
164 if (!isRoot && !objXPathElement.toString().isEmpty()) {
165 elementStack.push(objXPathElement.toString());
166 }
167
168 /*
169 * Do nothing if the object to be validated is not type/state or if the objectstructure with constraints is not
170 * provided
171 */
172 if (null == objStructure.getObjectStructure()) {
173 return;
174 }
175
176 for (FieldDefinition f : objStructure.getObjectStructure().getAttributes()) {
177 validateField(results, f, objStructure, dataProvider, elementStack, rootData, rootObjStructure, contextInfo);
178
179 // Use Custom Validators
180 if (f.getCustomValidatorClass() != null || f.isServerSide() && serverSide) {
181 Validator customValidator = validatorFactory.getValidator(f.getCustomValidatorClass());
182 if (customValidator == null) {
183 throw new RuntimeException("Custom Validator " + f.getCustomValidatorClass() + " was not configured in this context");
184 }
185 List<ValidationResultInfo> l = customValidator.validateObject(f, data, objStructure.getObjectStructure(), elementStack, null);
186 results.addAll(l);
187 }
188 }
189 if (!isRoot && !objXPathElement.toString().isEmpty()) {
190 elementStack.pop();
191 }
192
193 /* All Field validations are returned right now */
194 // List<ValidationResultInfo> resultsBuffer = new
195 // ArrayList<ValidationResultInfo>();
196 // for (ValidationResultContainer vc : results) {
197 // if (skipFields.contains(vc.getElement()) == false) {
198 // resultsBuffer.add(vc);
199 // }
200 // }
201 // results = resultsBuffer;
202 }
203
204 public void validateField(List<ValidationResultInfo> results, FieldDefinition field, ObjectStructureHierarchy objStruct, ConstraintDataProvider dataProvider, Stack<String> elementStack, Object rootData, ObjectStructureDefinition rootObjectStructure, ContextInfo contextInfo) {
205
206 Object value = dataProvider.getValue(field.getName());
207
208 // Handle null values in field
209 if (value == null || "".equals(value.toString().trim())) {
210 processConstraint(results, field, objStruct, value, dataProvider, elementStack, rootData, rootObjectStructure, contextInfo);
211 return; // no need to do further processing
212 }
213
214 /*
215 * For complex object structures only the following constraints apply 1. TypeStateCase 2. MinOccurs 3. MaxOccurs
216 */
217 if (DataType.COMPLEX.equals(field.getDataType())) {
218 ObjectStructureHierarchy nestedObjStruct = new ObjectStructureHierarchy();
219 nestedObjStruct.setParentObjectStructureHierarchy(objStruct);
220
221 if (null != field.getDataObjectStructure()) {
222 nestedObjStruct.setObjectStructure(field.getDataObjectStructure());
223 }
224
225 elementStack.push(field.getName());
226 // beanPathStack.push(field.isDynamic()?"attributes("+field.getName()+")":field.getName());
227
228 if (value instanceof Collection) {
229
230 String xPathForCollection = getElementXpath(elementStack) + "/*";
231
232 int i = 0;
233 for (Object o : (Collection<?>) value) {
234 elementStack.push(Integer.toString(i));
235 // beanPathStack.push(!beanPathStack.isEmpty()?beanPathStack.pop():""+"["+i+"]");
236 processNestedObjectStructure(results, o, nestedObjStruct, field, elementStack, rootData, rootObjectStructure, dataProvider, contextInfo);
237 // beanPathStack.pop();
238 // beanPathStack.push(field.isDynamic()?"attributes("+field.getName()+")":field.getName());
239 elementStack.pop();
240 i++;
241 }
242 if (field.getMinOccurs() != null && field.getMinOccurs() > ((Collection<?>) value).size()) {
243 ValidationResultInfo valRes = new ValidationResultInfo(xPathForCollection, value);
244 valRes.setError(MessageUtils.interpolate(getMessage("validation.minOccurs", contextInfo), toMap(field)));
245 results.add(valRes);
246 }
247
248 Integer maxOccurs = tryParse(field.getMaxOccurs());
249 if (maxOccurs != null && maxOccurs < ((Collection<?>) value).size()) {
250 ValidationResultInfo valRes = new ValidationResultInfo(xPathForCollection, value);
251 valRes.setError(MessageUtils.interpolate(getMessage("validation.maxOccurs", contextInfo), toMap(field)));
252 results.add(valRes);
253 }
254 } else {
255 if (null != value) {
256 processNestedObjectStructure(results, value, nestedObjStruct, field, elementStack, rootData, rootObjectStructure, dataProvider, contextInfo);
257 } else {
258 if (field.getMinOccurs() != null && field.getMinOccurs() > 0) {
259 ValidationResultInfo val = new ValidationResultInfo(getElementXpath(elementStack), value);
260 if (field.getLabelKey() != null) {
261 val.setError(getMessage(field.getLabelKey(), contextInfo));
262 } else {
263 val.setError(getMessage("validation.required", contextInfo));
264 }
265 results.add(val);
266 }
267 }
268 }
269
270 // beanPathStack.pop();
271 elementStack.pop();
272
273 } else { // If non complex data type
274
275 if (value instanceof Collection) {
276
277 if (((Collection<?>) value).isEmpty()) {
278 processConstraint(results, field, objStruct, "", dataProvider, elementStack, rootData, rootObjectStructure, contextInfo);
279 }
280
281 int i = 0;
282 for (Object o : (Collection<?>) value) {
283 // This is tricky, change the field name to the index in the elementStack(this is for lists of non
284 // complex types)
285 elementStack.push(field.getName());
286 FieldDefinition tempField = new FieldDefinition();
287 BeanUtils.copyProperties(field, tempField);
288 tempField.setName(Integer.toBinaryString(i));
289 processConstraint(results, tempField, objStruct, o, dataProvider, elementStack, rootData, rootObjectStructure, contextInfo);
290 elementStack.pop();
291 i++;
292 }
293
294 String xPath = getElementXpath(elementStack) + "/" + field.getName() + "/*";
295 if (field.getMinOccurs() != null && field.getMinOccurs() > ((Collection<?>) value).size()) {
296 ValidationResultInfo valRes = new ValidationResultInfo(xPath, value);
297 valRes.setError(MessageUtils.interpolate(getMessage("validation.minOccurs", contextInfo), toMap(field)));
298 results.add(valRes);
299 }
300
301 Integer maxOccurs = tryParse(field.getMaxOccurs());
302 if (maxOccurs != null && maxOccurs < ((Collection<?>) value).size()) {
303 ValidationResultInfo valRes = new ValidationResultInfo(xPath, value);
304 valRes.setError(MessageUtils.interpolate(getMessage("validation.maxOccurs", contextInfo), toMap(field)));
305 results.add(valRes);
306 }
307 } else {
308 processConstraint(results, field, objStruct, value, dataProvider, elementStack, rootData, rootObjectStructure, contextInfo);
309 }
310
311 }
312 }
313
314 protected Integer tryParse(String s) {
315 Integer result = null;
316 if (s != null) {
317 try {
318 result = Integer.valueOf(s);
319 } catch (NumberFormatException e) {
320 // do nothing
321 }
322 }
323 return result;
324 }
325
326 protected void processNestedObjectStructure(List<ValidationResultInfo> results, Object value, ObjectStructureHierarchy nestedObjStruct, FieldDefinition field,
327 Stack<String> elementStack, Object rootData, ObjectStructureDefinition rootObjStructure, ConstraintDataProvider parentDataProvider, ContextInfo contextInfo) {
328 validateObject(results, value, nestedObjStruct, elementStack, rootData, rootObjStructure, false, parentDataProvider, contextInfo);
329 }
330
331 protected void processConstraint(List<ValidationResultInfo> valResults, FieldDefinition field, ObjectStructureHierarchy objStructure, Object value, ConstraintDataProvider dataProvider, Stack<String> elementStack, Object rootData, ObjectStructureDefinition rootObjStructure, ContextInfo contextInfo) {
332
333 // Process Case Constraint
334 // Case Constraint are only evaluated on the field. Nested case constraints are currently ignored
335 Constraint caseConstraint = processCaseConstraint(valResults, field.getCaseConstraint(), objStructure, value, dataProvider, elementStack, rootData, rootObjStructure, contextInfo);
336
337 Constraint constraint = (null != caseConstraint) ? caseConstraint : field;
338
339 processBaseConstraints(valResults, constraint, field, value, elementStack, contextInfo);
340
341 // Stop other checks if value is null
342 if (value == null || "".equals(value.toString().trim())) {
343 return;
344 }
345
346 String elementPath = getElementXpath(elementStack) + "/" + field.getName();
347
348 // Process Valid Chars
349 if (null != constraint.getValidChars()) {
350 ValidationResultInfo val = processValidCharConstraint(elementPath, constraint.getValidChars(), dataProvider, value, contextInfo);
351 if (null != val) {
352 valResults.add(val);
353 }
354 }
355
356 // Process Require Constraints (only if this field has value)
357 if (value != null && !"".equals(value.toString().trim())) {
358 if (null != constraint.getRequireConstraint() && constraint.getRequireConstraint().size() > 0) {
359 for (RequiredConstraint rc : constraint.getRequireConstraint()) {
360 ValidationResultInfo val = processRequireConstraint(elementPath, rc, field, objStructure.getObjectStructure(), dataProvider, contextInfo);
361 if (null != val) {
362 valResults.add(val);
363 // FIXME: For clarity, might be better to handle this in the processRequireConstraint method instead.
364 processCrossFieldWarning(valResults, rc, val.getErrorLevel(), field.getName(), contextInfo);
365 }
366 }
367 }
368 }
369
370 // Process Occurs Constraint
371 if (null != constraint.getOccursConstraint() && constraint.getOccursConstraint().size() > 0) {
372 for (MustOccurConstraint oc : constraint.getOccursConstraint()) {
373 ValidationResultInfo val = processOccursConstraint(elementPath, oc, field, objStructure.getObjectStructure(), dataProvider, contextInfo);
374 if (null != val) {
375 valResults.add(val);
376 }
377 }
378 }
379
380 // Process lookup Constraint
381 if (null != constraint.getLookupDefinition()) {
382 processLookupConstraint(valResults, constraint.getLookupDefinition(), field, elementStack, dataProvider, objStructure.getObjectStructure(), rootData, rootObjStructure, value, contextInfo);
383 }
384 }
385
386 protected ValidationResultInfo processRequireConstraint(String element, RequiredConstraint constraint, FieldDefinition field, ObjectStructureDefinition objStructure, ConstraintDataProvider dataProvider, ContextInfo contextInfo) {
387
388 ValidationResultInfo val = null;
389
390 String fieldName = constraint.getFieldPath();// TODO parse fieldname from here
391 Object fieldValue = dataProvider.getValue(fieldName);
392
393 boolean result = true;
394
395 if (fieldValue instanceof java.lang.String) {
396 result = hasText((String) fieldValue);
397 } else if (fieldValue instanceof Collection) {
398 result = (((Collection<?>) fieldValue).size() > 0);
399 } else {
400 result = (null != fieldValue) ? true : false;
401 }
402
403 if (!result) {
404 Map<String, Object> rMap = new HashMap<String, Object>();
405 rMap.put("field1", field.getName());
406 rMap.put("field2", fieldName);
407 val = new ValidationResultInfo(element, fieldValue);
408 val.setMessage(MessageUtils.interpolate(getMessage("validation.requiresField", contextInfo), rMap));
409 val.setLevel(constraint.getErrorLevel());
410 }
411
412 return val;
413 }
414
415 /**
416 * Process caseConstraint tag and sets any of the base constraint items if any of the when condition matches
417 *
418 * @param valResults
419 * @param caseConstraint
420 * @param objStructure
421 */
422 protected Constraint processCaseConstraint(List<ValidationResultInfo> valResults, CaseConstraint caseConstraint, ObjectStructureHierarchy objStructure, Object value, ConstraintDataProvider dataProvider, Stack<String> elementStack, Object rootData, ObjectStructureDefinition rootObjStructure, ContextInfo contextInfo) {
423
424 if (null == caseConstraint) {
425 return null;
426 }
427
428 String operator = (hasText(caseConstraint.getOperator())) ? caseConstraint.getOperator() : "EQUALS";
429 FieldDefinition caseField = null;
430 boolean absolutePath = false;
431 if (hasText(caseConstraint.getFieldPath())) {
432 if (caseConstraint.getFieldPath().startsWith("/")) {
433 absolutePath = true;
434 caseField = ValidatorUtils.getField(caseConstraint.getFieldPath().substring(1), rootObjStructure);
435 } else {
436 caseField = ValidatorUtils.getField(caseConstraint.getFieldPath(), objStructure);
437 }
438 }
439
440 // TODO: What happens when the field is not in the dataProvider?
441 Object fieldValue = value;
442 if (caseField != null) {
443 if (absolutePath) {
444 try {
445 if (caseField.isDynamic()) {
446 // Pull the value from the dynamic attribute map
447 // TODO There needs to be some mapping from PropertyUtils to the KS path
448 // Until then, this will only work for root level properties
449 Map<String, String> attributes = null;
450 Object atts = PropertyUtils.getNestedProperty(rootData, "attributes");
451 if (atts instanceof Map<?, ?>) {
452 attributes = (Map<String, String>) atts;
453 } else {
454 List<AttributeInfo> attToMap = (List<AttributeInfo>) atts;
455 if (attToMap != null) {
456 for (AttributeInfo atin : attToMap) {
457
458 try {
459 attributes.put(atin.getKey(), atin.getValue());
460 } catch (Exception e) {
461 System.out.print("Failed at " + rootData.getClass().getName() + " for object attributes");
462
463 }
464 }
465 }
466 }
467
468 if (attributes != null) {
469 fieldValue = attributes.get(caseConstraint.getFieldPath().substring(1));
470 }
471 } else {
472 fieldValue = PropertyUtils.getNestedProperty(rootData, caseConstraint.getFieldPath().substring(1));
473 }
474 } catch (IllegalAccessException e) {} catch (InvocationTargetException e) {} catch (NoSuchMethodException e) {}
475 } else {
476 fieldValue = ValidatorUtils.getFieldValue(caseConstraint.getFieldPath(), dataProvider);
477 }
478 }
479 DataType fieldDataType = (null != caseField ? caseField.getDataType() : null);
480
481 // If fieldValue is null then skip Case check
482 if (null == fieldValue) {
483 return null;
484 }
485
486 // Extract value for field Key
487 for (WhenConstraint wc : caseConstraint.getWhenConstraint()) {
488
489 if (hasText(wc.getValuePath())) {
490 Object whenValue = null;
491 if (wc.getValuePath().startsWith("/")) {
492 try {
493 whenValue = PropertyUtils.getNestedProperty(rootData, wc.getValuePath().substring(1));
494 } catch (IllegalAccessException e) {} catch (InvocationTargetException e) {} catch (NoSuchMethodException e) {}
495 } else {
496 whenValue = dataProvider.getValue(wc.getValuePath());
497 }
498 if (ValidatorUtils.compareValues(fieldValue, whenValue, fieldDataType, operator, caseConstraint.isCaseSensitive(), dateParser) && null != wc.getConstraint()) {
499 Constraint constraint = wc.getConstraint();
500 if (constraint.getCaseConstraint() != null) {
501 return processCaseConstraint(valResults, constraint.getCaseConstraint(), objStructure, value, dataProvider, elementStack, rootData, rootObjStructure, contextInfo);
502 } else {
503 processCrossFieldWarning(valResults, caseConstraint, constraint, value, constraint.getErrorLevel(), contextInfo);
504 return constraint;
505 }
506 }
507 } else {
508 List<Object> whenValueList = wc.getValues();
509
510 for (Object whenValue : whenValueList) {
511 if (ValidatorUtils.compareValues(fieldValue, whenValue, fieldDataType, operator, caseConstraint.isCaseSensitive(), dateParser) && null != wc.getConstraint()) {
512 Constraint constraint = wc.getConstraint();
513 if (constraint.getCaseConstraint() != null) {
514 return processCaseConstraint(valResults, constraint.getCaseConstraint(), objStructure, value, dataProvider, elementStack, rootData, rootObjStructure, contextInfo);
515 } else {
516 processCrossFieldWarning(valResults, caseConstraint, constraint, value, constraint.getErrorLevel(), contextInfo);
517 return constraint;
518 }
519 }
520 }
521 }
522 }
523
524 return null;
525 }
526
527 public ValidationResultInfo processValidCharConstraint(String element, ValidCharsConstraint vcConstraint, ConstraintDataProvider dataProvider, Object value, ContextInfo contextInfo) {
528
529 ValidationResultInfo val = null;
530
531 StringBuilder fieldValue = new StringBuilder();
532 String validChars = vcConstraint.getValue();
533
534 fieldValue.append(ValidatorUtils.getString(value));
535
536 int typIdx = validChars.indexOf(":");
537 String processorType = "regex";
538 if (-1 == typIdx) {
539 validChars = "[" + validChars + "]*";
540 } else {
541 processorType = validChars.substring(0, typIdx);
542 validChars = validChars.substring(typIdx + 1);
543 }
544
545 if ("regex".equalsIgnoreCase(processorType)) {
546 if (fieldValue == null || !fieldValue.toString().matches(validChars)) {
547 val = new ValidationResultInfo(element, fieldValue);
548 if (vcConstraint.getLabelKey() != null) {
549 val.setError(getMessage(vcConstraint.getLabelKey(), contextInfo));
550 } else {
551 val.setError(getMessage("validation.validCharsFailed", contextInfo));
552 }
553 }
554 }
555
556 return val;
557 }
558
559 /**
560 * Computes if all the filed required in the occurs clause are between the min and max
561 *
562 * @param element
563 * @param constraint
564 * @param field
565 * @param objStructure
566 * @param dataProvider
567 * @return
568 */
569 protected ValidationResultInfo processOccursConstraint(String element, MustOccurConstraint constraint, FieldDefinition field, ObjectStructureDefinition objStructure, ConstraintDataProvider dataProvider, ContextInfo contextInfo) {
570
571 boolean result = false;
572 int trueCount = 0;
573
574 ValidationResultInfo val = null;
575
576 for (RequiredConstraint rc : constraint.getRequiredFields()) {
577 trueCount += (processRequireConstraint("", rc, field, objStructure, dataProvider, contextInfo) != null) ? 1 : 0;
578 }
579
580 for (MustOccurConstraint oc : constraint.getOccurs()) {
581 trueCount += (processOccursConstraint("", oc, field, objStructure, dataProvider, contextInfo) != null) ? 1 : 0;
582 }
583
584 result = (trueCount >= constraint.getMin() && trueCount <= constraint.getMax()) ? true : false;
585
586 if (!result) {
587 // TODO: figure out what data should go here instead of null
588 val = new ValidationResultInfo(element, null);
589 val.setMessage(getMessage("validation.occurs", contextInfo));
590 val.setLevel(constraint.getErrorLevel());
591 }
592
593 return val;
594 }
595
596 // TODO: Implement lookup constraint
597 protected void processLookupConstraint(List<ValidationResultInfo> valResults, LookupConstraint lookupConstraint, FieldDefinition field, Stack<String> elementStack, ConstraintDataProvider dataProvider, ObjectStructureDefinition objStructure, Object rootData, ObjectStructureDefinition rootObjStructure, Object value, ContextInfo contextInfo) {
598 if (lookupConstraint == null) {
599 return;
600 }
601
602 // Create search params based on the param mapping
603 List<SearchParamInfo> params = new ArrayList<SearchParamInfo>();
604
605 for (CommonLookupParam paramMapping : lookupConstraint.getParams()) {
606 // Skip params that are the search param id key
607 if (lookupConstraint.getSearchParamIdKey() != null && lookupConstraint.getSearchParamIdKey().equals(paramMapping.getKey())) {
608 continue;
609 }
610
611 SearchParamInfo param = new SearchParamInfo();
612
613 param.setKey(paramMapping.getKey());
614
615 // If the value of the search param comes form another field then get it
616 if (paramMapping.getFieldPath() != null && !paramMapping.getFieldPath().isEmpty()) {
617 FieldDefinition lookupField = null;
618 boolean absolutePath = false;
619 if (hasText(paramMapping.getFieldPath())) {
620 if (paramMapping.getFieldPath().startsWith("/")) {
621 absolutePath = true;
622 lookupField = ValidatorUtils.getField(paramMapping.getFieldPath().substring(1), rootObjStructure);
623 } else {
624 lookupField = ValidatorUtils.getField(paramMapping.getFieldPath(), objStructure);
625 }
626 }
627 Object fieldValue = null;
628 if (lookupField != null) {
629 if (absolutePath) {
630 try {
631 if (lookupField.isDynamic()) {
632 // Pull the value from the dynamic attribute map
633 // Until then, this will only work for root level properties
634 Map<String, String> attributes = (Map<String, String>) PropertyUtils.getNestedProperty(rootData, "attributes");
635 if (attributes != null) {
636 fieldValue = attributes.get(paramMapping.getFieldPath().substring(1));
637 }
638 } else {
639 fieldValue = PropertyUtils.getNestedProperty(rootData, paramMapping.getFieldPath().substring(1));
640 }
641 } catch (IllegalAccessException e) {} catch (InvocationTargetException e) {} catch (NoSuchMethodException e) {}
642 } else {
643 fieldValue = dataProvider.getValue(lookupField.getName());
644 }
645 } else {
646 fieldValue = dataProvider.getValue(paramMapping.getFieldPath());
647 }
648
649 if (fieldValue instanceof String) {
650 param.getValues().add((String) fieldValue);
651 } else if (fieldValue instanceof List<?>) {
652 param.setValues((List<String>) fieldValue);
653 }
654 } else if (paramMapping.getDefaultValueString() != null) {
655 param.getValues().add(paramMapping.getDefaultValueString());
656 } else {
657 param.setValues(paramMapping.getDefaultValueList());
658 }
659 params.add(param);
660 }
661
662 if (lookupConstraint.getSearchParamIdKey() != null) {
663 SearchParamInfo param = new SearchParamInfo();
664 param.setKey(lookupConstraint.getSearchParamIdKey());
665 if (value instanceof String) {
666 param.getValues().add((String) value);
667 } else if (value instanceof List<?>) {
668 param.setValues((List<String>) value);
669 }
670 params.add(param);
671 }
672
673 SearchRequestInfo searchRequest = new SearchRequestInfo();
674 searchRequest.setMaxResults(1);
675 searchRequest.setStartAt(0);
676 searchRequest.setNeededTotalResults(false);
677 searchRequest.setSearchKey(lookupConstraint.getSearchTypeId());
678 searchRequest.setParams(params);
679
680 SearchResultInfo searchResult = null;
681 try {
682 searchResult = searchDispatcher.search(searchRequest, contextInfo);
683 } catch (Exception e) {
684 LOG.info("Error calling Search", e);
685 }
686 // If there are no search results then make a validation result
687 if (searchResult == null || searchResult.getRows() == null || searchResult.getRows().isEmpty()) {
688 ValidationResultInfo val = new ValidationResultInfo(getElementXpath(elementStack) + "/" + field.getName(), value);
689 val.setLevel(lookupConstraint.getErrorLevel());
690 val.setMessage(getMessage("validation.lookup", contextInfo));
691 valResults.add(val);
692 processCrossFieldWarning(valResults, lookupConstraint, lookupConstraint.getErrorLevel(), contextInfo);
693 }
694 }
695
696 protected void processBaseConstraints(List<ValidationResultInfo> valResults, Constraint constraint, FieldDefinition field, Object value, Stack<String> elementStack, ContextInfo contextInfo) {
697 DataType dataType = field.getDataType();
698 String name = field.getName();
699
700 if (value == null || "".equals(value.toString().trim())) {
701 if (constraint.getMinOccurs() != null && constraint.getMinOccurs() > 0) {
702 ValidationResultInfo val = new ValidationResultInfo(getElementXpath(elementStack) + "/" + name, value);
703 if (constraint.getLabelKey() != null) {
704 val.setError(getMessage(constraint.getLabelKey(), contextInfo));
705 } else {
706 val.setMessage(getMessage("validation.required", contextInfo));
707 }
708 val.setLevel(constraint.getErrorLevel());
709 valResults.add(val);
710 }
711 return;
712 }
713
714 String elementPath = getElementXpath(elementStack) + "/" + name;
715
716 if (DataType.STRING.equals(dataType)) {
717 validateString(value, constraint, elementPath, valResults, contextInfo);
718 } else if (DataType.INTEGER.equals(dataType)) {
719 validateInteger(value, constraint, elementPath, valResults, contextInfo);
720 } else if (DataType.LONG.equals(dataType)) {
721 validateLong(value, constraint, elementPath, valResults, contextInfo);
722 } else if (DataType.DOUBLE.equals(dataType)) {
723 validateDouble(value, constraint, elementPath, valResults, contextInfo);
724 } else if (DataType.FLOAT.equals(dataType)) {
725 validateFloat(value, constraint, elementPath, valResults, contextInfo);
726 } else if (DataType.BOOLEAN.equals(dataType)) {
727 validateBoolean(value, constraint, elementPath, valResults, contextInfo);
728 } else if (DataType.DATE.equals(dataType)) {
729 validateDate(value, constraint, elementPath, valResults, dateParser, contextInfo);
730 }
731 }
732
733 /**
734 * This adds a warning on the related field when a processed case-constraint results in a warning
735 *
736 * @param valResults
737 * @param crossConstraint
738 * @param constraint
739 */
740 protected void processCrossFieldWarning(List<ValidationResultInfo> valResults, CaseConstraint crossConstraint, Constraint constraint, Object value, ErrorLevel errorLevel, ContextInfo contextInfo) {
741 if ((ErrorLevel.WARN == errorLevel || ErrorLevel.ERROR == errorLevel) && (value == null || "".equals(value.toString().trim()))) {
742 if (constraint.getMinOccurs() != null && constraint.getMinOccurs() > 0) {
743
744 String crossFieldPath = crossConstraint.getFieldPath();
745 String crossFieldMessageId = crossConstraint.getFieldPathMessageId() == null ? "validation.required" : crossConstraint.getFieldPathMessageId();
746 addCrossFieldWarning(valResults, crossFieldPath, getMessage(crossFieldMessageId, contextInfo), errorLevel);
747 }
748 }
749 }
750
751 /**
752 * This adds a warning on the related field when a processed case-constraint results in a warning
753 *
754 * @param valResults
755 * @param requiredConstraint
756 * @param field
757 */
758 protected void processCrossFieldWarning(List<ValidationResultInfo> valResults, RequiredConstraint requiredConstraint, ErrorLevel errorLevel, String field, ContextInfo contextInfo) {
759 if ((ErrorLevel.WARN == errorLevel || ErrorLevel.ERROR == errorLevel) && requiredConstraint != null) {
760 String crossFieldPath = requiredConstraint.getFieldPath();
761 String crossFieldMessageId = requiredConstraint.getFieldPathMessageId() == null ? "validation.required" : requiredConstraint.getFieldPathMessageId();
762 addCrossFieldWarning(valResults, crossFieldPath, getMessage(crossFieldMessageId, contextInfo), errorLevel);
763 }
764 }
765
766 /**
767 * This adds a warning on the related field when a processed lookup-constraint results in a warning
768 *
769 * @param valResults
770 * @param lookupConstraint
771 */
772 protected void processCrossFieldWarning(List<ValidationResultInfo> valResults, LookupConstraint lookupConstraint, ErrorLevel errorLevel, ContextInfo contextInfo) {
773 if ((ErrorLevel.WARN == errorLevel || ErrorLevel.ERROR == errorLevel) && lookupConstraint != null) {
774 for (CommonLookupParam param : lookupConstraint.getParams()) {
775 if (param.getFieldPath() != null && !param.getFieldPath().isEmpty()) {
776 String crossFieldPath = param.getFieldPath();
777 String crossFieldMessageId = param.getFieldPathMessageId() == null ? "validation.lookup.cause" : param.getFieldPathMessageId();
778 addCrossFieldWarning(valResults, crossFieldPath, getMessage(crossFieldMessageId, contextInfo), errorLevel);
779 }
780 }
781 }
782 }
783
784 protected void addCrossFieldWarning(List<ValidationResultInfo> valResults, String crossFieldPath, String message, ErrorLevel errorLevel) {
785 // Check to see if the exact same validation message already exists on referenced field
786 boolean warnAlreadyExists = false;
787 for (ValidationResultInfo vr : valResults) {
788 if (vr.getElement().equals(crossFieldPath) && vr.getMessage().equals(message)) {
789 warnAlreadyExists = true;
790 }
791 }
792
793 // Only add this warning, if it hasn't been already added
794 if (!warnAlreadyExists) {
795 ValidationResultInfo val = new ValidationResultInfo(crossFieldPath, null);
796 val.setMessage(message);
797 val.setLevel(errorLevel);
798 valResults.add(val);
799 }
800 }
801
802 protected void validateBoolean(Object value, Constraint constraint, String element, List<ValidationResultInfo> results, ContextInfo contextInfo) {
803 if (!(value instanceof Boolean)) {
804 try {
805 Boolean.valueOf(value.toString());
806 } catch (Exception e) {
807 ValidationResultInfo val = new ValidationResultInfo(element, value);
808 val.setError(getMessage("validation.mustBeBoolean", contextInfo));
809 results.add(val);
810 }
811 }
812 }
813
814 protected void validateDouble(Object value, Constraint constraint, String element, List<ValidationResultInfo> results, ContextInfo contextInfo) {
815 Double v = null;
816
817 ValidationResultInfo val = new ValidationResultInfo(element, value);
818
819 if (value instanceof Number) {
820 v = ((Number) value).doubleValue();
821 } else {
822 try {
823 v = Double.valueOf(value.toString());
824 } catch (Exception e) {
825 val.setError(getMessage("validation.mustBeDouble", contextInfo));
826 }
827 }
828
829 if (val.isOk()) {
830 Double maxValue = ValidatorUtils.getDouble(constraint.getInclusiveMax());
831 Double minValue = ValidatorUtils.getDouble(constraint.getExclusiveMin());
832
833 if (maxValue != null && minValue != null) {
834 // validate range
835 if (v > maxValue || v < minValue) {
836 val.setError(MessageUtils.interpolate(getMessage("validation.outOfRange", contextInfo), toMap(constraint)));
837 }
838 } else if (maxValue != null) {
839 if (v > maxValue) {
840 val.setError(MessageUtils.interpolate(getMessage("validation.maxValueFailed", contextInfo), toMap(constraint)));
841 }
842 } else if (minValue != null) {
843 if (v < minValue) {
844 val.setError(MessageUtils.interpolate(getMessage("validation.minValueFailed", contextInfo), toMap(constraint)));
845 }
846 }
847 }
848
849 if (!val.isOk()) {
850 results.add(val);
851 }
852 }
853
854 protected void validateFloat(Object value, Constraint constraint, String element, List<ValidationResultInfo> results, ContextInfo contextInfo) {
855 Float v = null;
856
857 ValidationResultInfo val = new ValidationResultInfo(element, value);
858 if (value instanceof Number) {
859 v = ((Number) value).floatValue();
860 } else {
861 try {
862 v = Float.valueOf(value.toString());
863 } catch (Exception e) {
864 val.setError(getMessage("validation.mustBeFloat", contextInfo));
865 }
866 }
867
868 if (val.isOk()) {
869 Float maxValue = ValidatorUtils.getFloat(constraint.getInclusiveMax());
870 Float minValue = ValidatorUtils.getFloat(constraint.getExclusiveMin());
871
872 if (maxValue != null && minValue != null) {
873 // validate range
874 if (v > maxValue || v < minValue) {
875 val.setError(MessageUtils.interpolate(getMessage("validation.outOfRange", contextInfo), toMap(constraint)));
876 }
877 } else if (maxValue != null) {
878 if (v > maxValue) {
879 val.setError(MessageUtils.interpolate(getMessage("validation.maxValueFailed", contextInfo), toMap(constraint)));
880 }
881 } else if (minValue != null) {
882 if (v < minValue) {
883 val.setError(MessageUtils.interpolate(getMessage("validation.minValueFailed", contextInfo), toMap(constraint)));
884 }
885 }
886 }
887
888 if (!val.isOk()) {
889 results.add(val);
890 }
891 }
892
893 protected void validateLong(Object value, Constraint constraint, String element, List<ValidationResultInfo> results, ContextInfo contextInfo) {
894 Long v = null;
895
896 ValidationResultInfo val = new ValidationResultInfo(element, value);
897 if (value instanceof Number) {
898 v = ((Number) value).longValue();
899 } else {
900 try {
901 v = Long.valueOf(value.toString());
902 } catch (Exception e) {
903 val.setError(getMessage("validation.mustBeLong", contextInfo));
904 }
905 }
906
907 if (val.isOk()) {
908 Long maxValue = ValidatorUtils.getLong(constraint.getInclusiveMax());
909 Long minValue = ValidatorUtils.getLong(constraint.getExclusiveMin());
910
911 if (maxValue != null && minValue != null) {
912 // validate range
913 if (v > maxValue || v < minValue) {
914 val.setError(MessageUtils.interpolate(getMessage("validation.outOfRange", contextInfo), toMap(constraint)));
915 }
916 } else if (maxValue != null) {
917 if (v > maxValue) {
918 val.setError(MessageUtils.interpolate(getMessage("validation.maxValueFailed", contextInfo), toMap(constraint)));
919 }
920 } else if (minValue != null) {
921 if (v < minValue) {
922 val.setError(MessageUtils.interpolate(getMessage("validation.minValueFailed", contextInfo), toMap(constraint)));
923 }
924 }
925 }
926
927 if (!val.isOk()) {
928 results.add(val);
929 }
930
931 }
932
933 protected void validateInteger(Object value, Constraint constraint, String element, List<ValidationResultInfo> results, ContextInfo contextInfo) {
934 Integer v = null;
935
936 ValidationResultInfo val = new ValidationResultInfo(element, value);
937
938 if (value instanceof Number) {
939 v = ((Number) value).intValue();
940 } else {
941 try {
942 v = Integer.valueOf(value.toString());
943 } catch (Exception e) {
944 val.setError(getMessage("validation.mustBeInteger", contextInfo));
945 }
946 }
947
948 if (val.isOk()) {
949 Integer maxValue = ValidatorUtils.getInteger(constraint.getInclusiveMax());
950 Integer minValue = ValidatorUtils.getInteger(constraint.getExclusiveMin());
951
952 if (maxValue != null && minValue != null) {
953 // validate range
954 if (v > maxValue || v < minValue) {
955 val.setError(MessageUtils.interpolate(getMessage("validation.outOfRange", contextInfo), toMap(constraint)));
956 }
957 } else if (maxValue != null) {
958 if (v > maxValue) {
959 val.setError(MessageUtils.interpolate(getMessage("validation.maxValueFailed", contextInfo), toMap(constraint)));
960 }
961 } else if (minValue != null) {
962 if (v < minValue) {
963 val.setError(MessageUtils.interpolate(getMessage("validation.minValueFailed", contextInfo), toMap(constraint)));
964 }
965 }
966 }
967
968 if (!val.isOk()) {
969 results.add(val);
970 }
971 }
972
973 protected void validateDate(Object value, Constraint constraint, String element, List<ValidationResultInfo> results, DateParser dateParser, ContextInfo contextInfo) {
974 ValidationResultInfo val = new ValidationResultInfo(element, value);
975
976 Date v = null;
977
978 if (value instanceof Date) {
979 v = (Date) value;
980 } else {
981 try {
982 v = dateParser.parseDate(value.toString());
983 } catch (Exception e) {
984 val.setError(getMessage("validation.mustBeDate", contextInfo));
985 }
986 }
987
988 if (val.isOk()) {
989 Date maxValue = ValidatorUtils.getDate(constraint.getInclusiveMax(), dateParser);
990 Date minValue = ValidatorUtils.getDate(constraint.getExclusiveMin(), dateParser);
991
992 if (maxValue != null && minValue != null) {
993 // validate range
994 if (v.getTime() > maxValue.getTime() || v.getTime() < minValue.getTime()) {
995 val.setError(MessageUtils.interpolate(getMessage("validation.outOfRange", contextInfo), toMap(constraint)));
996 }
997 } else if (maxValue != null) {
998 if (v.getTime() > maxValue.getTime()) {
999 val.setError(MessageUtils.interpolate(getMessage("validation.maxValueFailed", contextInfo), toMap(constraint)));
1000 }
1001 } else if (minValue != null) {
1002 if (v.getTime() < minValue.getTime()) {
1003 val.setError(MessageUtils.interpolate(getMessage("validation.minValueFailed", contextInfo), toMap(constraint)));
1004 }
1005 }
1006 }
1007
1008 if (!val.isOk()) {
1009 results.add(val);
1010 }
1011 }
1012
1013 protected void validateString(Object value, Constraint constraint, String element, List<ValidationResultInfo> results, ContextInfo contextInfo) {
1014
1015 if (value == null) {
1016 value = "";
1017 }
1018 String s = value.toString().trim();
1019
1020 ValidationResultInfo val = new ValidationResultInfo(element, value);
1021
1022 Integer maxLength = tryParse(constraint.getMaxLength());
1023 if (maxLength != null && constraint.getMinLength() != null && constraint.getMinLength() > 0) {
1024 if (s.length() > maxLength || s.length() < constraint.getMinLength()) {
1025 val.setError(MessageUtils.interpolate(getMessage("validation.lengthOutOfRange", contextInfo), toMap(constraint)));
1026 }
1027 } else if (maxLength != null) {
1028 if (s.length() > Integer.parseInt(constraint.getMaxLength())) {
1029 val.setError(MessageUtils.interpolate(getMessage("validation.maxLengthFailed", contextInfo), toMap(constraint)));
1030 }
1031 } else if (constraint.getMinLength() != null && constraint.getMinLength() > 0) {
1032 if (s.length() < constraint.getMinLength()) {
1033 val.setError(MessageUtils.interpolate(getMessage("validation.minLengthFailed", contextInfo), toMap(constraint)));
1034 }
1035 }
1036
1037 if (!val.isOk()) {
1038 results.add(val);
1039 }
1040 }
1041
1042 protected String getMessage(String messageId, ContextInfo contextInfo) {
1043 if (null == messageService) {
1044 return messageId;
1045 }
1046
1047 // TODO: this need to be properly implemented.
1048 LocaleInfo locale = new LocaleInfo();
1049 locale.setLocaleLanguage(messageLocaleKey);
1050 MessageInfo msg = null;
1051 try {
1052 msg = messageService.getMessage(locale, messageGroupKey, messageId, contextInfo);
1053 } catch (DoesNotExistException e) {
1054 return "";
1055 } catch (InvalidParameterException e) {
1056 return "";
1057 } catch (MissingParameterException e) {
1058 return "";
1059 } catch (OperationFailedException e) {
1060 return "";
1061 } catch (PermissionDeniedException e) {
1062 return "";
1063 }
1064
1065 return msg.getValue();
1066 }
1067
1068 protected String getElementXpath(Stack<String> elementStack) {
1069 StringBuilder xPath = new StringBuilder();
1070 Iterator<String> itr = elementStack.iterator();
1071 while (itr.hasNext()) {
1072 xPath.append(itr.next());
1073 if (itr.hasNext()) {
1074 xPath.append("/");
1075 }
1076 }
1077
1078 return xPath.toString();
1079 }
1080
1081 /*
1082 * Homemade has text so we dont need outside libs.
1083 */
1084 protected boolean hasText(String string) {
1085
1086 if (string == null || string.length() < 1) {
1087 return false;
1088 }
1089 int stringLength = string.length();
1090
1091 for (int i = 0; i < stringLength; i++) {
1092 char currentChar = string.charAt(i);
1093 if (' ' != currentChar || '\t' != currentChar || '\n' != currentChar) {
1094 return true;
1095 }
1096 }
1097
1098 return false;
1099 }
1100
1101 protected Map<String, Object> toMap(Constraint c) {
1102 Map<String, Object> result = new HashMap<String, Object>();
1103 result.put("minOccurs", c.getMinOccurs());
1104 result.put("maxOccurs", c.getMaxOccurs());
1105 result.put("minLength", c.getMinLength());
1106 result.put("maxLength", c.getMaxLength());
1107 result.put("minValue", c.getExclusiveMin());
1108 result.put("maxValue", c.getInclusiveMax());
1109 // result.put("dataType", c.getDataType());
1110
1111 return result;
1112 }
1113
1114 public SearchService getSearchDispatcher() {
1115 return searchDispatcher;
1116 }
1117
1118 public void setSearchDispatcher(SearchService searchDispatcher) {
1119 this.searchDispatcher = searchDispatcher;
1120 }
1121
1122 @Override
1123 public List<ValidationResultInfo> validateObject(FieldDefinition field, Object o, ObjectStructureDefinition objStructure, Stack<String> elementStack, ContextInfo contextInfo) {
1124 return null;
1125 }
1126 }