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