001/*
002 * Stallion Core: A Modern Web Framework
003 *
004 * Copyright (C) 2015 - 2016 Stallion Software LLC.
005 *
006 * This program is free software: you can redistribute it and/or modify it under the terms of the
007 * GNU General Public License as published by the Free Software Foundation, either version 2 of
008 * the License, or (at your option) any later version. This program is distributed in the hope that
009 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
011 * License for more details. You should have received a copy of the GNU General Public License
012 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
013 *
014 *
015 *
016 */
017
018package io.stallion.dataAccess.filtering;
019
020import io.stallion.dataAccess.Model;
021import io.stallion.dataAccess.LocalMemoryStash;
022import io.stallion.exceptions.UsageException;
023import io.stallion.reflection.PropertyComparator;
024import io.stallion.reflection.PropertyUtils;
025import io.stallion.services.Log;
026import io.stallion.utils.Literals;
027import org.apache.commons.codec.digest.DigestUtils;
028import org.apache.commons.lang3.StringUtils;
029
030
031import java.math.BigInteger;
032import java.util.*;
033import java.util.function.Consumer;
034
035import static io.stallion.utils.Literals.asArray;
036import static io.stallion.utils.Literals.empty;
037import static io.stallion.utils.Literals.list;
038
039/**
040 * A FilterChain is the default way by which data from a ModelController is actually queried
041 * and accessed.
042 *
043 * You use it as follows:
044 *
045 * List&lt;Books&gt; books = MyBooksController.instance()
046 *     .filter('published', true')
047 *     .filter('author', 'Mark Twain')
048 *     .exclude('publishDate.year', 2014)
049 *     .sort('publishDate', 'desc')
050 *     .all();
051 *
052 * You can chain together as many "filter" commands as you want. The chain is not actually executed
053 * until you call one of the access methods: all(), first(), count(), groupBy()
054 *
055 *
056 * @param <T>
057 */
058public class FilterChain<T extends Model> implements Iterable<T> {
059    private List<T> originalObjects;
060    private List<T> objects;
061    protected ArrayList<FilterOperation> operations = new ArrayList<FilterOperation>();
062    private String sortField = "";
063    private SortDirection sortDirection;
064    private String bucket;
065    private String extraCacheKey = "";
066    private boolean _includeDeleted = false;
067    private Integer matchingCount = 0;
068    private LocalMemoryStash<T> stash;
069
070    public FilterChain(String bucket, LocalMemoryStash<T> stash) {
071        this.setBucket(bucket);
072        this.stash = stash;
073        this.originalObjects = new ArrayList();
074    }
075
076    public FilterChain(String bucket, List<T> originalObjects, LocalMemoryStash<T> stash) {
077        this.setBucket(bucket);
078        this.stash = stash;
079        this.originalObjects = (List<T>)originalObjects;
080    }
081    public FilterChain(String bucket, List<T> originalObjects, FilterOperation op, LocalMemoryStash<T> stash) {
082        this.setBucket(bucket);
083        this.stash = stash;
084        this.originalObjects = originalObjects;
085        operations.add(op);
086    }
087
088    public FilterChain(FilterChain chain, LocalMemoryStash<T> stash) {
089        this.setBucket(chain.getBucket());
090        this.stash = stash;
091        this.operations = (ArrayList<FilterOperation>)chain.operations.clone();
092        this.sortField = chain.sortField;
093        this.sortDirection = chain.sortDirection;
094        this.originalObjects = chain.originalObjects;
095    }
096
097    /**
098     * Do a basic equality filter
099     *
100     * @param name
101     * @param val
102     * @return
103     */
104    public FilterChain<T> filter(String name, Object val) {
105        return filter(name, val, "eq");
106    }
107
108    /**
109     * Do a basic equality filter.
110     *
111     * @param name
112     * @param value
113     * @return
114     */
115    public FilterChain<T> filter(String name, Comparable value)  {
116        return filter(name, value, "eq");
117    }
118
119    /**
120     * Add a filter to the chain
121     * @param name
122     * @param value
123     * @param op
124     * @return
125     */
126    public FilterChain<T> filter(String name, Object value, String op) {
127        return filterBy(name, value, FilterOperator.fromString(op));
128    }
129
130    public FilterChain<T> filter(String name, Comparable value, String op) {
131        return filterBy(name, value, FilterOperator.fromString(op));
132    }
133
134    /**
135     * Add a filter to the chain with a custom operator
136     * @param name
137     * @param value
138     * @param op
139     * @return
140     */
141    public FilterChain<T> filterBy(String name, Object value, FilterOperator op) {
142
143        FilterOperation fop = new FilterOperation();
144        fop.setFieldName(name);
145        fop.setOperator(op);
146        fop.setOriginalValue(value);
147        if (value instanceof Iterable) {
148            fop.setIterable(value);
149        }
150        return cloneChainAndAddOperation(fop);
151    }
152
153
154    /**
155     * Add a filter to the chain with a custom operator
156     * @param name
157     * @param value
158     * @param op
159     * @return
160     */
161    public FilterChain<T> filterBy(String name, Comparable value, FilterOperator op) {
162        FilterOperation fop = new FilterOperation();
163        fop.setFieldName(name);
164        fop.setOperator(op);
165        fop.setOriginalValue(value);
166        return cloneChainAndAddOperation(fop);
167    }
168
169
170    /**
171     * Searches for @value in all @fields, using a case-insensitive
172     * string contains search.
173     *
174     * @param value
175     * @param value
176     * @return
177     */
178    public FilterChain<T> search(String value, String...fields) {
179        if (fields.length == 0) {
180            throw new UsageException("You must include at least one field to search");
181        }
182        if (Literals.empty(value)) {
183            throw new UsageException("value must be at least one character long");
184        }
185
186        List<Or> ors = list();
187        for (String field: fields) {
188            ors.add(new Or(field, value, FilterOperator.LIKE));
189        }
190        FilterChain<T> chain = andAnyOf(asArray(ors, Or.class));
191        return chain;
192    }
193
194
195    /**
196     * Excludes all matching items instead of including them.
197     *
198     * @param name
199     * @param value
200     * @return
201     */
202    public FilterChain exclude(String name, Object value)  {
203        return exclude(name, value, "eq");
204    }
205
206
207    /**
208     * Excludes all matching items instead of including them.
209     *
210     * @param name
211     * @param value
212     * @param op
213     * @return
214     */
215    public FilterChain exclude(String name, Object value, String op) {
216        return excludeBy(name, value, FilterOperator.fromString(op));
217    }
218
219    /**
220     * Excludes all matching items instead of including them.
221     *
222     * @param name
223     * @param value
224     * @param op
225     * @return
226     */
227    public FilterChain excludeBy(String name, Object value, FilterOperator op)  {
228        FilterOperation fop = new FilterOperation();
229        fop.setFieldName(name);
230        fop.setOperator(op);
231        fop.setOriginalValue(value);
232        fop.setIsExclude(true);
233        return cloneChainAndAddOperation(fop);
234    }
235
236    public FilterChain<T> andAnyOf(Map<String, Object> allowedMatches) {
237        List<Or> ors = list();
238        for (Map.Entry<String, Object> allowed: allowedMatches.entrySet()) {
239            ors.add(new Or(allowed.getKey(), allowed.getValue()));
240        }
241        return andAnyOf(asArray(ors, Or.class));
242    }
243
244    public FilterChain<T> andAnyOf(List<String>...tuples) {
245        List<Or> ors = list();
246        for (List<String> tuple: tuples) {
247            Or or;
248            if (tuple.size() == 2) {
249                or = new Or(tuple.get(0), tuple.get(1));
250            } else if (tuple.size() == 3) {
251                or = new Or(tuple.get(0), tuple.get(1), tuple.get(2));
252            } else {
253                throw new UsageException("When calling andAnyOf, you must pass in a list of strings that either [field, value] or [field, value, op]");
254            }
255            ors.add(or);
256        }
257        return andAnyOf(asArray(ors, Or.class));
258    }
259
260    public FilterChain<T> andAnyOf(Or...ors) {
261        if (ors.length == 0) {
262            return this;
263        }
264        FilterOperation operation = new FilterOperation();
265        operation.setOrOperation(true);
266        operation.setOrSubOperations(list());
267        for(Or or: ors) {
268            FilterOperation subOp = new FilterOperation();
269            subOp.setOriginalValue(or.getValue());
270            subOp.setFieldName(or.getField());
271            subOp.setOperator(or.getOp());
272            operation.getOrSubOperations().add(subOp);
273        }
274        return cloneChainAndAddOperation(operation);
275    }
276
277    public FilterChain<T> excludeAnyOf(Or...ors) {
278        if (ors.length == 0) {
279            return this;
280        }
281        FilterOperation operation = new FilterOperation();
282        operation.setOrOperation(true);
283        operation.setOrSubOperations(list());
284        operation.setIsExclude(true);
285        for(Or or: ors) {
286            FilterOperation subOp = new FilterOperation();
287            subOp.setOriginalValue(or.getValue());
288            subOp.setFieldName(or.getField());
289            subOp.setOperator(or.getOp());
290            operation.getOrSubOperations().add(subOp);
291        }
292        return cloneChainAndAddOperation(operation);
293    }
294
295
296    protected FilterChain<T> cloneChainAndAddOperation(FilterOperation operation) {
297        FilterChain<T> chain = newCopy();
298        chain.operations.add(operation);
299        return chain;
300    }
301
302    /**
303     * Create a copy this filter chain.
304     *
305     * @return
306     */
307    protected FilterChain<T> newCopy() {
308        FilterChain<T> chain = new FilterChain<T>(this.getBucket(), originalObjects, stash);
309        chain.operations = (ArrayList<FilterOperation>)operations.clone();
310        chain.setIncludeDeleted(getIncludeDeleted());
311        return chain;
312    }
313
314    /**
315     * Count the objects matching this filter, grouped by fieldNames.
316     *
317     * @param fieldNames
318     * @return
319     */
320    public List<FilterGroup<T>> countBy(String...fieldNames) {
321        Object cached = getCached("countBy");
322        if (cached != null) {
323            return (List<FilterGroup<T>>)cached;
324        }
325        if (objects == null) {
326            process();
327        }
328
329        HashMap<String, FilterGroup<T>> groupByGroupKey = new HashMap<>();
330        boolean[] hasDot = new boolean[fieldNames.length];
331        for (int i=0; i<fieldNames.length;i++) {
332            hasDot[i] = fieldNames[i].contains(".");
333        }
334        List<String> groupKeys = new ArrayList<>();
335        for(T o: objects) {
336            String groupKey = "";
337            for (int i=0; i<fieldNames.length; i++) {
338                String fieldName = fieldNames[i];
339                if (hasDot[i]) {
340
341                } else {
342                    groupKey += PropertyUtils.getProperty(o, fieldName).toString() + Literals.GSEP;
343                }
344            }
345            if (!groupByGroupKey.containsKey(groupKey)) {
346                groupByGroupKey.put(groupKey, new FilterGroup<T>(groupKey));
347                groupKeys.add(groupKey);
348                groupByGroupKey.get(groupKey).getItems().add(o);
349            }
350            groupByGroupKey.get(groupKey).incrCount();
351        }
352        List<FilterGroup<T>> groups = new ArrayList<>();
353        for (String groupKey: groupKeys) {
354            groups.add(groupByGroupKey.get(groupKey));
355        }
356        setCached("countBy", groups);
357        return groups;
358    }
359
360    /**
361     * Group the matching objects by field names. So if I wanted a group of blog posts
362     * written by some author, grouped by year/month I would do:
363     *
364     * BlogPostController.instance()
365     *     .filter("author", "Mark Twain")
366     *     .groupBy("publishDate.year", "publishDate.month");
367     *
368     *
369     *
370     * @param fieldNames
371     * @return
372     */
373    public List<FilterGroup<T>> groupBy(String...fieldNames) {
374        Object cached = getCached("groupBy");
375        if (cached != null) {
376            return (List<FilterGroup<T>>)cached;
377        }
378        if (objects == null) {
379            process();
380        }
381        List<FilterGroup<T>> groups = groupBy(objects, fieldNames);
382        setCached("groupBy", groups);
383        return groups;
384    }
385
386    /**
387     * Do a groupBy of the passed in objects.
388     *
389     * @param items
390     * @param fieldNames
391     * @param <Y>
392     * @return
393     */
394    private <Y> List<FilterGroup<Y>> groupBy(List<Y> items, String...fieldNames) {
395
396        List<List<String>> subGroupFields = new ArrayList<>();
397        List<String> prior = new ArrayList<>();
398        for (String fieldName: fieldNames) {
399            prior.add(fieldName);
400            subGroupFields.add(prior);
401        }
402
403
404
405
406        HashMap<String, FilterGroup<Y>> groupByGroupKey = new HashMap<>();
407        boolean[] hasDot = new boolean[fieldNames.length];
408        for (int i=0; i<fieldNames.length;i++) {
409            hasDot[i] = fieldNames[i].contains(".");
410        }
411        List<String> groupKeys = new ArrayList<>();
412
413
414        for(Y o: items) {
415            String groupKey = "";
416            for (int i=0; i<fieldNames.length; i++) {
417                String fieldName = fieldNames[i];
418                if (hasDot[i]) {
419                    groupKey += PropertyUtils.getDotProperty(o, fieldName).toString() + Literals.GSEP;
420                } else {
421                    groupKey += PropertyUtils.getProperty(o, fieldName).toString() + Literals.GSEP;
422                }
423            }
424            if (!groupByGroupKey.containsKey(groupKey)) {
425                FilterGroup newGroup = new FilterGroup<Y>(groupKey);
426                groupByGroupKey.put(groupKey, newGroup);
427                groupKeys.add(groupKey);
428            }
429            groupByGroupKey.get(groupKey).getItems().add(o);
430        }
431        List<FilterGroup<Y>> groups = new ArrayList<>();
432        for (String groupKey: groupKeys) {
433            groups.add(groupByGroupKey.get(groupKey));
434        }
435
436        /*
437        Group previousGroup = null;
438        for (int i=0; i<groups.size(); i++) {
439            Group group = groups.get(i);
440            if (previousGroup == null) {
441                for(List<String> subGroup: subGroupFields) {
442                    group.getFirstOfs().add(subGroup);
443                }
444            }
445            if (i+1 == groups.size()) {
446                for(List<String> subGroup: subGroupFields) {
447                    group.getLastOfs().add(subGroup);
448                }
449            }
450            if (previousGroup == null) {
451                previousGroup = group;
452                continue;
453            }
454            List<String> previousVals = Arrays.asList(StringUtils.split(previousGroup.getKey(), Literals.GSEP));
455            List<String> curVals = Arrays.asList(StringUtils.split(group.getKey(), Literals.GSEP));
456            for(int k=0;k< subGroupFields.size(); k++) {
457                List<String> subGroup = subGroupFields.get(k);
458                if (!curVals.subList(0, k+1).equals(previousVals.subList(0, k+1))) {
459                    previousGroup.getLastOfs().add(subGroup);
460                    group.getFirstOfs().add(subGroup);
461                }
462            }
463            previousGroup = group;
464        }
465            */
466        return groups;
467    }
468
469
470    /**
471     * Executes the filters and returns all matching items.
472     *
473     * @return
474     */
475    public List<T> all()  {
476        List<T> cached = (List<T>)getCached("all");
477        if (cached != null) {
478            return cached;
479        }
480        if (objects == null) {
481            process();
482        }
483        setCached("all", objects);
484        return objects;
485    }
486
487    /**
488     * Executes the filters and returns true if there are no matching items found.
489     *
490     * @return
491     */
492    public boolean empty()  {
493        Boolean cached = (Boolean)getCached("empty");
494        if (cached != null) {
495            return cached;
496        }
497        if (objects == null) {
498            process();
499        }
500        boolean result = objects.size() == 0;
501        setCached("empty", result);
502        return result;
503    }
504
505    /**
506     * Alias for empty(), no matching items.
507     * @return
508     */
509    public boolean isEmpty()  {
510        return empty();
511    }
512
513    /**
514     * Executes the filters, and returns a list of all values of the given property name
515     * for all matching objects.
516     *
517     * @param name
518     * @param <Y>
519     * @return
520     */
521    public <Y> List<Y> column(String name) {
522        List<Y> items = list();
523        for(T item: all()) {
524            items.add((Y)PropertyUtils.getPropertyOrMappedValue(item, name));
525        }
526        return items;
527    }
528
529    /**
530     * Executes the filters and returns the total matching count.
531     *
532     * @return
533     */
534    public int count()  {
535        Integer cached = (Integer)getCached("count");
536        if (cached != null) {
537            return cached;
538        }
539
540        if (objects == null) {
541            process();
542        }
543        int theCount = objects.size();
544        setCached("count", theCount);
545        return theCount;
546    }
547
548    /**
549     * Executes the filters and returns the first matching item.
550     *
551     * @return
552     */
553    public T first()  {
554        T cached = (T)getCached("first");
555        if (cached != null) {
556            if (getStash() instanceof LocalMemoryStash) {
557                cached = getStash().getController().detach(cached);
558            }
559            return cached;
560        }
561        process(1, 1, false);
562        List<T> things = objects;
563        if (things.size() > 0) {
564            T thing = things.get(0);
565            setCached("first", thing);
566            if (getStash() instanceof LocalMemoryStash) {
567                thing = getStash().detach(thing);
568            }
569            return thing;
570        } else {
571            return null;
572        }
573
574    }
575
576
577    /**
578     * Adds a sort direction the filter chain response.
579     *
580     * @param fieldName
581     * @param direction - either asc or desc
582     * @return
583     */
584    public FilterChain<T> sort(String fieldName, String direction)  {
585        return sortBy(fieldName, SortDirection.fromString(direction));
586    }
587
588    /**
589     * Adds a sort direction the filter chain response.
590     *
591     * @param fieldName
592     * @param direction
593     * @return
594     */
595    public FilterChain<T> sortBy(String fieldName, SortDirection direction)  {
596        FilterChain<T> chain = newCopy();
597        chain.setSortField(fieldName);
598        chain.setSortDirection(direction);
599        return chain;
600    }
601
602    /**
603     * Alias for pager(page, 10) (default page size of 10);
604     * @param page
605     * @return
606     */
607    public Pager pager(Integer page)  {
608        return this.pager(page, 10);
609    }
610
611    /**
612     * Executes the filters and returns a pager object. A page object
613     * has a subset of the items based on the desired page and the page size
614     *
615     * @param page - which page to return
616     * @param size - how many items are on a page
617     * @return
618     */
619    public Pager pager(Integer page, Integer size)  {
620        String methodKey = "pager"  + Literals.GSEP + page + Literals.GSEP + size;
621        Object cached = getCached(methodKey);
622        if (cached != null) {
623            return (Pager)cached;
624        }
625        if (page == null || page < 1) {
626            page = 1;
627        }
628        if (size == null) {
629            size = 10;
630        }
631
632        if (objects == null) {
633            process(page, size, true);
634        }
635
636
637        Pager pager = new Pager();
638        pager.setCurrentItems(objects);
639        if (objects.size() == 0) {
640            return pager;
641        }
642        pager.setCurrentPage(page);
643        pager.setItemsPerPage(size);
644        pager.setHasPreviousPage(true);
645        pager.setHasNextPage(true);
646        pager.setPreviousPageNumber(page - 1);
647        pager.setNextPageNumber(pager.getCurrentPage() + 1);
648
649        Integer startingIndex = (pager.getCurrentPage() - 1) * pager.getItemsPerPage();
650        Integer endingIndex = startingIndex + pager.getItemsPerPage();
651        if (endingIndex > getMatchingCount()) {
652            endingIndex = getMatchingCount();
653        }
654        if (endingIndex >= getMatchingCount()) {
655            pager.setHasNextPage(false);
656            pager.setNextPageNumber(pager.getCurrentPage());
657        }
658        if (pager.getCurrentPage() <= 1) {
659            pager.setHasPreviousPage(false);
660            pager.setPreviousPageNumber(pager.getCurrentPage());
661        }
662        if (size > 0) {
663            pager.setPageCount((getMatchingCount() / size));
664            if (getMatchingCount() % size > 0) {
665                pager.setPageCount(pager.getPageCount() + 1);
666            }
667        }
668        setCached(methodKey, pager);
669        return pager;
670    }
671
672
673    /**
674     * Alias for process(0, 100000, false)
675     */
676    protected void process() {
677        process(0, 100000, false);
678    }
679
680    /**
681     * Actually applies all filters and sorts to reduce the originalObjects into
682     * a subset of matched objects.
683     *
684     * @param page - the page to start from when returning results
685     * @param size - the number of results per page (the number of results to return)
686     * @param fetchTotalMatching - hydrate the total matching count field, even if
687     *                             we are returning a subet
688     */
689    protected void process(int page, int size, boolean fetchTotalMatching)  {
690        List<T> availableItems = originalObjects;
691
692        boolean hydrated = tryHydrateObjectsBasedOnUniqueKey();
693        if (hydrated) {
694            return;
695        }
696
697        // If we are filtering on a key, then we can reduce to a subset the number of items we have to look at
698        for (FilterOperation op : operations) {
699            if (op.isOrOperation()) {
700                continue;
701            }
702            if (!op.getIsExclude() && op.getOperator().equals(FilterOperator.EQUAL) && stash.getUniqueFields().contains(op.getFieldName())) {
703                T availableItem = stash.forUniqueKey(op.getFieldName(), op.getOriginalValue());
704                if (availableItem != null) {
705                    availableItems = list(availableItem);
706                } else {
707                    availableItems = list();
708                }
709                break;
710            }
711            if (!op.getIsExclude() && op.getOperator().equals(FilterOperator.EQUAL) && stash.getKeyFields().contains(op.getFieldName())) {
712                availableItems = stash.listForKey(op.getFieldName(), op.getOriginalValue());
713                if (availableItems == null) {
714                    availableItems = list();
715                }
716                break;
717            }
718        }
719
720        // Filter down availableItems into items, based on applying the filter operations to each item
721        List<T> items = new ArrayList<T>();
722        for(T o: availableItems) {
723            Boolean exclude = false;
724            if (getIncludeDeleted() != true && o.getDeleted() == true) {
725                continue;
726            }
727            for (FilterOperation operation: getOperations()) {
728                boolean matches = checkItemMatchesFilterAndExcludes(operation, o);
729                if (!matches) {
730                    exclude = true;
731                    break;
732                }
733            }
734            if (!exclude) {
735                items.add(o);
736            }
737        }
738
739        // Apply the sort
740        if (!Literals.empty(getSortField())) {
741            //BeanComparator beanComparator = new BeanComparator(getSortField());
742            PropertyComparator comparator = new PropertyComparator(getSortField());
743            Collections.sort(items, comparator);
744            if (getSortDirection().equals(SortDirection.DESC)) {
745                Collections.reverse(items);
746            }
747        }
748
749        // Set the internal objects fields to the filtered and sorted items;
750        objects = items;
751        // Get the total count that matched the filtering
752        matchingCount = objects.size();
753
754        // If we are implementing paging, and want to limit the objects to a page,
755        // we do so here.
756        if (size > 0) {
757            if (page < 1) {
758                page = 1;
759            }
760            int start = (page - 1) * size;
761            int end = start + size;
762            if (end > objects.size()) {
763                end = objects.size();
764            }
765            if (start >= objects.size()) {
766                objects = new ArrayList<>();
767            } else if (end > start) {
768                objects = objects.subList(start, end);
769            }
770        }
771    }
772
773    private boolean tryHydrateObjectsBasedOnUniqueKey() {
774        boolean matchedUnique = true;
775
776        if (operations.size() > 1) {
777            return false;
778        }
779
780        // If we are filtering on the id, or on a unique key, then we can short circuit the process
781        for (FilterOperation op: operations) {
782            if (!"=".equals(op.getOperator()) || op.getIsExclude()) {
783                continue;
784            }
785            if (!op.getFieldName().equals("id") && !stash.getUniqueFields().contains(op.getFieldName())) {
786                continue;
787            }
788            T o = null;
789            if (op.getFieldName().equals("id")) {
790                o = stash.forId((Long)op.getOriginalValue());
791            } else {
792                o = stash.forUniqueKey(op.getFieldName(), op.getOriginalValue());
793            }
794            if (o == null) {
795                objects = list();
796                return true;
797            }
798            if (!getIncludeDeleted() && o.getDeleted()) {
799                objects = list();
800                return true;
801            }
802            objects = list(o);
803            matchingCount = 1;
804            return true;
805        }
806        return false;
807    }
808
809
810    /**
811     * Checks whether the object matches the complete FilterOperation, taking into account
812     * OR subOperations and the isExcludes flag.
813     *
814     * @param op
815     * @param o
816     * @return
817     */
818    private boolean checkItemMatchesFilterAndExcludes(FilterOperation op, T o) {
819        // Handle an OR operation by getting a list of or requirements and recursing down
820        boolean matches = false;
821        if (op.isOrOperation()) {
822            for(FilterOperation subOp: op.getOrSubOperations()) {
823                boolean thisMatches = checkItemMatchesFilterConditions(subOp, o);
824                if (thisMatches){
825                    matches = true;
826                    break;
827                }
828            }
829        } else {
830            matches = checkItemMatchesFilterConditions(op, o);
831        }
832        if (op.getIsExclude()) {
833            return !matches;
834        } else {
835            return matches;
836        }
837    }
838
839    /**
840     * Check whether the operation conditions match the object, without taking into account
841     * isExcludes or "OR" subOperations.
842     *
843     * @param op
844     * @param o
845     * @return
846     */
847    private boolean checkItemMatchesFilterConditions(FilterOperation op, T o) {
848
849        Object propValue = null;
850        if (op.hasDot()) {
851            propValue = PropertyUtils.getDotProperty(o, op.getFieldName());
852        } else {
853            propValue = PropertyUtils.getPropertyOrMappedValue(o, op.getFieldName());
854        }
855
856        // Filter out nulls
857        if (propValue == null && op.getOriginalValue() != null) {
858            return false;
859        }
860        if (propValue == null && op.getOriginalValue() == null && op.getOperator().equals(FilterOperator.EQUAL)) {
861            return true;
862        }
863
864        // Handle the IN operator
865        if (op.getOperator().equals(FilterOperator.IN)) {
866            Boolean isIn = false;
867            Iterable values = (Iterable)propValue;
868            for(Object val: values) {
869                if (val.equals(op.getOriginalValue())) {
870                    isIn = true;
871                    break;
872                }
873            }
874            return isIn;
875        }
876        if (op.getOperator().equals(FilterOperator.ANY)) {
877            Iterable vals = (Iterable)op.getOriginalValue();
878            boolean matches = false;
879            for (Object val: vals) {
880                if (val.equals(propValue)) {
881                    matches = true;
882                    break;
883                }
884            }
885            return matches;
886        }
887
888        // Apply a bunch of heuristics to make sure we are comparing like types,
889        // we don't want to filter something out because we are comparing a Long to an Integer
890        hydrateTypedValue(op, propValue);
891
892        // Filter out nulls again, based on type conversion
893        if (propValue == null && op.getTypedValue() != null) {
894            Log.info("Null value: id:{0} field:{1} objVal:''{2}'' ", o.getId(), op.getFieldName(), propValue);
895            return false;
896        }
897        Log.finest("Compare id:{0} field:{1} objVal:''{2}'' objValType:{3} filterTypedVal:''{4}'' filterValType: {5}", o.getId(), op.getFieldName(), propValue, propValue.getClass().getCanonicalName(), op.getTypedValue(), op.getTypedValue().getClass().getName());
898
899        // When comparing booleans the string "true" should be considered true, and "false" false, this
900        // is important when applying a filter coming from an untyped query string.
901        if (op.getTypedValue() instanceof Boolean && propValue instanceof String) {
902            return op.getTypedValue().toString().toLowerCase().equals((String) propValue.toString().toLowerCase());
903        }
904
905        if (op.getOperator().equals(FilterOperator.EQUAL)) {
906            return op.getTypedValue().equals(propValue);
907        }
908        if (op.getOperator().equals(FilterOperator.NOT_EQUAL)) {
909            return !op.getTypedValue().equals(propValue);
910        }
911
912        if (op.getOperator().equals(FilterOperator.LIKE)) {
913            return StringUtils.containsIgnoreCase(propValue.toString(), op.getTypedValue().toString());
914        }
915
916        int i = op.getComparableValue().compareTo(propValue);
917        if (op.getOperator().equals(FilterOperator.GREATER_THAN)) {
918            return i < 0;
919        }
920        if (op.getOperator().equals(FilterOperator.LESS_THAN)) {
921            return i > 0;
922        }
923        if (op.getOperator().equals(FilterOperator.GREATER_THAN_OR_EQUAL)) {
924            return i <= 0;
925        }
926        if (op.getOperator().equals(FilterOperator.LESS_THAN_OR_EQUAL)) {
927            return i >= 0;
928        }
929        throw new UsageException("You used an uninplemented filter operation: " + op);
930    }
931
932
933
934
935    /**
936     * Hydrate the FilterOperation.typedValue based on gueessing or reflecting
937     * on the type of the property as passed in.
938     *
939     * @param op
940     * @param propValue
941     */
942    private void hydrateTypedValue(FilterOperation op, Object propValue) {
943        if (op.getTypedValue() != null) {
944            return;
945        }
946        if (op.getOriginalValue() == null) {
947            return;
948        }
949        if (op.getOriginalValue().getClass().equals(propValue.getClass())) {
950            op.setTypedValue(op.getOriginalValue());
951            return;
952        }
953        if (op.getOriginalValue().getClass().equals(String.class)) {
954            String val = (String) op.getOriginalValue();
955            if (propValue != null) {
956                if (propValue.getClass().equals(Integer.class)) {
957                    op.setTypedValue(Integer.parseInt(val));
958                } else if (propValue.getClass().equals(Long.class)) {
959                    op.setTypedValue(Long.parseLong(val));
960                } else if (propValue.getClass().equals(Boolean.class)) {
961                    op.setTypedValue(Boolean.parseBoolean(val));
962
963                } else {
964                    op.setTypedValue(op.getOriginalValue());
965                }
966            } else {
967                op.setTypedValue(op.getOriginalValue());
968            }
969        } else if (op.getOriginalValue() instanceof BigInteger) {
970            op.setTypedValue(((BigInteger) op.getOriginalValue()).longValue());
971
972        } else if (op.getOriginalValue() instanceof Integer && propValue instanceof Long) {
973            op.setTypedValue(new Long((Integer) op.getOriginalValue()));
974        } else if (op.getOriginalValue() instanceof Integer && propValue instanceof Float) {
975            op.setTypedValue(((Integer) op.getOriginalValue()).floatValue());
976        } else if (op.getOriginalValue() instanceof Integer && propValue instanceof Double) {
977            op.setTypedValue(((Integer) op.getOriginalValue()).doubleValue());
978        } else if (op.getOriginalValue() instanceof Long && propValue instanceof Double) {
979            op.setTypedValue(((Long) op.getOriginalValue()).doubleValue());
980        } else if (op.getOriginalValue() instanceof Long && propValue instanceof Float) {
981            op.setTypedValue(((Long) op.getOriginalValue()).floatValue());
982        } else if (propValue instanceof Boolean) {
983            if (op.getOriginalValue() instanceof Integer || op.getOriginalValue() instanceof Long) {
984                if ((Integer)op.getOriginalValue() == 0) {
985                    op.setTypedValue(false);
986                } else if ((Integer)op.getOriginalValue() == 1){
987                    op.setTypedValue(true);
988                }
989            }
990        }
991        if (op.getTypedValue() == null) {
992            op.setTypedValue(op.getOriginalValue());
993        }
994    }
995
996    protected Object getCached(String methodName) {
997        String key = buildKey(methodName);
998        if (checkSkipCache(key)) {
999            return null;
1000        }
1001        Object result = FilterCache.get(this.getBucket(), key);
1002        return result;
1003    }
1004
1005    protected boolean checkSkipCache(String key) {
1006        return false;
1007    }
1008
1009    protected void setCached(String methodName, Object val) {
1010        String key = buildKey(methodName);
1011        FilterCache.set(this.getBucket(), key, val);
1012    }
1013
1014    private String buildKey(String methodName) {
1015        StringBuilder builder = new StringBuilder();
1016        builder.append(methodName + Literals.GSEP);
1017        if (this.originalObjects.size() > 0) {
1018            builder.append(this.originalObjects.get(0).getClass().getCanonicalName());
1019        }
1020        for (FilterOperation op: operations) {
1021            //Log.finest("fn: {0} op: {1} isExclude: {2} orgVal: {3}", op.getFieldName(), op.getOperator(), op.getIsExclude(), op.getOriginalValue());
1022            String ov = "<null>";
1023            if (op.getOriginalValue() != null) {
1024                ov = op.getOriginalValue().toString();
1025            }
1026            builder.append(op.getFieldName() + op.getOperator() + op.getIsExclude() + Literals.GSEP + ov + Literals.GSEP);
1027            if (op.isOrOperation()) {
1028                for (FilterOperation subOp: op.getOrSubOperations()) {
1029                    ov = "<null>";
1030                    if (subOp.getOriginalValue() != null) {
1031                        ov = subOp.getOriginalValue().toString();
1032                    }
1033                    builder.append(subOp.getFieldName() + subOp.getOperator() + subOp.getIsExclude() + Literals.GSEP + ov + Literals.GSEP);
1034                }
1035            }
1036        }
1037        builder.append(getIncludeDeleted());
1038        builder.append(getSortField());
1039        builder.append(getSortDirection());
1040        builder.append(Literals.GSEP + getExtraCacheKey());
1041        String fullKey = builder.toString();
1042        return DigestUtils.md5Hex(fullKey);
1043    }
1044
1045    @Override
1046    public Iterator<T> iterator() {
1047        if (objects == null) {
1048            process();
1049        }
1050        return objects.iterator();
1051    }
1052
1053    @Override
1054    public void forEach(Consumer<? super T> action) {
1055        if (objects == null) {
1056            try {
1057                process();
1058            } catch (Exception e) {
1059                throw new RuntimeException(e);
1060            }
1061        }
1062        objects.forEach(action);
1063    }
1064
1065    @Override
1066    public Spliterator<T> spliterator() {
1067        if (objects == null) {
1068            try {
1069                process();
1070            } catch (Exception e) {
1071                throw new RuntimeException(e);
1072            }
1073        }
1074        return objects.spliterator();
1075    }
1076
1077
1078
1079    public String getSortField() {
1080        return sortField;
1081    }
1082
1083    protected void setSortField(String sortField) {
1084        this.sortField = sortField;
1085    }
1086
1087    public SortDirection getSortDirection() {
1088        return sortDirection;
1089    }
1090
1091    protected void setSortDirection(SortDirection sortDirection) {
1092        this.sortDirection = sortDirection;
1093    }
1094
1095    public String getBucket() {
1096        return bucket;
1097    }
1098
1099    protected void setBucket(String bucket) {
1100        this.bucket = bucket;
1101    }
1102
1103    public String getExtraCacheKey() {
1104        return extraCacheKey;
1105    }
1106
1107    protected void setExtraCacheKey(String extraCacheKey) {
1108        this.extraCacheKey = extraCacheKey;
1109    }
1110
1111    public Boolean getIncludeDeleted() {
1112        return _includeDeleted;
1113    }
1114
1115    protected FilterChain<T> setIncludeDeleted(Boolean includeDeleted) {
1116        this._includeDeleted = includeDeleted;
1117        return this;
1118    }
1119
1120    public FilterChain<T> includeDeleted() {
1121        _includeDeleted = true;
1122        return this;
1123    }
1124
1125    public ArrayList<FilterOperation> getOperations() {
1126        return operations;
1127    }
1128
1129    protected Integer getMatchingCount() {
1130        return matchingCount;
1131    }
1132
1133    protected void setMatchingCount(Integer matchingCount) {
1134        this.matchingCount = matchingCount;
1135    }
1136
1137    protected List<T> getObjects() {
1138        return objects;
1139    }
1140
1141    protected void setObjects(List<T> objects) {
1142        this.objects = objects;
1143    }
1144
1145    LocalMemoryStash<T> getStash() {
1146        return stash;
1147    }
1148
1149    FilterChain setStash(LocalMemoryStash<T> stash) {
1150        this.stash = stash;
1151        return this;
1152    }
1153}