001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.utils; 014 015import static java.util.Calendar.*; 016import static org.apache.juneau.internal.StringUtils.*; 017 018import java.lang.reflect.*; 019import java.text.*; 020import java.util.*; 021import java.util.regex.*; 022 023import org.apache.juneau.*; 024import org.apache.juneau.internal.*; 025 026/** 027 * Designed to provide search/view/sort/paging filtering on tabular in-memory POJO models. 028 * 029 * <p> 030 * It can also perform just view filtering on beans/maps. 031 * 032 * <p> 033 * Examples of tabular POJO models: 034 * <ul> 035 * <li><tt>Collection{@code <Map>}</tt> 036 * <li><tt>Collection{@code <Bean>}</tt> 037 * <li><tt>Map[]</tt> 038 * <li><tt>Bean[]</tt> 039 * </ul> 040 * 041 * <p> 042 * Tabular POJO models can be thought of as tables of data. For example, a list of the following beans... 043 * <p class='bcode'> 044 * <jk>public</jk> MyBean { 045 * <jk>public int</jk> myInt; 046 * <jk>public</jk> String myString; 047 * <jk>public</jk> Date myDate; 048 * } 049 * <p> 050 * ... can be thought of a table containing the following columns... 051 * <p> 052 * <table class='styled code'> 053 * <tr><th>myInt</th><th>myString</th><th>myDate</th></tr> 054 * <tr><td>123</td><td>'foobar'</td><td>yyyy/MM/dd HH:mm:ss</td></tr> 055 * <tr><td colspan=3>...</td></tr> 056 * </table> 057 * 058 * <p> 059 * From this table, you can perform the following functions: 060 * <ul class='spaced-list'> 061 * <li> 062 * Search - Return only rows where a search pattern matches. 063 * <li> 064 * View - Return only the specified subset of columns in the specified order. 065 * <li> 066 * Sort - Sort the table by one or more columns. 067 * <li> 068 * Position/limit - Only return a subset of rows. 069 * </ul> 070 * 071 * <h5 class='topic'>Search</h5> 072 * 073 * The search capabilities allow you to filter based on query patterns against strings, dates, and numbers. 074 * Queries take the form of a Map with column names as keys, and search patterns as values. 075 * <br>Multiple search patterns are ANDed (i.e. all patterns must match for the row to be returned). 076 * 077 * <h5 class='section'>Example:</h5> 078 * <ul class='spaced-list'> 079 * <li> 080 * <tt>{myInt:'123'}</tt> - Return only rows where the <tt>myInt</tt> column is 123. 081 * <li> 082 * <tt>{myString:'foobar'}</tt> - Return only rows where the <tt>myString</tt> column is 'foobar'. 083 * <li> 084 * <tt>{myDate:'2001'}</tt> - Return only rows where the <tt>myDate</tt> column have dates in the year 2001. 085 * </ul> 086 * 087 * <h5 class='topic'>String Patterns</h5> 088 * 089 * Any objects can be queried against using string patterns. 090 * If the objects being searched are not strings, then the patterns are matched against whatever is return by the 091 * {@code Object#toString()} method. 092 * 093 * <h5 class='topic'>Example string query patterns:</h5> 094 * <ul> 095 * <li><tt>foo</tt> - The string 'foo' 096 * <li><tt>foo bar</tt> - The string 'foo' or the string 'bar' 097 * <li><tt>'foo bar'</tt> - The phrase 'foo bar' 098 * <li><tt>"foo bar"</tt> - The phrase 'foo bar' 099 * <li><tt>foo*</tt> - <tt>*</tt> matches zero-or-more characters. 100 * <li><tt>foo?</tt> - <tt>?</tt> matches exactly one character 101 * </ul> 102 * 103 * <h5 class='section'>Notes:</h5> 104 * <ul class='spaced-list'> 105 * <li> 106 * Whitespace is ignored around search patterns. 107 * <li> 108 * Prepend <tt>+</tt> to tokens that must match. (e.g. <tt>+foo* +*bar</tt>) 109 * <li> 110 * Prepend <tt>-</tt> to tokens that must not match. (e.g. <tt>+foo* -*bar</tt>) 111 * </ul> 112 * 113 * <h5 class='topic'>Numeric Patterns</h5> 114 * 115 * Any object of type {@link Number} (or numeric primitives) can be searched using numeric patterns. 116 * 117 * <h5 class='topic'>Example numeric query patterns:</h5> 118 * <ul> 119 * <li><tt>123</tt> - The single number 123 120 * <li><tt>1 2 3</tt> - 1, 2, or 3 121 * <li><tt>1-100</tt> - Between 1 and 100 122 * <li><tt>1 - 100</tt> - Between 1 and 100 123 * <li><tt>1 - 100 200-300</tt> - Between 1 and 100 or between 200 and 300 124 * <li><tt>> 100</tt> - Greater than 100 125 * <li><tt>>= 100</tt> - Greater than or equal to 100 126 * <li><tt>!123</tt> - Not 123 127 * </ul> 128 * 129 * <h5 class='section'>Notes:</h5> 130 * <ul class='spaced-list'> 131 * <li> 132 * Whitespace is ignored in search patterns. 133 * <li> 134 * Negative numbers are supported. 135 * </ul> 136 * 137 * <h5 class='topic'>Date Patterns</h5> 138 * 139 * Any object of type {@link Date} or {@link Calendar} can be searched using date patterns. 140 * 141 * <p> 142 * The default valid input timestamp formats (which can be overridden via the {@link #setValidTimestampFormats(String...)} 143 * method are... 144 * 145 * <ul> 146 * <li><tt>yyyy.MM.dd.HH.mm.ss</tt> 147 * <li><tt>yyyy.MM.dd.HH.mm</tt> 148 * <li><tt>yyyy.MM.dd.HH</tt> 149 * <li><tt>yyyy.MM.dd</tt> 150 * <li><tt>yyyy.MM</tt> 151 * <li><tt>yyyy</tt> 152 * </ul> 153 * 154 * <h5 class='topic'>Example date query patterns:</h5> 155 * <ul> 156 * <li><tt>2001</tt> - A specific year. 157 * <li><tt>2001.01.01.10.50</tt> - A specific time. 158 * <li><tt>>2001</tt> - After a specific year. 159 * <li><tt>>=2001</tt> - During or after a specific year. 160 * <li><tt>2001 - 2003.06.30</tt> - A date range. 161 * <li><tt>2001 2003 2005</tt> - Multiple date patterns are ORed. 162 * </ul> 163 * 164 * <h5 class='section'>Notes:</h5> 165 * <ul class='spaced-list'> 166 * <li> 167 * Whitespace is ignored in search patterns. 168 * </ul> 169 * 170 * <h5 class='topic'>View</h5> 171 * 172 * The view capability allows you to return only the specified subset of columns in the specified order. 173 * <br>The view parameter is a list of either <tt>Strings</tt> or <tt>Maps</tt>. 174 * 175 * <h5 class='topic'>Example view parameters:</h5> 176 * <ul> 177 * <li><tt>column1</tt> - Return only column 'column1'. 178 * <li><tt>column2, column1</tt> - Return only columns 'column2' and 'column1' in that order. 179 * </ul> 180 * 181 * <h5 class='topic'>Sort</h5> 182 * 183 * The sort capability allows you to sort values by the specified rows. 184 * <br>The sort parameter is a list of strings with an optional <js>'+'</js> or <js>'-'</js> suffix representing 185 * ascending and descending order accordingly. 186 * 187 * <h5 class='topic'>Example sort parameters:</h5> 188 * <ul> 189 * <li><tt>column1</tt> - Sort rows by column 'column1' ascending. 190 * <li><tt>column1+</tt> - Sort rows by column 'column1' ascending. 191 * <li><tt>column1-</tt> - Sort rows by column 'column1' descending. 192 * <li><tt>column1, column2-</tt> - Sort rows by column 'column1' ascending, then 'column2' descending. 193 * </ul> 194 * 195 * <h5 class='topic'>Paging</h5> 196 * 197 * Use the <tt>position</tt> and <tt>limit</tt> parameters to specify a subset of rows to return. 198 */ 199@SuppressWarnings({"unchecked","rawtypes"}) 200public final class PojoQuery { 201 202 private Object input; 203 private ClassMeta type; 204 private BeanSession session; 205 206 /** 207 * Constructor. 208 * 209 * @param input The POJO we're going to be filtering. 210 * @param session The bean session to use to create bean maps for beans. 211 */ 212 public PojoQuery(Object input, BeanSession session) { 213 this.input = input; 214 this.type = session.getClassMetaForObject(input); 215 this.session = session; 216 } 217 218 /** 219 * Filters the input object as a collection of maps. 220 * 221 * @param args The search arguments. 222 * @return The filtered collection. 223 * Returns the unaltered input if the input is not a collection or array of objects. 224 */ 225 public List filter(SearchArgs args) { 226 227 if (input == null) 228 return null; 229 230 if (! type.isCollectionOrArray()) 231 throw new FormattedRuntimeException("Cannot call filterCollection() on class type ''{0}''", type); 232 233 // Create a new ObjectList 234 ObjectList l = (ObjectList)replaceWithMutables(input); 235 236 // Do the search 237 CollectionFilter filter = new CollectionFilter(args.getSearch(), args.isIgnoreCase()); 238 filter.doQuery(l); 239 240 // If sort or view isn't empty, then we need to make sure that all entries in the 241 // list are maps. 242 Map<String,Boolean> sort = args.getSort(); 243 List<String> view = args.getView(); 244 245 if ((! sort.isEmpty()) || (! view.isEmpty())) { 246 if (! sort.isEmpty()) 247 doSort(l, sort); 248 if (! view.isEmpty()) 249 doView(l, view); 250 } 251 252 // Do the paging. 253 int pos = args.getPosition(); 254 int limit = args.getLimit(); 255 if (pos != 0 || limit != 0) { 256 int end = (limit == 0 || limit+pos >= l.size()) ? l.size() : limit + pos; 257 pos = Math.min(pos, l.size()); 258 ObjectList l2 = new DelegateList(((DelegateList)l).getClassMeta()); 259 l2.addAll(l.subList(pos, end)); 260 l = l2; 261 } 262 263 return l; 264 } 265 266 /* 267 * If there are any non-Maps in the specified list, replaces them with BeanMaps. 268 */ 269 private Object replaceWithMutables(Object o) { 270 if (o == null) 271 return null; 272 ClassMeta cm = session.getClassMetaForObject(o); 273 if (cm.isCollection()) { 274 ObjectList l = new DelegateList(session.getClassMetaForObject(o)); 275 for (Object o2 : (Collection)o) 276 l.add(replaceWithMutables(o2)); 277 return l; 278 } 279 if (cm.isMap() && o instanceof BeanMap) { 280 BeanMap bm = (BeanMap)o; 281 DelegateBeanMap dbm = new DelegateBeanMap(bm.getBean(), session); 282 for (Object key : bm.keySet()) 283 dbm.addKey(key.toString()); 284 return dbm; 285 } 286 if (cm.isBean()) { 287 BeanMap bm = session.toBeanMap(o); 288 DelegateBeanMap dbm = new DelegateBeanMap(bm.getBean(), session); 289 for (Object key : bm.keySet()) 290 dbm.addKey(key.toString()); 291 return dbm; 292 } 293 if (cm.isMap()) { 294 Map m = (Map)o; 295 DelegateMap dm = new DelegateMap(session.getClassMetaForObject(m)); 296 for (Map.Entry e : (Set<Map.Entry>)m.entrySet()) 297 dm.put(e.getKey().toString(), e.getValue()); 298 return dm; 299 } 300 if (cm.isArray()) { 301 return replaceWithMutables(Arrays.asList((Object[])o)); 302 } 303 return o; 304 } 305 306 /* 307 * Sorts the specified list by the sort list. 308 */ 309 private static void doSort(List list, Map<String,Boolean> sortList) { 310 311 // We reverse the list and sort last to first. 312 List<String> columns = new ArrayList<>(sortList.keySet()); 313 Collections.reverse(columns); 314 315 for (final String c : columns) { 316 final boolean isDesc = sortList.get(c); 317 318 Comparator comp = new Comparator<Map>() { 319 @Override /* Comparator */ 320 public int compare(Map m1, Map m2) { 321 Comparable v1 = toComparable(m1.get(c)), v2 = toComparable(m2.get(c)); 322 if (v1 == null && v2 == null) 323 return 0; 324 if (v1 == null) 325 return (isDesc ? -1 : 1); 326 if (v2 == null) 327 return (isDesc ? 1 : -1); 328 return (isDesc ? v2.compareTo(v1) : v1.compareTo(v2)); 329 } 330 }; 331 Collections.sort(list, comp); 332 } 333 } 334 335 static final Comparable toComparable(Object o) { 336 if (o == null) 337 return null; 338 if (o instanceof Comparable) 339 return (Comparable)o; 340 if (o instanceof Map) 341 return ((Map)o).size(); 342 if (o.getClass().isArray()) 343 return Array.getLength(o); 344 return o.toString(); 345 } 346 347 /* 348 * Filters all but the specified view columns on all entries in the specified list. 349 */ 350 private static void doView(List list, List<String> view) { 351 for (ListIterator i = list.listIterator(); i.hasNext();) { 352 Object o = i.next(); 353 Map m = (Map)o; 354 doView(m, view); 355 } 356 } 357 358 /* 359 * Creates a new Map with only the entries specified in the view list. 360 */ 361 private static Map doView(Map m, List<String> view) { 362 if (m instanceof DelegateMap) 363 ((DelegateMap)m).filterKeys(view); 364 else 365 ((DelegateBeanMap)m).filterKeys(view); 366 return m; 367 } 368 369 370 //==================================================================================================== 371 // CollectionFilter 372 //==================================================================================================== 373 private class CollectionFilter { 374 IMatcher entryMatcher; 375 376 public CollectionFilter(Map query, boolean ignoreCase) { 377 if (query != null && ! query.isEmpty()) 378 entryMatcher = new MapMatcher(query, ignoreCase); 379 } 380 381 public void doQuery(List in) { 382 if (in == null || entryMatcher == null) 383 return; 384 for (Iterator i = in.iterator(); i.hasNext();) { 385 Object o = i.next(); 386 if (! entryMatcher.matches(o)) 387 i.remove(); 388 } 389 } 390 } 391 392 //==================================================================================================== 393 // IMatcher 394 //==================================================================================================== 395 private interface IMatcher<E> { 396 public boolean matches(E o); 397 } 398 399 //==================================================================================================== 400 // MapMatcher 401 //==================================================================================================== 402 /* 403 * Matches on a Map only if all specified entry matchers match. 404 */ 405 private class MapMatcher implements IMatcher<Map> { 406 407 Map<String,IMatcher> entryMatchers = new HashMap<>(); 408 409 public MapMatcher(Map query, boolean ignoreCase) { 410 for (Map.Entry e : (Set<Map.Entry>)query.entrySet()) 411 if (e.getKey() != null && e.getValue() != null) 412 entryMatchers.put(e.getKey().toString(), new ObjectMatcher(e.getValue().toString(), ignoreCase)); 413 } 414 415 @Override /* IMatcher */ 416 public boolean matches(Map m) { 417 if (m == null) 418 return false; 419 for (Map.Entry<String,IMatcher> e : entryMatchers.entrySet()) { 420 String key = e.getKey(); 421 Object val = null; 422 if (m instanceof BeanMap) { 423 val = ((BeanMap)m).getRaw(key); 424 } else { 425 val = m.get(key); 426 } 427 if (! e.getValue().matches(val)) 428 return false; 429 } 430 return true; 431 } 432 } 433 434 //==================================================================================================== 435 // ObjectMatcher 436 //==================================================================================================== 437 /* 438 * Matcher that uses the correct matcher based on object type. 439 * Used for objects when we can't determine the object type beforehand. 440 */ 441 private class ObjectMatcher implements IMatcher<Object> { 442 443 String searchPattern; 444 boolean ignoreCase; 445 DateMatcher dateMatcher; 446 NumberMatcher numberMatcher; 447 StringMatcher stringMatcher; 448 449 ObjectMatcher(String searchPattern, boolean ignoreCase) { 450 this.searchPattern = searchPattern; 451 this.ignoreCase = ignoreCase; 452 } 453 454 @Override /* IMatcher */ 455 public boolean matches(Object o) { 456 if (o instanceof Collection) { 457 for (Object o2 : (Collection)o) 458 if (matches(o2)) 459 return true; 460 return false; 461 } 462 if (o != null && o.getClass().isArray()) { 463 for (int i = 0; i < Array.getLength(o); i++) 464 if (matches(Array.get(o, i))) 465 return true; 466 return false; 467 } 468 if (o instanceof Map) { 469 for (Object o2 : ((Map)o).values()) 470 if (matches(o2)) 471 return true; 472 return false; 473 } 474 if (o instanceof Number) 475 return getNumberMatcher().matches(o); 476 if (o instanceof Date || o instanceof Calendar) 477 return getDateMatcher().matches(o); 478 return getStringMatcher().matches(o); 479 } 480 481 private IMatcher getNumberMatcher() { 482 if (numberMatcher == null) 483 numberMatcher = new NumberMatcher(searchPattern); 484 return numberMatcher; 485 } 486 487 private IMatcher getStringMatcher() { 488 if (stringMatcher == null) 489 stringMatcher = new StringMatcher(searchPattern, ignoreCase); 490 return stringMatcher; 491 } 492 493 private IMatcher getDateMatcher() { 494 if (dateMatcher == null) 495 dateMatcher = new DateMatcher(searchPattern); 496 return dateMatcher; 497 } 498 } 499 500 //==================================================================================================== 501 // NumberMatcher 502 //==================================================================================================== 503 private static class NumberMatcher implements IMatcher<Number> { 504 505 private NumberPattern[] numberPatterns; 506 507 /** 508 * Construct a number matcher for the given search pattern. 509 * 510 * @param searchPattern A date search paattern. See class usage for a description. 511 */ 512 public NumberMatcher(String searchPattern) { 513 numberPatterns = new NumberPattern[1]; 514 numberPatterns[0] = new NumberPattern(searchPattern); 515 516 } 517 518 /** 519 * Returns 'true' if this integer matches the pattern(s). 520 */ 521 @Override /* IMatcher */ 522 public boolean matches(Number in) { 523 for (int i = 0; i < numberPatterns.length; i++) { 524 if (! numberPatterns[i].matches(in)) 525 return false; 526 } 527 return true; 528 } 529 530 } 531 532 /** 533 * A construct representing a single search pattern. 534 */ 535 private static class NumberPattern { 536 NumberRange[] numberRanges; 537 538 public NumberPattern(String searchPattern) { 539 540 List<NumberRange> l = new LinkedList<>(); 541 542 for (String s : breakUpTokens(searchPattern)) { 543 boolean isNot = (s.charAt(0) == '!'); 544 String token = s.substring(1); 545 Pattern p = Pattern.compile("(([<>]=?)?)(-?\\d+)(-?(-?\\d+)?)"); 546 547 // Possible patterns: 548 // 123, >123, <123, >=123, <=123, >-123, >=-123, 123-456, -123--456 549 // Regular expression used: (([<>]=?)?)(-?\d+)(-??(-?\d+)) 550 Matcher m = p.matcher(token); 551 552 // If a non-numeric value was passed in for a numeric value, just set the value to '0'. 553 // (I think this might resolve a workaround in custom queries). 554 if (! m.matches()) 555 throw new FormattedRuntimeException("Numeric value didn't match pattern: ''{0}''", token); 556 //m = numericPattern.matcher("0"); 557 558 String arg1 = m.group(1); 559 String start = m.group(3); 560 String end = m.group(5); 561 562 l.add(new NumberRange(arg1, start, end, isNot)); 563 } 564 565 numberRanges = l.toArray(new NumberRange[l.size()]); 566 } 567 568 private static List<String> breakUpTokens(String s) { 569 // Get rid of whitespace in "123 - 456" 570 s = s.replaceAll("(-?\\d+)\\s*-\\s*(-?\\d+)", "$1-$2"); 571 // Get rid of whitespace in ">= 123" 572 s = s.replaceAll("([<>]=?)\\s+(-?\\d+)", "$1$2"); 573 // Get rid of whitespace in "! 123" 574 s = s.replaceAll("(!)\\s+(-?\\d+)", "$1$2"); 575 576 // Replace all commas with whitespace 577 // Allows for alternate notation of: 123,456... 578 s = s.replaceAll(",", " "); 579 580 String[] s2 = s.split("\\s+"); 581 582 // Make all tokens 'ORed'. There is no way to AND numeric tokens. 583 for (int i = 0; i < s2.length; i++) 584 if (! startsWith(s2[i], '!')) 585 s2[i] = "^"+s2[i]; 586 587 List<String> l = new LinkedList<>(); 588 l.addAll(Arrays.asList(s2)); 589 return l; 590 } 591 592 public boolean matches(Number number) { 593 if (numberRanges.length == 0) return true; 594 for (int i = 0; i < numberRanges.length; i++) 595 if (numberRanges[i].matches(number)) 596 return true; 597 return false; 598 } 599 } 600 601 /** 602 * A construct representing a single search range in a single search pattern. 603 * All possible forms of search patterns are boiled down to these number ranges. 604 */ 605 private static class NumberRange { 606 int start; 607 int end; 608 boolean isNot; 609 610 public NumberRange(String arg, String start, String end, boolean isNot) { 611 612 this.isNot = isNot; 613 614 // 123, >123, <123, >=123, <=123, >-123, >=-123, 123-456, -123--456 615 if (arg.equals("") && end == null) { // 123 616 this.start = Integer.parseInt(start); 617 this.end = this.start; 618 } else if (arg.equals(">")) { 619 this.start = Integer.parseInt(start)+1; 620 this.end = Integer.MAX_VALUE; 621 } else if (arg.equals(">=")) { 622 this.start = Integer.parseInt(start); 623 this.end = Integer.MAX_VALUE; 624 } else if (arg.equals("<")) { 625 this.start = Integer.MIN_VALUE; 626 this.end = Integer.parseInt(start)-1; 627 } else if (arg.equals("<=")) { 628 this.start = Integer.MIN_VALUE; 629 this.end = Integer.parseInt(start); 630 } else { 631 this.start = Integer.parseInt(start); 632 this.end = Integer.parseInt(end); 633 } 634 } 635 636 public boolean matches(Number n) { 637 long i = n.longValue(); 638 boolean b = (i>=start && i<=end); 639 if (isNot) b = !b; 640 return b; 641 } 642 } 643 644 //==================================================================================================== 645 // DateMatcher 646 //==================================================================================================== 647 /** The list of all valid timestamp formats */ 648 private SimpleDateFormat[] validTimestampFormats = new SimpleDateFormat[0]; 649 { 650 setValidTimestampFormats("yyyy.MM.dd.HH.mm.ss","yyyy.MM.dd.HH.mm","yyyy.MM.dd.HH","yyyy.MM.dd","yyyy.MM","yyyy"); 651 } 652 653 /** 654 * Use this method to override the allowed search patterns when used in locales where time formats are different. 655 * 656 * @param s A comma-delimited list of valid time formats. 657 */ 658 public void setValidTimestampFormats(String...s) { 659 validTimestampFormats = new SimpleDateFormat[s.length]; 660 for (int i = 0; i < s.length; i++) 661 validTimestampFormats[i] = new SimpleDateFormat(s[i]); 662 } 663 664 private class DateMatcher implements IMatcher<Object> { 665 666 private TimestampPattern[] patterns; 667 668 /** 669 * Construct a timestamp matcher for the given search pattern. 670 * 671 * @param searchPattern The search pattern. 672 */ 673 DateMatcher(String searchPattern) { 674 patterns = new TimestampPattern[1]; 675 patterns[0] = new TimestampPattern(searchPattern); 676 677 } 678 679 /** 680 * Returns <jk>true</jk> if the specified date matches the pattern passed in through the constructor. 681 * 682 * <p> 683 * <br>The Object can be of type {@link Date} or {@link Calendar}. 684 * <br>Always returns <jk>false</jk> on <jk>null</jk> input. 685 */ 686 @Override /* IMatcher */ 687 public boolean matches(Object in) { 688 if (in == null) return false; 689 690 Calendar c = null; 691 if (in instanceof Calendar) 692 c = (Calendar)in; 693 else if (in instanceof Date) { 694 c = Calendar.getInstance(); 695 c.setTime((Date)in); 696 } else { 697 return false; 698 } 699 for (int i = 0; i < patterns.length; i++) { 700 if (! patterns[i].matches(c)) 701 return false; 702 } 703 return true; 704 } 705 } 706 707 /** 708 * A construct representing a single search pattern. 709 */ 710 private class TimestampPattern { 711 TimestampRange[] ranges; 712 List<TimestampRange> l = new LinkedList<>(); 713 714 public TimestampPattern(String s) { 715 716 // Handle special case where timestamp is enclosed in quotes. 717 // This can occur on hyperlinks created by group-by queries. 718 // e.g. '2007/01/29 04:17:43 PM' 719 if (s.charAt(0) == '\'' && s.charAt(s.length()-1) == '\'') 720 s = s.substring(1, s.length()-1); 721 722 // Pattern for finding <,>,<=,>= 723 Pattern p1 = Pattern.compile("^\\s*([<>](?:=)?)\\s*(\\S+.*)$"); 724 // Pattern for finding range dash (e.g. xxx - yyy) 725 Pattern p2 = Pattern.compile("^(\\s*-\\s*)(\\S+.*)$"); 726 727 // States are... 728 // 1 - Looking for <,>,<=,>= 729 // 2 - Looking for single date. 730 // 3 - Looking for start date. 731 // 4 - Looking for - 732 // 5 - Looking for end date. 733 int state = 1; 734 735 String op = null; 736 CalendarP startDate = null; 737 738 ParsePosition pp = new ParsePosition(0); 739 Matcher m = null; 740 String seg = s; 741 742 while (! seg.equals("") || state != 1) { 743 if (state == 1) { 744 m = p1.matcher(seg); 745 if (m.matches()) { 746 op = m.group(1); 747 seg = m.group(2); 748 state = 2; 749 } else { 750 state = 3; 751 } 752 } else if (state == 2) { 753 l.add(new TimestampRange(op, parseDate(seg, pp))); 754 //tokens.add("^"+op + parseTimestamp(seg, pp)); 755 seg = seg.substring(pp.getIndex()).trim(); 756 pp.setIndex(0); 757 state = 1; 758 } else if (state == 3) { 759 startDate = parseDate(seg, pp); 760 seg = seg.substring(pp.getIndex()).trim(); 761 pp.setIndex(0); 762 state = 4; 763 } else if (state == 4) { 764 // Look for '-' 765 m = p2.matcher(seg); 766 if (m.matches()) { 767 state = 5; 768 seg = m.group(2); 769 } else { 770 // This is a single date (e.g. 2002/01/01) 771 l.add(new TimestampRange(startDate)); 772 state = 1; 773 } 774 } else if (state == 5) { 775 l.add(new TimestampRange(startDate, parseDate(seg, pp))); 776 seg = seg.substring(pp.getIndex()).trim(); 777 pp.setIndex(0); 778 state = 1; 779 } 780 } 781 782 ranges = l.toArray(new TimestampRange[l.size()]); 783 } 784 785 public boolean matches(Calendar c) { 786 if (ranges.length == 0) return true; 787 for (int i = 0; i < ranges.length; i++) 788 if (ranges[i].matches(c)) 789 return true; 790 return false; 791 } 792 } 793 794 /** 795 * A construct representing a single search range in a single search pattern. 796 * All possible forms of search patterns are boiled down to these timestamp ranges. 797 */ 798 private static class TimestampRange { 799 Calendar start; 800 Calendar end; 801 802 public TimestampRange(CalendarP start, CalendarP end) { 803 this.start = start.copy().roll(MILLISECOND, -1).getCalendar(); 804 this.end = end.roll(1).getCalendar(); 805 } 806 807 public TimestampRange(CalendarP singleDate) { 808 this.start = singleDate.copy().roll(MILLISECOND, -1).getCalendar(); 809 this.end = singleDate.roll(1).getCalendar(); 810 } 811 812 public TimestampRange(String op, CalendarP singleDate) { 813 if (op.equals(">")) { 814 this.start = singleDate.roll(1).roll(MILLISECOND, -1).getCalendar(); 815 this.end = new CalendarP(new Date(Long.MAX_VALUE), 0).getCalendar(); 816 } else if (op.equals("<")) { 817 this.start = new CalendarP(new Date(0), 0).getCalendar(); 818 this.end = singleDate.getCalendar(); 819 } else if (op.equals(">=")) { 820 this.start = singleDate.roll(MILLISECOND, -1).getCalendar(); 821 this.end = new CalendarP(new Date(Long.MAX_VALUE), 0).getCalendar(); 822 } else if (op.equals("<=")) { 823 this.start = new CalendarP(new Date(0), 0).getCalendar(); 824 this.end = singleDate.roll(1).getCalendar(); 825 } 826 } 827 828 public boolean matches(Calendar c) { 829 boolean b = (c.after(start) && c.before(end)); 830 return b; 831 } 832 } 833 834 private static int getPrecisionField(String pattern) { 835 if (pattern.indexOf('s') != -1) 836 return SECOND; 837 if (pattern.indexOf('m') != -1) 838 return MINUTE; 839 if (pattern.indexOf('H') != -1) 840 return HOUR_OF_DAY; 841 if (pattern.indexOf('d') != -1) 842 return DAY_OF_MONTH; 843 if (pattern.indexOf('M') != -1) 844 return MONTH; 845 if (pattern.indexOf('y') != -1) 846 return YEAR; 847 return Calendar.MILLISECOND; 848 } 849 850 851 /** 852 * Parses a timestamp string off the beginning of the string segment 'seg'. 853 * Goes through each possible valid timestamp format until it finds a match. 854 * The position where the parsing left off is stored in pp. 855 * 856 * @param seg The string segment being parsed. 857 * @param pp Where parsing last left off. 858 * @return An object representing a timestamp. 859 */ 860 CalendarP parseDate(String seg, ParsePosition pp) { 861 862 CalendarP cal = null; 863 864 for (int i = 0; i < validTimestampFormats.length && cal == null; i++) { 865 pp.setIndex(0); 866 SimpleDateFormat f = validTimestampFormats[i]; 867 Date d = f.parse(seg, pp); 868 int idx = pp.getIndex(); 869 if (idx != 0) { 870 // it only counts if the next character is '-', 'space', or end-of-string. 871 char c = (seg.length() == idx ? 0 : seg.charAt(idx)); 872 if (c == 0 || c == '-' || Character.isWhitespace(c)) 873 cal = new CalendarP(d, getPrecisionField(f.toPattern())); 874 } 875 } 876 877 if (cal == null) 878 throw new FormattedRuntimeException("Invalid date encountered: ''{0}''", seg); 879 880 return cal; 881 } 882 883 /** 884 * Combines a Calendar with a precision identifier. 885 */ 886 private static class CalendarP { 887 public Calendar c; 888 public int precision; 889 890 public CalendarP(Date date, int precision) { 891 c = Calendar.getInstance(); 892 c.setTime(date); 893 this.precision = precision; 894 } 895 896 public CalendarP copy() { 897 return new CalendarP(c.getTime(), precision); 898 } 899 900 public CalendarP roll(int field, int amount) { 901 c.add(field, amount); 902 return this; 903 } 904 905 public CalendarP roll(int amount) { 906 return roll(precision, amount); 907 } 908 909 public Calendar getCalendar() { 910 return c; 911 } 912 } 913 914 //==================================================================================================== 915 // StringMatcher 916 //==================================================================================================== 917 private static class StringMatcher implements IMatcher<Object> { 918 919 private SearchPattern[] searchPatterns; 920 921 /** 922 * Construct a string matcher for the given search pattern. 923 * 924 * @param searchPattern The search pattern. See class usage for details. 925 * @param ignoreCase If <jk>true</jk>, use case-insensitive matching. 926 */ 927 public StringMatcher(String searchPattern, boolean ignoreCase) { 928 this.searchPatterns = new SearchPattern[1]; 929 this.searchPatterns[0] = new SearchPattern(searchPattern, ignoreCase); 930 } 931 932 /** 933 * Returns 'true' if this string matches the pattern(s). 934 * Always returns false on null input. 935 */ 936 @Override /* IMatcher */ 937 public boolean matches(Object in) { 938 if (in == null) return false; 939 for (int i = 0; i < searchPatterns.length; i++) { 940 if (! searchPatterns[i].matches(in.toString())) 941 return false; 942 } 943 return true; 944 } 945 946 } 947 /** 948 * A construct representing a single search pattern. 949 */ 950 private static class SearchPattern { 951 Pattern[] orPatterns, andPatterns, notPatterns; 952 953 public SearchPattern(String searchPattern, boolean ignoreCase) { 954 955 List<Pattern> ors = new LinkedList<>(); 956 List<Pattern> ands = new LinkedList<>(); 957 List<Pattern> nots = new LinkedList<>(); 958 959 for (String arg : breakUpTokens(searchPattern)) { 960 char prefix = arg.charAt(0); 961 String token = arg.substring(1); 962 963 token = token.replaceAll("([\\?\\*\\+\\\\\\[\\]\\{\\}\\(\\)\\^\\$\\.])", "\\\\$1"); 964 token = token.replace("\u9997", ".*"); 965 token = token.replace("\u9996", ".?"); 966 967 if (! token.startsWith(".*")) 968 token = "^" + token; 969 if (! token.endsWith(".*")) 970 token = token + "$"; 971 972 int flags = Pattern.DOTALL; 973 if (ignoreCase) 974 flags |= Pattern.CASE_INSENSITIVE; 975 976 Pattern p = Pattern.compile(token, flags); 977 978 if (prefix == '^') 979 ors.add(p); 980 else if (prefix == '+') 981 ands.add(p); 982 else if (prefix == '-') 983 nots.add(p); 984 } 985 orPatterns = ors.toArray(new Pattern[ors.size()]); 986 andPatterns = ands.toArray(new Pattern[ands.size()]); 987 notPatterns = nots.toArray(new Pattern[nots.size()]); 988 } 989 990 /** 991 * Break up search pattern into separate tokens. 992 */ 993 private static List<String> breakUpTokens(String s) { 994 995 // If the string is null or all whitespace, return an empty vector. 996 if (s == null || s.trim().length() == 0) 997 return Collections.emptyList(); 998 999 // Pad with spaces. 1000 s = " " + s + " "; 1001 1002 // Replace instances of [+] and [-] inside single and double quotes with 1003 // \u2001 and \u2002 for later replacement. 1004 int escapeCount = 0; 1005 boolean inSingleQuote = false; 1006 boolean inDoubleQuote = false; 1007 char[] ca = s.toCharArray(); 1008 for (int i = 0; i < ca.length; i++) { 1009 if (ca[i] == '\\') escapeCount++; 1010 else if (escapeCount % 2 == 0) { 1011 if (ca[i] == '\'') inSingleQuote = ! inSingleQuote; 1012 else if (ca[i] == '"') inDoubleQuote = ! inDoubleQuote; 1013 else if (ca[i] == '+' && (inSingleQuote || inDoubleQuote)) ca[i] = '\u9999'; 1014 else if (ca[i] == '-' && (inSingleQuote || inDoubleQuote)) ca[i] = '\u9998'; 1015 } 1016 if (ca[i] != '\\') escapeCount = 0; 1017 } 1018 s = new String(ca); 1019 1020 // Remove spaces between '+' or '-' and the keyword. 1021 //s = perl5Util.substitute("s/([\\+\\-])\\s+/$1/g", s); 1022 s = s.replaceAll("([\\+\\-])\\s+", "$1"); 1023 1024 // Replace: [*]->[\u3001] as placeholder for '%', ignore escaped. 1025 s = replace(s, '*', '\u9997', true); 1026 // Replace: [?]->[\u3002] as placeholder for '_', ignore escaped. 1027 s = replace(s, '?', '\u9996', true); 1028 // Replace: [\*]->[*], [\?]->[?] 1029 s = unEscapeChars(s, new char[]{'*','?'}); 1030 1031 // Remove spaces 1032 s = s.trim(); 1033 1034 // Re-replace the [+] and [-] characters inside quotes. 1035 s = s.replace('\u9999', '+'); 1036 s = s.replace('\u9998', '-'); 1037 1038 String[] sa = splitQuoted(s, ' '); 1039 List<String> l = new ArrayList<>(sa.length); 1040 int numOrs = 0; 1041 for (int i = 0; i < sa.length; i++) { 1042 String token = sa[i]; 1043 int len = token.length(); 1044 if (len > 0) { 1045 char c = token.charAt(0); 1046 String s2 = null; 1047 if ((c == '+' || c == '-') && len > 1) 1048 s2 = token.substring(1); 1049 else { 1050 s2 = token; 1051 c = '^'; 1052 numOrs++; 1053 } 1054 // Trim off leading and trailing single and double quotes. 1055 if (s2.matches("\".*\"") || s2.matches("'.*'")) 1056 s2 = s2.substring(1, s2.length()-1); 1057 1058 // Replace: [\"]->["] 1059 s2 = unEscapeChars(s2, new char[]{'"','\''}); 1060 1061 // Un-escape remaining escaped backslashes. 1062 s2 = unEscapeChars(s2, new char[]{'\\'}); 1063 1064 l.add(c + s2); 1065 } 1066 } 1067 1068 // If there's a single OR clause, turn it into an AND clause (makes the SQL cleaner). 1069 if (numOrs == 1) { 1070 int ii = l.size(); 1071 for (int i = 0; i < ii; i++) { 1072 String x = l.get(i); 1073 if (x.charAt(0) == '^') 1074 l.set(i, '+'+x.substring(1)); 1075 } 1076 } 1077 return l; 1078 } 1079 1080 public boolean matches(String input) { 1081 if (input == null) return false; 1082 for (int i = 0; i < andPatterns.length; i++) 1083 if (! andPatterns[i].matcher(input).matches()) 1084 return false; 1085 for (int i = 0; i < notPatterns.length; i++) 1086 if (notPatterns[i].matcher(input).matches()) 1087 return false; 1088 for (int i = 0; i < orPatterns.length; i++) 1089 if (orPatterns[i].matcher(input).matches()) 1090 return true; 1091 return orPatterns.length == 0; 1092 } 1093 1094 } 1095 1096 /* 1097 * Same as split(String, char), but does not split on characters inside 1098 * single quotes. 1099 * Does not split on escaped delimiters, and escaped quotes are also ignored. 1100 * Example: 1101 * split("a,b,c",',') -> {"a","b","c"} 1102 * split("a,'b,b,b',c",',') -> {"a","'b,b,b'","c"} 1103 */ 1104 static final String[] splitQuoted(String s, char c) { 1105 1106 if (s == null || s.matches("\\s*")) 1107 return new String[0]; 1108 1109 List<String> l = new LinkedList<>(); 1110 char[] sArray = s.toCharArray(); 1111 int x1 = 0; 1112 int escapeCount = 0; 1113 boolean inSingleQuote = false; 1114 boolean inDoubleQuote = false; 1115 for (int i = 0; i < sArray.length; i++) { 1116 if (sArray[i] == '\\') escapeCount++; 1117 else if (escapeCount % 2 == 0) { 1118 if (sArray[i] == '\'' && ! inDoubleQuote) inSingleQuote = ! inSingleQuote; 1119 else if (sArray[i] == '"' && ! inSingleQuote) inDoubleQuote = ! inDoubleQuote; 1120 else if (sArray[i] == c && ! inSingleQuote && ! inDoubleQuote) { 1121 String s2 = new String(sArray, x1, i-x1).trim(); 1122 l.add(s2); 1123 x1 = i+1; 1124 } 1125 } 1126 if (sArray[i] != '\\') escapeCount = 0; 1127 } 1128 String s2 = new String(sArray, x1, sArray.length-x1).trim(); 1129 l.add(s2); 1130 1131 return l.toArray(new String[l.size()]); 1132 } 1133 1134 /** 1135 * Replaces tokens in a string with a different token. 1136 * 1137 * <p> 1138 * replace("A and B and C", "and", "or") -> "A or B or C" 1139 * replace("andandand", "and", "or") -> "ororor" 1140 * replace(null, "and", "or") -> null 1141 * replace("andandand", null, "or") -> "andandand" 1142 * replace("andandand", "", "or") -> "andandand" 1143 * replace("A and B and C", "and", null) -> "A B C" 1144 * @param ignoreEscapedChars Specify 'true' if escaped 'from' characters should be ignored. 1145 */ 1146 static String replace(String s, char from, char to, boolean ignoreEscapedChars) { 1147 if (s == null) return null; 1148 1149 char[] sArray = s.toCharArray(); 1150 1151 int escapeCount = 0; 1152 int singleQuoteCount = 0; 1153 int doubleQuoteCount = 0; 1154 for (int i = 0; i < sArray.length; i++) { 1155 char c = sArray[i]; 1156 if (c == '\\' && ignoreEscapedChars) 1157 escapeCount++; 1158 else if (escapeCount % 2 == 0) { 1159 if (c == from && singleQuoteCount % 2 == 0 && doubleQuoteCount % 2 == 0) 1160 sArray[i] = to; 1161 } 1162 if (sArray[i] != '\\') escapeCount = 0; 1163 } 1164 return new String(sArray); 1165 } 1166 1167 /** 1168 * Removes escape characters (specified by escapeChar) from the specified characters. 1169 */ 1170 static String unEscapeChars(String s, char[] toEscape) { 1171 char escapeChar = '\\'; 1172 if (s == null) return null; 1173 if (s.length() == 0) return s; 1174 StringBuffer sb = new StringBuffer(s.length()); 1175 char[] sArray = s.toCharArray(); 1176 for (int i = 0; i < sArray.length; i++) { 1177 char c = sArray[i]; 1178 1179 if (c == escapeChar) { 1180 if (i+1 != sArray.length) { 1181 char c2 = sArray[i+1]; 1182 boolean isOneOf = false; 1183 for (int j = 0; j < toEscape.length && ! isOneOf; j++) 1184 isOneOf = (c2 == toEscape[j]); 1185 if (isOneOf) { 1186 i++; 1187 } else if (c2 == escapeChar) { 1188 sb.append(escapeChar); 1189 i++; 1190 } 1191 } 1192 } 1193 sb.append(sArray[i]); 1194 } 1195 return sb.toString(); 1196 } 1197} 1198 1199