001/**
002 * Copyright 2005-2018 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.rice.krad.datadictionary.validation;
017
018import java.math.BigDecimal;
019import java.text.ParseException;
020import java.util.Collection;
021import java.util.Date;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025
026import org.apache.commons.lang.StringUtils;
027import org.kuali.rice.core.api.data.DataType;
028import org.kuali.rice.core.api.datetime.DateTimeService;
029import org.kuali.rice.krad.datadictionary.exception.AttributeValidationException;
030import org.kuali.rice.krad.uif.UifConstants;
031
032/**
033 * ValidationUtils provides static utility methods for validation processing
034 *
035 * <p>Inherited from Kuali Student and adapted extensively</p>
036 *
037 * @author Kuali Rice Team (rice.collab@kuali.org)
038 */
039public class ValidationUtils {
040
041    /**
042     * constructs a path by appending the attribute name to the provided path
043     *
044     * @param attributePath - a string representation of specifically which attribute (at some depth) is being accessed
045     * @param attributeName - the attribute name
046     * @return the path
047     */
048    public static String buildPath(String attributePath, String attributeName) {
049        if (StringUtils.isNotBlank(attributeName)) {
050            if (StringUtils.isNotBlank(attributePath)) {
051                return new StringBuilder(attributePath).append(".").append(attributeName).toString();
052            }
053
054            return attributeName;
055        }
056        return attributePath;
057    }
058
059    /**
060     * Used to get the rightmost index value of an attribute path.
061     *
062     * @param attributePath
063     * @return the right index of value of attribute path, -1 if path has no index
064     */
065    public static int getLastPathIndex(String attributePath) {
066        int index = -1;
067
068        int leftBracket = attributePath.lastIndexOf("[");
069        int rightBracket = attributePath.lastIndexOf("]");
070
071        if (leftBracket > 0 && rightBracket > leftBracket) {
072            String indexString = attributePath.substring(leftBracket + 1, rightBracket);
073            try {
074                index = Integer.valueOf(indexString).intValue();
075            } catch (NumberFormatException e) {
076                // Will just return -1
077            }
078        }
079
080        return index;
081    }
082
083    /**
084     * compares the value provided by the user and the one specified by the {@code WhenConstraint}
085     *
086     * @param fieldValue the value found in the field specified by a {@code CaseConstraint}'s {@code propertyName}
087     * @param whenValue the value specified by a {@code WhenConstraint}
088     * @param dataType the data type of the field which caseConstraint's propertyName refers to
089     * @param operator the relationship to check between the {@code fieldValue} and the {@code whenValue}
090     * @param isCaseSensitive whether string comparison will be carried out in a case sensitive fashion
091     * @param dateTimeService used to convert strings to dates
092     * @return true if the value matches the constraint
093     */
094    public static boolean compareValues(Object fieldValue, Object whenValue, DataType dataType, String operator,
095            boolean isCaseSensitive, DateTimeService dateTimeService) {
096
097        boolean result = false;
098        Integer compareResult = null;
099
100        if (UifConstants.CaseConstraintOperators.HAS_VALUE.equalsIgnoreCase(operator)) {
101            if (fieldValue == null) {
102                return "false".equals(whenValue.toString().toLowerCase());
103            }
104            if (fieldValue instanceof String && ((String) fieldValue).isEmpty()) {
105                return "false".equals(whenValue.toString().toLowerCase());
106            }
107            if (fieldValue instanceof Collection && ((Collection<?>) fieldValue).isEmpty()) {
108                return "false".equals(whenValue.toString().toLowerCase());
109            }
110            return "true".equals(whenValue.toString().toLowerCase());
111        }
112        // Convert objects into appropriate data types
113        if (null != dataType) {
114            if (DataType.STRING.equals(dataType)) {
115                String v1 = getString(fieldValue);
116                String v2 = getString(whenValue);
117
118                if (!isCaseSensitive) {
119                    v1 = v1.toUpperCase();
120                    v2 = v2.toUpperCase();
121                }
122
123                compareResult = v1.compareTo(v2);
124            } else if (DataType.INTEGER.equals(dataType)) {
125                Integer v1 = getInteger(fieldValue);
126                Integer v2 = getInteger(whenValue);
127                compareResult = v1.compareTo(v2);
128            } else if (DataType.LONG.equals(dataType)) {
129                Long v1 = getLong(fieldValue);
130                Long v2 = getLong(whenValue);
131                compareResult = v1.compareTo(v2);
132            } else if (DataType.DOUBLE.equals(dataType)) {
133                Double v1 = getDouble(fieldValue);
134                Double v2 = getDouble(whenValue);
135                compareResult = v1.compareTo(v2);
136            } else if (DataType.FLOAT.equals(dataType)) {
137                Float v1 = getFloat(fieldValue);
138                Float v2 = getFloat(whenValue);
139                compareResult = v1.compareTo(v2);
140            } else if (DataType.BOOLEAN.equals(dataType)) {
141                Boolean v1 = getBoolean(fieldValue);
142                Boolean v2 = getBoolean(whenValue);
143                compareResult = v1.compareTo(v2);
144            } else if (DataType.DATE.equals(dataType)) {
145                Date v1 = getDate(fieldValue, dateTimeService);
146                Date v2 = getDate(whenValue, dateTimeService);
147                compareResult = v1.compareTo(v2);
148            }
149        }
150
151        if (null != compareResult) {
152            if ((UifConstants.CaseConstraintOperators.EQUALS.equalsIgnoreCase(operator) || UifConstants
153                    .CaseConstraintOperators.GREATER_THAN_EQUAL.equalsIgnoreCase(operator) || UifConstants
154                    .CaseConstraintOperators.LESS_THAN_EQUAL.equalsIgnoreCase(operator)) && 0 == compareResult) {
155                result = true;
156            }
157
158            if ((UifConstants.CaseConstraintOperators.NOT_EQUAL.equalsIgnoreCase(operator) || UifConstants
159                    .CaseConstraintOperators.NOT_EQUALS.equalsIgnoreCase(operator) || UifConstants
160                    .CaseConstraintOperators.GREATER_THAN.equalsIgnoreCase(operator)) && compareResult >= 1) {
161                result = true;
162            }
163
164            if ((UifConstants.CaseConstraintOperators.NOT_EQUAL.equalsIgnoreCase(operator) || UifConstants
165                    .CaseConstraintOperators.NOT_EQUALS.equalsIgnoreCase(operator) || UifConstants
166                    .CaseConstraintOperators.LESS_THAN.equalsIgnoreCase(operator)) && compareResult <= -1) {
167                result = true;
168            }
169        }
170
171        return result;
172    }
173
174    /**
175     * converts the provided object into an integer
176     *
177     * @param o - the object to convert
178     * @return the integer value
179     */
180    public static Integer getInteger(Object o) {
181        Integer result = null;
182        if (o instanceof Integer) {
183            return (Integer) o;
184        }
185        if (o == null) {
186            return null;
187        }
188        if (o instanceof Number) {
189            return ((Number) o).intValue();
190        }
191        String s = o.toString();
192        if (s != null && s.trim().length() > 0) {
193            result = Integer.valueOf(s.trim());
194        }
195        return result;
196    }
197
198    /**
199     * converts the provided object into a long
200     *
201     * @param o - the object to convert
202     * @return the long value
203     */
204    public static Long getLong(Object o) {
205        Long result = null;
206        if (o instanceof Long) {
207            return (Long) o;
208        }
209        if (o == null) {
210            return null;
211        }
212        if (o instanceof Number) {
213            return ((Number) o).longValue();
214        }
215        String s = o.toString();
216        if (s != null && s.trim().length() > 0) {
217            result = Long.valueOf(s.trim());
218        }
219        return result;
220    }
221
222    /**
223     * converts the provided object into an float
224     *
225     * @param o - the object to convert
226     * @return the float value
227     */
228    public static Float getFloat(Object o) {
229        Float result = null;
230        if (o instanceof Float) {
231            return (Float) o;
232        }
233        if (o == null) {
234            return null;
235        }
236        if (o instanceof Number) {
237            return ((Number) o).floatValue();
238        }
239        String s = o.toString();
240        if (s != null && s.trim().length() > 0) {
241            result = Float.valueOf(s.trim());
242        }
243        return result;
244    }
245
246    /**
247     * converts the provided object into a double
248     *
249     * @param o - the object to convert
250     * @return the double value
251     */
252    public static Double getDouble(Object o) {
253        Double result = null;
254        if (o instanceof BigDecimal) {
255            return ((BigDecimal) o).doubleValue();
256        }
257        if (o instanceof Double) {
258            return (Double) o;
259        }
260        if (o == null) {
261            return null;
262        }
263        if (o instanceof Number) {
264            return ((Number) o).doubleValue();
265        }
266        String s = o.toString();
267        if (s != null && s.trim().length() > 0) {
268            result = Double.valueOf(s.trim());
269        }
270        return result;
271    }
272
273    /**
274     * determines whether the provided object is a date and tries to converts non-date values
275     *
276     * @param object - the object to convert/cast into a date
277     * @param dateTimeService - used to convert strings to dates
278     * @return a date object
279     * @throws IllegalArgumentException
280     */
281    public static Date getDate(Object object, DateTimeService dateTimeService) throws IllegalArgumentException {
282        Date result = null;
283        if (object instanceof Date) {
284            return (Date) object;
285        }
286        if (object == null) {
287            return null;
288        }
289        String s = object.toString();
290        if (s != null && s.trim().length() > 0) {
291            try {
292                result = dateTimeService.convertToDate(s.trim());
293            } catch (ParseException e) {
294                throw new IllegalArgumentException(e);
295            }
296        }
297        return result;
298    }
299
300    /**
301     * converts the provided object into a string
302     *
303     * @param o - the object to convert
304     * @return the string value
305     */
306    public static String getString(Object o) {
307        if (o instanceof String) {
308            return (String) o;
309        }
310        if (o == null) {
311            return null;
312        }
313        return o.toString();
314    }
315
316    /**
317     * converts the provided object into a boolean
318     *
319     * @param o - the object to convert
320     * @return the boolean value
321     */
322    public static Boolean getBoolean(Object o) {
323        Boolean result = null;
324        if (o instanceof Boolean) {
325            return (Boolean) o;
326        }
327        if (o == null) {
328            return null;
329        }
330        String s = o.toString();
331        if (s != null && s.trim().length() > 0) {
332            result = Boolean.parseBoolean(s.trim());
333        }
334        return result;
335    }
336
337    /**
338     * checks whether the string contains non-whitespace characters
339     *
340     * @param string
341     * @return true if the string contains at least one none-whitespace character, false otherwise
342     */
343    public static boolean hasText(String string) {
344
345        if (string == null || string.length() < 1) {
346            return false;
347        }
348        int stringLength = string.length();
349
350        for (int i = 0; i < stringLength; i++) {
351            char currentChar = string.charAt(i);
352            if (' ' != currentChar || '\t' != currentChar || '\n' != currentChar) {
353                return true;
354            }
355        }
356
357        return false;
358    }
359
360    /**
361     * Checks whether the provided object is null, or if a String, List, Set or Map is empty.
362     *
363     * @param value - the object to check
364     * @return true if the object is null or if a String, List, Set, or Map is empty, false otherwise
365     */
366    public static boolean isNullOrEmpty(Object value) {
367        boolean nullOrEmpty = false;
368        if (value == null) {
369            nullOrEmpty = true;
370        }
371        else if (value instanceof String) {
372            nullOrEmpty = StringUtils.isBlank(((String) value).trim());
373        }
374        else if (value instanceof List) {
375            nullOrEmpty = ((List)value).isEmpty();
376        }
377        else if (value instanceof Set) {
378            nullOrEmpty = ((Set)value).isEmpty();
379        }
380        else if (value instanceof Map) {
381            nullOrEmpty = ((Map)value).isEmpty();
382        }
383
384        return nullOrEmpty;
385    }
386
387    /**
388     * defines possible result values of a comparison operation
389     */
390    public static enum Result {
391        VALID, INVALID, UNDEFINED
392    }
393
394    ;
395
396    /**
397     * attempts to convert the provided value to the given dataType
398     *
399     * @param value - the object to convert
400     * @param dataType - the data type to convert into
401     * @param dateTimeService - used to convert strings to dates
402     * @return the converted value if null or successful, otherwise throws an exception
403     * @throws AttributeValidationException
404     */
405    public static Object convertToDataType(Object value, DataType dataType,
406            DateTimeService dateTimeService) throws AttributeValidationException {
407        Object returnValue = value;
408
409        if (null == value) {
410            return null;
411        }
412
413        switch (dataType) {
414            case BOOLEAN:
415                if (!(value instanceof Boolean)) {
416                    returnValue = Boolean.valueOf(value.toString());
417
418                    // Since the Boolean.valueOf is exceptionally loose - it basically takes any string and makes it false
419                    if (!value.toString().equalsIgnoreCase("TRUE") && !value.toString().equalsIgnoreCase("FALSE")) {
420                        throw new AttributeValidationException("Value " + value.toString() + " is not a boolean!");
421                    }
422                }
423                break;
424            case INTEGER:
425                if (!(value instanceof Number)) {
426                    returnValue = Integer.valueOf(value.toString());
427                }
428                break;
429            case LONG:
430                if (!(value instanceof Number)) {
431                    returnValue = Long.valueOf(value.toString());
432                }
433                break;
434            case DOUBLE:
435                if (!(value instanceof Number)) {
436                    returnValue = Double.valueOf(value.toString());
437                }
438                if (((Double) returnValue).isNaN()) {
439                    throw new AttributeValidationException("Infinite Double values are not valid!");
440                }
441                if (((Double) returnValue).isInfinite()) {
442                    throw new AttributeValidationException("Infinite Double values are not valid!");
443                }
444                break;
445            case FLOAT:
446                if (!(value instanceof Number)) {
447                    returnValue = Float.valueOf(value.toString());
448                }
449                if (((Float) returnValue).isNaN()) {
450                    throw new AttributeValidationException("NaN Float values are not valid!");
451                }
452                if (((Float) returnValue).isInfinite()) {
453                    throw new AttributeValidationException("Infinite Float values are not valid!");
454                }
455                break;
456            case TRUNCATED_DATE:
457            case DATE:
458                if (!(value instanceof Date)) {
459                    try {
460                        returnValue = dateTimeService.convertToDate(value.toString());
461                    } catch (ParseException pe) {
462                        throw new AttributeValidationException("Value " + value.toString() + " is not a date!");
463                    }
464                }
465                break;
466            case STRING:
467        }
468
469        return returnValue;
470    }
471
472    /**
473     * checks whether the provided value is greater than the limit given
474     *
475     * @param value - the object to check
476     * @param limit - the limit to use
477     * @param <T>
478     * @return one of the values in {@link  Result}
479     */
480    public static <T> Result isGreaterThan(T value, Comparable<T> limit) {
481        return limit == null ? Result.UNDEFINED : (limit.compareTo(value) < 0 ? Result.VALID : Result.INVALID);
482    }
483
484    /**
485     * checks whether the provided value is greater than or equal to the limit given
486     *
487     * @param value - the object to check
488     * @param limit - the limit to use
489     * @param <T>
490     * @return one of the values in {@link  Result}
491     */
492    public static <T> Result isGreaterThanOrEqual(T value, Comparable<T> limit) {
493        return limit == null ? Result.UNDEFINED : (limit.compareTo(value) <= 0 ? Result.VALID : Result.INVALID);
494    }
495
496    /**
497     * checks whether the provided value is less than the limit given
498     *
499     * @param value - the object to check
500     * @param limit - the limit to use
501     * @param <T>
502     * @return one of the values in {@link  Result}
503     */
504    public static <T> Result isLessThan(T value, Comparable<T> limit) {
505        return limit == null ? Result.UNDEFINED : (limit.compareTo(value) > 0 ? Result.VALID : Result.INVALID);
506    }
507
508    /**
509     * checks whether the provided value is greater than the limit given
510     *
511     * @param value - the object to check
512     * @param limit - the limit to use
513     * @param <T>
514     * @return one of the values in {@link  Result}
515     */
516    public static <T> Result isLessThanOrEqual(T value, Comparable<T> limit) {
517        return limit == null ? Result.UNDEFINED : (limit.compareTo(value) >= 0 ? Result.VALID : Result.INVALID);
518    }
519
520    /**
521     * converts a path into an array of its path components
522     *
523     * @param fieldPath - a string representation of specifically which attribute (at some depth) is being accessed
524     * @return the array of path components
525     */
526    public static String[] getPathTokens(String fieldPath) {
527        return (fieldPath != null && fieldPath.contains(".") ? fieldPath.split("\\.") : new String[]{fieldPath});
528    }
529
530}
531