001    /**
002     * Copyright 2004-2014 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.student.datadictionary.util;
017    
018    import java.util.ArrayList;
019    import java.util.Date;
020    import java.util.HashSet;
021    import java.util.List;
022    import java.util.Set;
023    import java.util.regex.Pattern;
024    import java.util.regex.PatternSyntaxException;
025    import org.kuali.rice.core.api.datetime.DateTimeService;
026    import org.kuali.rice.core.api.uif.DataType;
027    import org.kuali.rice.krad.datadictionary.AttributeDefinition;
028    import org.kuali.rice.krad.datadictionary.DataObjectEntry;
029    import org.kuali.rice.krad.datadictionary.validation.ValidationUtils;
030    import org.kuali.rice.krad.datadictionary.validation.constraint.CaseConstraint;
031    import org.kuali.rice.krad.datadictionary.validation.constraint.LookupConstraint;
032    import org.kuali.rice.krad.datadictionary.validation.constraint.ValidCharactersConstraint;
033    import org.kuali.rice.krad.datadictionary.validation.constraint.WhenConstraint;
034    
035    public class DictionaryValidator {
036    
037        private DateTimeService dateTimeService;
038        private DataObjectEntry doe;
039        private Set<DataObjectEntry> alreadyValidated;
040    
041        public DictionaryValidator(DataObjectEntry doe,
042                Set<DataObjectEntry> alreadyValidated) {
043            this.doe = doe;
044            this.alreadyValidated = alreadyValidated;
045        }
046    
047        public DateTimeService getDateTimeService() {
048            return dateTimeService;
049        }
050    
051        public void setDateTimeService(DateTimeService dateTimeService) {
052            this.dateTimeService = dateTimeService;
053        }
054    
055        public List<String> validate() {
056            List<String> errors = new ArrayList();
057            if (doe.getFullClassName() == null) {
058                errors.add("The class name cannot be be left null");
059            }
060            if (doe.getEntryClass() == null) {
061                errors.add("The entry class should not be left null");
062            }
063            if (!doe.getEntryClass().getName().equals(doe.getFullClassName())) {
064                errors.add("The entry class should match the full class name");
065            }
066    
067    //  else if (this.getClass (ode.getName ()) == null)
068    //  {
069    //   errors.add ("The name does not exist on the class path");
070    //  }
071    
072            if (doe.getAttributes() == null) {
073                errors.add("getAttribues () is null");
074                return errors;
075            }
076            if (doe.getCollections() == null) {
077                errors.add("getCollections () is null");
078                return errors;
079            }
080            if (doe.getComplexAttributes() == null) {
081                errors.add("getComplexAttributes ()");
082                return errors;
083            }
084            if (doe.getCollections().isEmpty()
085                    && doe.getComplexAttributes().isEmpty()
086                    && doe.getAttributes().isEmpty()) {
087                errors.add("No fields of any kind defined for this complex object structure");
088                return errors;
089            }
090    
091            Set<String> fieldNames = new HashSet();
092    
093            for (AttributeDefinition ad : doe.getAttributes()) {
094                if (ad.getName() != null) {
095                    if (!fieldNames.add(ad.getName())) {
096                        errors.add(ad.getName() + " is defined more than once");
097                    }
098                }
099                errors.addAll(validateAttributeDefinition(ad));
100            }
101    
102            doe.completeValidation();
103    
104            return errors;
105        }
106    
107        private List<String> validateAttributeDefinition(AttributeDefinition ad) {
108            List<String> errors = new ArrayList();
109            if (ad.getName() == null) {
110                errors.add("name cannot be null");
111            } else if (ad.getName().trim().equals("")) {
112                errors.add("name cannot be blank");
113            } else if (ad.getDataType() == null) {
114                errors.add(ad.getName() + " has a null data type");
115    //        } else if (ad.getDataType().equals(DataType.COMPLEX)) {
116    //            errorIfNotNull(errors, ad, "exclusiveMin", ad.getExclusiveMin());
117    //            errorIfNotNull(errors, ad, "inclusiveMax", ad.getInclusiveMax());
118    //            errorIfNotNull(errors, ad, "max length", ad.getMaxLength());
119    //            errorIfNotNull(errors, ad, "min length", ad.getMinLength());
120    //            errorIfNotNull(errors, ad, "valid chars", ad.getValidCharactersConstraint());
121    //            errorIfNotNull(errors, ad, "lookup", ad.getLookupDefinition());
122            }
123    //        validateConversion(errors, ad.getName(), "defaultValue", ad.getDataType(), ad.getDefaultValue());
124            validateConversion(errors, ad.getName(), "exclusiveMin", ad.getDataType(), ad.getExclusiveMin());
125            validateConversion(errors, ad.getName(), "inclusiveMax", ad.getDataType(), ad.getInclusiveMax());
126            //TODO: Cross compare to make sure min is not greater than max and that default value is valid itself
127    
128            if (ad.getLookupDefinition() != null) {
129                errors.addAll(validateLookup(ad, ad.getLookupDefinition()));
130            }
131            if (ad.getCaseConstraint() != null) {
132                errors.addAll(validateCase(ad, ad.getCaseConstraint()));
133            }
134            if (ad.getValidCharactersConstraint() != null) {
135                errors.addAll(validateValidChars(ad, ad.getValidCharactersConstraint()));
136            }
137            return errors;
138        }
139    
140        private void errorIfNotNull(List<String> errors, AttributeDefinition fd,
141                String validation,
142                Object value) {
143            if (value != null) {
144                errors.add("field " + fd.getName() + " has a " + validation
145                        + " but it cannot be specified on a complex type");
146            }
147        }
148    
149        private Object validateConversion(List<String> errors, String fieldName,
150                String propertyName, DataType dataType,
151                Object value) {
152            if (value == null) {
153                return null;
154            }
155            switch (dataType) {
156                case STRING:
157                    return value.toString().trim();
158    //    case DATE, TRUNCATED_DATE, BOOLEAN, INTEGER, FLOAT, DOUBLE, LONG, COMPLEX
159                case LONG:
160                    try {
161                        return ValidationUtils.getLong(value);
162                    } catch (NumberFormatException ex) {
163                        errors.add(
164                                "field " + fieldName
165                                + " has a " + propertyName
166                                + " that cannot be converted into a long integer");
167                    }
168                    return null;
169                case INTEGER:
170                    try {
171                        return ValidationUtils.getInteger(value);
172                    } catch (NumberFormatException ex) {
173                        errors.add(
174                                "field " + fieldName
175                                + " has a " + propertyName + " that cannot be converted into an integer");
176                    }
177                    return null;
178                case FLOAT:
179                    try {
180                        return ValidationUtils.getFloat(value);
181                    } catch (NumberFormatException ex) {
182                        errors.add(
183                                "field " + fieldName
184                                + " has a " + propertyName
185                                + " that cannot be converted into a floating point value");
186                    }
187                    return null;
188                case DOUBLE:
189                    try {
190                        return ValidationUtils.getFloat(value);
191                    } catch (NumberFormatException ex) {
192                        errors.add(
193                                "field " + fieldName
194                                + " has a " + propertyName
195                                + " that cannot be converted into a double sized floating point value");
196                    }
197                    return null;
198                case BOOLEAN:
199                    if (value instanceof Boolean) {
200                        return ((Boolean) value).booleanValue();
201                    }
202                    if (value instanceof String) {
203                        if (((String) value).trim().equalsIgnoreCase("true")) {
204                            return true;
205                        }
206                        if (((String) value).trim().equalsIgnoreCase("false")) {
207                            return true;
208                        }
209                    }
210                    errors.add(
211                            "field " + fieldName
212                            + " has a " + propertyName
213                            + " that cannot be converted into a boolean true/false");
214                    return null;
215                case DATE:
216                case TRUNCATED_DATE:
217                    if (value instanceof Date) {
218                        return (Date) value;
219                    }
220                    try {
221                        // TODO: make the date parser configurable like the validator is
222                        return ValidationUtils.getDate(value, dateTimeService);
223                    } catch (Exception e) {
224                        errors.add(
225                                "field " + fieldName
226                                + " has a " + propertyName
227                                + " that cannot be converted into a date");
228                    }
229                    return null;
230                default:
231                    errors.add(
232                            "field " + fieldName
233                            + " has a " + propertyName
234                            + " that cannot be converted into an unknown/unhandled data type");
235                    return null;
236            }
237        }
238    
239        private List<String> validateValidChars(AttributeDefinition fd,
240                ValidCharactersConstraint vc) {
241            List<String> errors = new ArrayList();
242            String validChars = vc.getValue();
243            /*
244            int typIdx = validChars.indexOf(":");
245            String processorType = "regex";
246            if (-1 == typIdx) {
247            validChars = "[" + validChars + "]*";
248            } else {
249            processorType = validChars.substring(0, typIdx);
250            validChars = validChars.substring(typIdx + 1);
251            }
252            if (!processorType.equalsIgnoreCase("regex")) {
253            errors.add(
254            "field " + fd.getName()
255            + " has an invalid valid chars processor type: a simple list of characters or a regex: is supported");
256            return errors;
257            }
258             */
259            try {
260                Pattern pattern = Pattern.compile(validChars);
261            } catch (PatternSyntaxException ex) {
262                errors.add("field " + fd.getName()
263                        + " has in invalid character pattern for a regular expression: "
264                        + validChars);
265            }
266            return errors;
267        }
268    
269        private List<String> validateLookup(AttributeDefinition fd, LookupConstraint lc) {
270            List<String> errors = new ArrayList();
271            if (lc.getParams() == null) {
272                errors.add("field " + fd.getName() + " has a lookup with null parameters");
273            }
274            //TODO: more validation
275            return errors;
276        }
277        public static final String GREATER_THAN_EQUAL = "greater_than_equal";
278        public static final String LESS_THAN_EQUAL = "less_than_equal";
279        public static final String GREATER_THAN = "greater_than";
280        public static final String LESS_THAN = "less_than";
281        public static final String EQUALS = "equals";
282        public static final String NOT_EQUAL = "not_equal";
283        private static final String[] VALID_OPERATORS = {
284            NOT_EQUAL, EQUALS, GREATER_THAN_EQUAL, LESS_THAN_EQUAL, GREATER_THAN, LESS_THAN
285        };
286    
287        private List<String> validateCase(AttributeDefinition fd, CaseConstraint cc) {
288            List<String> errors = new ArrayList();
289            if (cc.getOperator() == null) {
290                errors.add("field " + fd.getName()
291                        + " has a case constraint with no operator");
292            } else {
293                boolean found = false;
294                for (int i = 0; i < VALID_OPERATORS.length; i++) {
295                    if (VALID_OPERATORS[i].equalsIgnoreCase(cc.getOperator())) {
296                        found = true;
297                        break;
298                    }
299                }
300                if (!found) {
301                    errors.add("field " + fd.getName()
302                            + " has a case constraint with an unknown operator "
303                            + cc.getOperator());
304                }
305            }
306            if (cc.getPropertyName() == null) {
307                errors.add(
308                        "field " + fd.getName()
309                        + " has a case constraint with a null for the field to use for the comparison");
310            } else if (cc.getPropertyName().trim().equals("")) {
311                errors.add(
312                        "field " + fd.getName()
313                        + " has a case constraint with blanks for the field to use for the comparison");
314            }
315            if (cc.getWhenConstraint() == null) {
316                errors.add("field " + fd.getName()
317                        + " has a case constraint but null when statements");
318                return errors;
319            }
320            if (cc.getWhenConstraint().size() == 0) {
321                errors.add("field " + fd.getName()
322                        + " has a case constraint but has no when statements");
323            }
324            for (WhenConstraint wc : cc.getWhenConstraint()) {
325                if (wc.getConstraint() == null) {
326                    errors.add(
327                            "field " + fd.getName()
328                            + " has a as case constraint with a when statement that has no overriding constraints specified");
329                }
330            }
331            //TODO: more validation
332            return errors;
333        }
334    }