001/**
002 * Copyright 2005-2016 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.krms.impl.repository.mock;
017
018import java.lang.reflect.InvocationTargetException;
019import java.math.BigDecimal;
020import java.math.BigInteger;
021import java.util.ArrayList;
022import java.util.Calendar;
023import java.util.Collection;
024import java.util.Date;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.concurrent.atomic.AtomicInteger;
029import java.util.concurrent.atomic.AtomicLong;
030import java.util.regex.Pattern;
031import org.apache.commons.beanutils.NestedNullException;
032import org.apache.commons.beanutils.PropertyUtils;
033import org.joda.time.DateTime;
034import org.kuali.rice.core.api.criteria.AndPredicate;
035import org.kuali.rice.core.api.criteria.EqualPredicate;
036import org.kuali.rice.core.api.criteria.GreaterThanOrEqualPredicate;
037import org.kuali.rice.core.api.criteria.GreaterThanPredicate;
038import org.kuali.rice.core.api.criteria.LessThanOrEqualPredicate;
039import org.kuali.rice.core.api.criteria.LessThanPredicate;
040import org.kuali.rice.core.api.criteria.LikePredicate;
041import org.kuali.rice.core.api.criteria.OrPredicate;
042import org.kuali.rice.core.api.criteria.Predicate;
043import org.kuali.rice.core.api.criteria.QueryByCriteria;
044
045/**
046 * A helper class for the Mock implementation to match criteria to values on the
047 * object
048 *
049 * @author nwright
050 */
051public class CriteriaMatcherInMemory<T> {
052
053    public CriteriaMatcherInMemory() {
054        super();
055    }
056    private QueryByCriteria criteria;
057
058    public QueryByCriteria getCriteria() {
059        return criteria;
060    }
061
062    public void setCriteria(QueryByCriteria criteria) {
063        this.criteria = criteria;
064    }
065
066    /**
067     * finds all of the supplied objects that match the specified criteria
068     *
069     * @param all
070     * @return filtered list
071     */
072    public Collection<T> findMatching(Collection<T> all) {
073        // no criteria means get all
074        if (criteria == null) {
075            return all;
076        }
077        int count = -1;
078        int startAt = 0;
079        if (this.criteria.getStartAtIndex() != null) {
080            startAt = this.criteria.getStartAtIndex();
081        }
082        int maxResults = Integer.MAX_VALUE;
083        if (this.criteria.getMaxResults() != null) {
084            maxResults = this.criteria.getMaxResults();
085        }
086        List<T> selected = new ArrayList<T>();
087        for (T obj : all) {
088            if (matches(obj)) {
089                count++;
090                if (count < startAt) {
091                    continue;
092                }
093                selected.add(obj);
094                if (count > maxResults) {
095                    break;
096                }
097            }
098        }
099        return selected;
100    }
101
102    /**
103     * Checks if an object matches the criteria
104     *
105     * @param infoObject
106     * @return
107     */
108    public boolean matches(T infoObject) {
109        return matches(infoObject, this.criteria.getPredicate());
110    }
111
112    /**
113     * protected for testing
114     */
115    protected boolean matches(T infoObject, Predicate predicate) {
116        // no predicate matches everyting
117        if (predicate == null) {
118            return true;
119        }
120        if (predicate instanceof OrPredicate) {
121            return matchesOr(infoObject, (OrPredicate) predicate);
122        }
123        if (predicate instanceof AndPredicate) {
124            return matchesAnd(infoObject, (AndPredicate) predicate);
125        }
126        if (predicate instanceof EqualPredicate) {
127            return matchesEqual(infoObject, (EqualPredicate) predicate);
128        }
129        if (predicate instanceof LessThanPredicate) {
130            return matchesLessThan(infoObject, (LessThanPredicate) predicate);
131        }
132        if (predicate instanceof LessThanOrEqualPredicate) {
133            return matchesLessThanOrEqual(infoObject, (LessThanOrEqualPredicate) predicate);
134        }
135        if (predicate instanceof GreaterThanPredicate) {
136            return matchesGreaterThan(infoObject, (GreaterThanPredicate) predicate);
137        }
138        if (predicate instanceof GreaterThanOrEqualPredicate) {
139            return matchesGreaterThanOrEqual(infoObject, (GreaterThanOrEqualPredicate) predicate);
140        }
141        if (predicate instanceof LikePredicate) {
142            return matchesLike(infoObject, (LikePredicate) predicate);
143        }
144        throw new UnsupportedOperationException("predicate type not supported yet in in-memory mathcer" + predicate.getClass().getName());
145    }
146
147    private boolean matchesOr(T infoObject, OrPredicate predicate) {
148        for (Predicate subPred : predicate.getPredicates()) {
149            if (matches(infoObject, subPred)) {
150                return true;
151            }
152        }
153        return false;
154    }
155
156    private boolean matchesAnd(T infoObject, AndPredicate predicate) {
157        for (Predicate subPred : predicate.getPredicates()) {
158            if (!matches(infoObject, subPred)) {
159                return false;
160            }
161        }
162        return true;
163    }
164
165    private boolean matchesEqual(T infoObject, EqualPredicate predicate) {
166        Object dataValue = extractValue(predicate.getPropertyPath(), infoObject);
167        return matchesEqual(dataValue, predicate.getValue().getValue());
168    }
169
170    private boolean matchesLessThan(T infoObject, LessThanPredicate predicate) {
171        Object dataValue = extractValue(predicate.getPropertyPath(), infoObject);
172        return matchesLessThan(dataValue, predicate.getValue().getValue());
173    }
174
175    private boolean matchesLessThanOrEqual(T infoObject, LessThanOrEqualPredicate predicate) {
176        Object dataValue = extractValue(predicate.getPropertyPath(), infoObject);
177        if (matchesLessThan(dataValue, predicate.getValue().getValue())) {
178            return true;
179        }
180        return matchesEqual(dataValue, predicate.getValue().getValue());
181    }
182
183    private boolean matchesGreaterThan(T infoObject, GreaterThanPredicate predicate) {
184        Object dataValue = extractValue(predicate.getPropertyPath(), infoObject);
185        return matchesGreaterThan(dataValue, predicate.getValue().getValue());
186    }
187
188    private boolean matchesGreaterThanOrEqual(T infoObject, GreaterThanOrEqualPredicate predicate) {
189        Object dataValue = extractValue(predicate.getPropertyPath(), infoObject);
190        if (matchesGreaterThan(dataValue, predicate.getValue().getValue())) {
191            return true;
192        }
193        return matchesEqual(dataValue, predicate.getValue().getValue());
194    }
195
196    private boolean matchesLike(T infoObject, LikePredicate predicate) {
197        Object dataValue = extractValue(predicate.getPropertyPath(), infoObject);
198        return matchesLike(dataValue, predicate.getValue().getValue());
199    }
200
201    protected static Object extractValue(String fieldPath, Object infoObject) {
202
203        try {
204            if (infoObject == null) {
205                return null;
206            }
207            Object value = PropertyUtils.getNestedProperty(infoObject, fieldPath);
208            // translate boolean to string so we can compare
209            // Have to do this because RICE's predicate does not support boolean 
210            // because it is database oriented and most DB do not support booleans natively.
211            if (value instanceof Boolean) {
212                return value.toString();
213            }
214            // See Rice's CriteriaSupportUtils.determineCriteriaValue where data normalized 
215            // translate date to joda DateTime because that is what RICE PredicateFactory does 
216            // similar to rest of the types 
217            if (value instanceof Date) {
218                return new DateTime ((Date) value);
219            }
220            if (value instanceof Calendar) {
221                return new DateTime ((Calendar) value);
222            }
223            if (value instanceof Short) {
224                return BigInteger.valueOf(((Short) value).longValue());
225            }
226            if (value instanceof AtomicLong) {
227                return BigInteger.valueOf(((AtomicLong) value).longValue());
228            }
229            if (value instanceof AtomicInteger) {
230                return BigInteger.valueOf(((AtomicInteger) value).longValue());
231            }
232            if (value instanceof Integer) {
233                return BigInteger.valueOf(((Integer)value).longValue());
234            }
235            if (value instanceof Long) {
236                return BigInteger.valueOf(((Long)value).longValue());
237            }
238            if (value instanceof Float) {
239                return BigDecimal.valueOf(((Float)value).doubleValue());
240            }
241            if (value instanceof Double) {
242                return BigDecimal.valueOf(((Double)value).doubleValue());
243            }
244            return value;
245        } catch (NestedNullException ex) {
246            return null;
247        }  catch (IllegalAccessException ex) {
248            throw new IllegalArgumentException(fieldPath, ex);
249        } catch (InvocationTargetException ex) {
250            throw new IllegalArgumentException(fieldPath, ex);
251        } catch (NoSuchMethodException ex) {
252            throw new IllegalArgumentException(fieldPath, ex);
253        }
254//        }
255//        return value;
256    }
257
258    public static boolean matchesEqual(Object dataValue, Object criteriaValue) {
259        if (dataValue == criteriaValue) {
260            return true;
261        }
262        if (dataValue == null && criteriaValue == null) {
263            return true;
264        }
265        if (dataValue == null) {
266            return false;
267        }
268        return dataValue.equals(criteriaValue);
269    }
270
271    public static boolean matchesLessThan(Object dataValue, Object criteriaValue) {
272        if (dataValue == criteriaValue) {
273            return false;
274        }
275        if (dataValue == null && criteriaValue == null) {
276            return false;
277        }
278        if (dataValue == null) {
279            return false;
280        }
281        if (criteriaValue instanceof Comparable) {
282            Comparable comp1 = (Comparable) dataValue;
283            Comparable comp2 = (Comparable) criteriaValue;
284            if (comp1.compareTo(comp2) < 0) {
285                return true;
286            }
287            return false;
288        }
289        throw new IllegalArgumentException("The values are not comparable " + criteriaValue);
290    }
291
292    public static boolean matchesGreaterThan(Object dataValue, Object criteriaValue) {
293        if (dataValue == criteriaValue) {
294            return false;
295        }
296        if (dataValue == null && criteriaValue == null) {
297            return false;
298        }
299        if (dataValue == null) {
300            return false;
301        }
302        if (criteriaValue instanceof Comparable) {
303            Comparable comp1 = (Comparable) dataValue;
304            Comparable comp2 = (Comparable) criteriaValue;
305            if (comp1.compareTo(comp2) > 0) {
306                return true;
307            }
308            return false;
309        }
310        throw new IllegalArgumentException("The values are not comparable " + criteriaValue);
311    }
312    // cache
313    private transient Map<String, Pattern> patternCache = new HashMap<String, Pattern>();
314
315    private Pattern getPattern(String expr) {
316        Pattern p = patternCache.get(expr);
317        if (p == null) {
318            p = compilePattern(expr);
319            patternCache.put(expr, p);
320        }
321        return p;
322    }
323
324    public boolean matchesLike(Object dataValue, Object criteriaValue) {
325        if (dataValue == criteriaValue) {
326            return false;
327        }
328        if (dataValue == null && criteriaValue == null) {
329            return false;
330        }
331        if (dataValue == null) {
332            return false;
333        }
334        return matchesLikeCachingPattern(dataValue.toString(), criteriaValue.toString());
335    }
336
337    /**
338     * this was taken from
339     * http://stackoverflow.com/questions/898405/how-to-implement-a-sql-like-like-operator-in-java
340     */
341    public boolean matchesLikeCachingPattern(final String str, final String expr) {
342        return matchesLike(str, getPattern(expr));
343    }
344
345    private static Pattern compilePattern(final String expr) {
346        String regex = quotemeta(expr);
347        regex = regex.replace("_", ".").replace("%", ".*?");
348        Pattern p = Pattern.compile(regex, Pattern.DOTALL);
349        return p;
350    }
351
352    /**
353     * This was taken from
354     *
355     * http://stackoverflow.com/questions/898405/how-to-implement-a-sql-like-like-operator-in-java
356     */
357    public static boolean matchesLike(final String str, final String expr) {
358        Pattern p = compilePattern(expr);
359        return matchesLike(str, p);
360    }
361
362    private static boolean matchesLike(final String str, final Pattern p) {
363        return p.matcher(str).matches();
364    }
365
366    private static String quotemeta(String s) {
367        if (s == null) {
368            throw new IllegalArgumentException("String cannot be null");
369        }
370
371        int len = s.length();
372        if (len == 0) {
373            return "";
374        }
375
376        StringBuilder sb = new StringBuilder(len * 2);
377        for (int i = 0; i < len; i++) {
378            char c = s.charAt(i);
379            if ("[](){}.*+?$^|#\\".indexOf(c) != -1) {
380                sb.append("\\");
381            }
382            sb.append(c);
383        }
384        return sb.toString();
385    }
386}