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    }