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 * <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>&gt; 100</tt> - Greater than 100
125 *    <li><tt>&gt;= 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>&gt;2001</tt>   - After a specific year.
159 *    <li><tt>&gt;=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