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