001 /*
002 * Copyright 2011 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 1.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/ecl1.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 }