001 /*
002 * Copyright 2011 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.osedu.org/licenses/ECL-2.0
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.r2.common.datadictionary.util;
017
018import org.kuali.rice.core.api.uif.DataType;
019import org.kuali.student.r2.common.datadictionary.infc.AttributeDefinitionInfc;
020import org.kuali.student.r2.common.datadictionary.infc.DictionaryEntry;
021import org.kuali.student.r2.common.exceptions.InvalidParameterException;
022import org.kuali.student.r2.common.exceptions.OperationFailedException;
023import org.kuali.student.r2.common.infc.Comparison;
024import org.kuali.student.r2.common.infc.Criteria;
025import org.kuali.student.r2.common.util.date.DateFormatters;
026
027import java.sql.Timestamp;
028import java.util.ArrayList;
029import java.util.Date;
030import java.util.List;
031
032 /**
033 * A class that validates the criteria against the dictionary
034 *
035 * It also sets the parsedValues and parsedOperators so they may be used in the implementation
036 * The call sequence is sometehing like this:
037 * <ol>
038 * <li>construct this validator
039 * <li>Configure this validator with criteria to be valiated
040 * <li>Get the dictionary entry of the object to be validated by calling DataDictionaryServiceInfc.getDataDictionaryEntry (the ref object's URI)
041 * <li>Configure this validator with the dictionary entry that you just got from the dictionary
042 * <li>Call the validate () method which will throw exceptions if problems
043 * <li>get the parsed values (@see getParsedValues ()) and use them in the implementation
044 * <li>Get the parsed operators and use in the implementation, if you want
045 * </ol>
046 * @author nwright
047 */
048public class CriteriaValidatorParser {
049
050    public enum Operator {
051
052        EQ, IN, GT, LT, NEQ, GTE, LTE, LIKE, BETWEEN;
053    }
054    private Criteria criteria;
055    private DictionaryEntry dictionaryEntry;
056    private transient List<Object> parsedValues;
057    private transient List<Operator> parsedOperators;
058
059    public CriteriaValidatorParser() {
060    }
061
062    public Criteria getCriteria() {
063        return criteria;
064    }
065
066    public void setCriteria(Criteria criteria) {
067        this.criteria = criteria;
068    }
069
070    public DictionaryEntry getDictionaryEntry() {
071        return dictionaryEntry;
072    }
073
074    public void setDictionaryEntry(DictionaryEntry dictionaryEntry) {
075        this.dictionaryEntry = dictionaryEntry;
076    }
077
078    /**
079     * Get the string operators translated as an Operator enum
080     * @return list of operators in the same ordinal position as the supplied operators in the list of ComparisonInfc objects
081     */
082    public List<Operator> getParsedOperators() {
083        return parsedOperators;
084    }
085
086    /**
087     * The values parsed as their respective data types, String, Date, Integer, etc.
088     *
089     * Special considerations:
090     * IN operators return a List of one or more parsed values who's order is the samne as the order in the ComparisonInfc
091     * BETWEEN operators return a list of two parsed values who's values coorespond to the from to values of a between operation.
092     *
093     * @return a list of parsed values in the same ordinal positions as the unparsed values in the list of ComparisonInfc objects
094     */
095    public List<Object> getParsedValues() {
096        return parsedValues;
097    }
098
099
100
101    /**
102     * Validate the criteria thowing an InvalidParameterException if there are problems
103     *
104     * TODO: Consider getting all the errors and throw them as a group instead of as we find them
105     *
106     * @throws InvalidParameterException
107     * @throws OperationFailedException
108     */
109    public void validate()
110            throws InvalidParameterException,
111            OperationFailedException {
112        parsedValues = new ArrayList<Object>();
113        parsedOperators = new ArrayList <Operator> ();
114        if (this.criteria.getComparisons() == null) {
115            throw new InvalidParameterException ("Comparisons list is null -- to select all specify an empty list");
116        }
117        int i = 0;
118        for (Comparison comparison : this.criteria.getComparisons()) {
119            this.validate(i, comparison);
120            i++;
121        }
122    }
123
124    private void validate(int i, Comparison comparison)
125            throws InvalidParameterException,
126            OperationFailedException {
127        String fieldKey = comparison.getFieldKey();
128        String operator = comparison.getOperator();
129        List<String> values = comparison.getValues();
130        AttributeDefinitionInfc ad = this.getAttributeDefinition(fieldKey);
131        if (ad == null) {
132            throw new InvalidParameterException("The " + i + "th comparison's field key " + fieldKey + " is not defined in the dictionary");
133        }
134        if (operator == null) {
135            throw new InvalidParameterException("The " + i + "th comparison's operator is null");
136        }
137        if (operator.equals("=")) {
138            this.parsedOperators.add(Operator.EQ);
139        } else if (operator.equals("<")) {
140            this.parsedOperators.add(Operator.LT);
141        } else if (operator.equals(">")) {
142            this.parsedOperators.add(Operator.GT);
143        } else if (operator.equals("!=")) {
144            this.parsedOperators.add(Operator.NEQ);
145        } else if (operator.equals("<=")) {
146            this.parsedOperators.add(Operator.LTE);
147        } else if (operator.equals(">=")) {
148            this.parsedOperators.add(Operator.GTE);
149        } else if (operator.equals("in")) {
150            this.parsedOperators.add(Operator.IN);
151        } else if (operator.equals("between")) {
152            this.parsedOperators.add(Operator.BETWEEN);
153        } else if (operator.equals("like")) {
154            this.parsedOperators.add(Operator.LIKE);
155            if (ad.getDataType().equals (DataType.STRING)) {
156            throw new InvalidParameterException("The " + i + "th comparison's operator is LIKE which can only be applied to strings, " + ad.getDataType() + " is invalid.");
157            }
158        } else {
159            throw new InvalidParameterException("The " + i + "th comparison's operator, " + operator + ", is invalid.");
160        }
161        if (values == null) {
162            throw new InvalidParameterException("The " + i + "th comparison's values list is required and cannot be null");
163        }
164        if (values.isEmpty()) {
165            throw new InvalidParameterException("The " + i + "th comparison's values list is required and cannot be an empty list");
166        }
167        if (values.get(0) == null) {
168            if (!operator.equals("=") && !operator.equals("!=")) {
169                throw new InvalidParameterException("The " + i + "th comparison's value is null but the operator " + operator + " is a comparison operator that does not apply");
170            }
171            return;
172        }
173        if (operator.equals("between")) {
174            if (values.size() != 2) {
175                throw new InvalidParameterException("The " + i + "th comparison is a between operator which requires two values, found " + values.size());
176            }
177            if (values.get(0) == null) {
178                throw new InvalidParameterException("The " + i + "th comparison is a between operator but the first value is null");
179            }
180            if (values.get(1) == null) {
181                throw new InvalidParameterException("The " + i + "th comparison is a between operator but the second value is null");
182            }
183        } else if (values.size() > 1) {
184            if (!operator.equals("in")) {
185                throw new InvalidParameterException("The " + i + "th comparison's value is a list but the operator " + operator + " is a comparison operator that does not apply");
186            }
187        }
188        switch (ad.getDataType()) {
189            case STRING:
190                break;
191            case DATE:
192            case TRUNCATED_DATE:
193                break;
194            case BOOLEAN:
195                if (! operator.equals("=") && !operator.equals("!=")) {
196                    throw new InvalidParameterException("The " + i + "th comparison's operator " + operator + " is a comparison operator that does not apply to the field's boolean data type");
197                }
198            case INTEGER:
199            case FLOAT:
200            case DOUBLE:
201            case LONG:
202                break;
203//            case COMPLEX:
204//                if (! operator.equals("=") && !operator.equals("!=")) {
205//                    throw new InvalidParameterException("The " + i + "th comparison's operator " + operator + " is a comparison operator that does not apply to the field's complex data type");
206//                }
207//                if (values.get(0) == null) {
208//                    throw new InvalidParameterException("The " + i + "th comparison's value is not null but attribute type is complex. Complex can only be checked to see if it is null or not null");
209//                }
210        }
211        parsedValues.add(parseValues(i, ad.getDataType(), comparison.getValues(), comparison.getIsIgnoreCase()));
212    }
213
214    private Object parseValues(int i, DataType dataType, List<String> values, boolean ignoreCase)
215            throws InvalidParameterException {
216        if (values.size() == 1) {
217            return parseValue(i, dataType, values.get(0), ignoreCase);
218        }
219        List<Object> list = new ArrayList<Object>();
220        for (String value : values) {
221            list.add(parseValue(i, dataType, value, ignoreCase));
222        }
223        return list;
224    }
225
226    private Object parseValue(int i, DataType dataType, String value, boolean ignoreCase)
227            throws InvalidParameterException {
228        if (value == null) {
229            return null;
230        }
231        switch (dataType) {
232            case STRING:
233                return parseString(i, value, ignoreCase);
234            case DATE:
235                return parseDateTime(i, value);
236            case TRUNCATED_DATE:
237                return parseDate(i, value);
238            case BOOLEAN:
239                return parseBoolean(i, value);
240            case INTEGER:
241                return parseInteger(i, value);
242            case FLOAT:
243                return parseFloat(i, value);
244            case DOUBLE:
245                return parseDouble(i, value);
246            case LONG:
247                return parseLong(i, value);
248//            case COMPLEX:
249//                throw new InvalidParameterException("The " + i + "th comparison's value is not null but attribute type is complex. Complex can only be checked to see if it is null or not null");
250            default:
251                throw new IllegalArgumentException("Unknown/unhandled datatype " + dataType);
252        }
253    }
254
255    private String parseString(int i, String cv, boolean ignoreCase) throws InvalidParameterException {
256        if (cv == null) {
257            return null;
258        }
259        if (ignoreCase) {
260            return cv.toLowerCase();
261        }
262        return cv;
263    }
264
265    private Timestamp parseDateTime(int i, String cv) throws InvalidParameterException {
266        try {
267            return new Timestamp(DateFormatters.DEFAULT_TIMESTAMP_FORMATTER.parse(cv).getTime());
268        } catch (IllegalArgumentException ex) {
269            throw new InvalidParameterException("The " + i + "th comparison's value " + cv + " cannot be parsed as a dateTime");
270        }
271    }
272
273    private Date parseDate(int i, String cv) throws InvalidParameterException {
274        try {
275            return DateFormatters.DEFAULT_DATE_FORMATTER.parse(cv);
276        } catch (IllegalArgumentException ex) {
277            throw new InvalidParameterException("The " + i + "th comparison's value " + cv + " cannot be parsed as a date");
278        }
279    }
280
281    private Integer parseInteger(int i, String cv) throws InvalidParameterException {
282        try {
283            return Integer.parseInt(cv);
284        } catch (NumberFormatException ex) {
285            throw new InvalidParameterException("The " + i + "th comparison's value " + cv + " cannot be parsed as an integer");
286        }
287    }
288
289    private Long parseLong(int i, String cv) throws InvalidParameterException {
290        try {
291            return Long.parseLong(cv);
292        } catch (NumberFormatException ex) {
293            throw new InvalidParameterException("The " + i + "th comparison's value " + cv + " cannot be parsed as a Long");
294        }
295    }
296
297    private Boolean parseBoolean(int i, String cv) throws InvalidParameterException {
298        if (cv.equalsIgnoreCase("true")) {
299            return Boolean.TRUE;
300        }
301        if (cv.equalsIgnoreCase("false")) {
302            return Boolean.FALSE;
303        }
304        throw new InvalidParameterException("The " + i + "th comparison's value " + cv + " cannot be parsed as a Boolean");
305
306    }
307
308    private Float parseFloat(int i, String cv) throws InvalidParameterException {
309        try {
310            return Float.parseFloat(cv);
311        } catch (NumberFormatException ex) {
312            throw new InvalidParameterException("The " + i + "th comparison's value " + cv + " cannot be parsed as an float");
313        }
314    }
315
316    private Double parseDouble(int i, String cv) throws InvalidParameterException {
317        try {
318            return Double.parseDouble(cv);
319        } catch (NumberFormatException ex) {
320            throw new InvalidParameterException("The " + i + "th comparison's value " + cv + " cannot be parsed as an double");
321        }
322    }
323
324    private String initLower(String str) {
325        if (str == null) {
326            return null;
327        }
328        if (str.length() == 0) {
329            return str;
330        }
331        if (str.length() == 1) {
332            return str.toLowerCase();
333        }
334        return str.substring(0, 1).toLowerCase() + str.substring(1);
335    }
336
337    private boolean calcIsList(AttributeDefinitionInfc ad) {
338        if (ad.getMaxOccurs() == null) {
339            return false;
340        }
341        if (ad.getMaxOccurs() <= 1) {
342            return false;
343        }
344        return true;
345
346    }
347
348    private AttributeDefinitionInfc getAttributeDefinition(String fk)
349            throws InvalidParameterException,
350            OperationFailedException {
351        for (AttributeDefinitionInfc ad : this.dictionaryEntry.getAttributes()) {
352            if (ad.getName().equals(fk)) {
353                return ad;
354            }
355        }
356       return null;
357    }
358
359}
360