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 */
016package org.kuali.student.datadictionary.util;
017
018import java.util.ArrayList;
019import java.util.Date;
020import java.util.HashSet;
021import java.util.List;
022import java.util.Set;
023import java.util.regex.Pattern;
024import java.util.regex.PatternSyntaxException;
025import org.kuali.rice.core.api.datetime.DateTimeService;
026import org.kuali.rice.core.api.uif.DataType;
027import org.kuali.rice.krad.datadictionary.AttributeDefinition;
028import org.kuali.rice.krad.datadictionary.DataObjectEntry;
029import org.kuali.rice.krad.datadictionary.validation.ValidationUtils;
030import org.kuali.rice.krad.datadictionary.validation.constraint.CaseConstraint;
031import org.kuali.rice.krad.datadictionary.validation.constraint.LookupConstraint;
032import org.kuali.rice.krad.datadictionary.validation.constraint.ValidCharactersConstraint;
033import org.kuali.rice.krad.datadictionary.validation.constraint.WhenConstraint;
034
035public 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}