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<Books> 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}