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