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