001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.objecttools;
018
019import static java.util.Calendar.*;
020import static org.apache.juneau.internal.StateMachineState.*;
021
022import java.text.*;
023import java.util.*;
024
025import org.apache.juneau.*;
026import org.apache.juneau.common.utils.*;
027import org.apache.juneau.internal.*;
028
029/**
030 * Date/time matcher factory for the {@link ObjectSearcher} class.
031 *
032 * <p>
033 *    The class provides searching based on the following patterns:
034 * </p>
035 * <ul>
036 *    <li><js>"property=2011"</js> - A single year
037 *    <li><js>"property=2011 2013 2015"</js> - Multiple years
038 *    <li><js>"property=2011-01"</js> - A single month
039 *    <li><js>"property=2011-01-01"</js> - A single day
040 *    <li><js>"property=2011-01-01T12"</js> - A single hour
041 *    <li><js>"property=2011-01-01T12:30"</js> - A single minute
042 *    <li><js>"property=2011-01-01T12:30:45"</js> - A single second
043 *    <li><js>"property=&gt;2011"</js>,<js>"property=&gt;=2011"</js>,<js>"property=&lt;2011"</js>,<js>"property=&lt;=2011"</js> - Open-ended ranges
044 *    <li><js>"property=&gt;2011"</js>,<js>"property=&gt;=2011"</js>,<js>"property=&lt;2011"</js>,<js>"property=&lt;=2011"</js> - Open-ended ranges
045 *    <li><js>"property=2011 - 2013-06-30"</js> - Closed ranges
046 * </ul>
047 *
048 * <h5 class='section'>See Also:</h5><ul>
049 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/ObjectTools">Object Tools</a>
050 * </ul>
051 */
052public class TimeMatcherFactory extends MatcherFactory {
053
054   /**
055    * Default reusable matcher.
056    */
057   public static final TimeMatcherFactory DEFAULT = new TimeMatcherFactory();
058
059   private final SimpleDateFormat[] formats;
060
061   /**
062    * Constructor.
063    */
064   protected TimeMatcherFactory() {
065      this.formats = getTimestampFormats();
066   }
067
068   /**
069    * TODO
070    *
071    * @return TODO
072    */
073   protected SimpleDateFormat[] getTimestampFormats() {
074      String[] s = getTimestampFormatStrings();
075      SimpleDateFormat[] a = new SimpleDateFormat[s.length];
076      for (int i = 0; i < s.length; i++)
077         a[i] = new SimpleDateFormat(s[i]);
078      return a;
079   }
080
081   /**
082    * TODO
083    *
084    * @return TODO
085    */
086   protected String[] getTimestampFormatStrings() {
087      return new String[]{
088         "yyyy-MM-dd'T'HH:mm:ss",
089         "yyyy-MM-dd'T'HH:mm",
090         "yyyy-MM-dd'T'HH",
091         "yyyy-MM-dd",
092         "yyyy-MM",
093         "yyyy"
094      };
095   }
096
097   @Override
098   public boolean canMatch(ClassMeta<?> cm) {
099      return cm.isDateOrCalendar();
100   }
101
102   @Override
103   public AbstractMatcher create(String pattern) {
104      return new TimeMatcher(formats, pattern);
105   }
106
107   /**
108    * A construct representing a single search pattern.
109    */
110   private static class TimeMatcher extends AbstractMatcher {
111
112      private static final AsciiSet
113         DT = AsciiSet.of("0123456789-:T./"),
114         WS = AsciiSet.of(" \t");
115
116      TimestampRange[] ranges;
117      List<TimestampRange> l = new LinkedList<>();
118
119      public TimeMatcher(SimpleDateFormat[] f, String s) {
120
121         // Possible patterns:
122         // >2000, <2000, >=2000, <=2000, > 2000, 2000 - 2001, '2000', >'2000', '2000'-'2001', '2000' - '2001'
123
124         // Possible states:
125         // S01 = Looking for [<]/[>]/quote/NUM ([>]=S02, [<]=S03, [']=S05, ["]=S06, NUM=S08)
126         // S02 = Found [>], looking for [=]/quote/NUM ([=]=S04, [']=S05, ["]=S06, NUM=S08)
127         // S03 = Found [<], looking for [=]/quote/NUM ([=]=S04, [']=S05, ["]=S06, NUM=S08)
128         // S04 = Found [>=] or [<=], looking for quote/NUM ([']=S05, ["]=S06, NUM=S08)
129         // S05 = Found ['], looking for ['] ([']=S01)
130         // S06 = Found ["], looking for ["] (["]=S01)
131         // S07 = Found [123"] or [123'], looking for WS (WS=S09)
132         // S08 = Found [2], looking for WS (WS=S09)
133         // S09 = Found [2000 ], looking for [-]/quote/NUM ([-]=S10, [']=S11, ["]=S12, NUM=S13)
134         // S10 = Found [2000 -], looking for quote/NUM ([']=S11, ["]=S12, NUM=S13)
135         // S11 = Found [2000 - '], looking for ['] ([']=S01)
136         // S12 = Found [2000 - "], looking for ["] (["]=S01)
137         // S13 = Found [2000 - 2], looking for WS (WS=S01)
138
139         StateMachineState state = S01;
140         int mark = 0;
141         Equality eq = Equality.NONE;
142         String s1 = null, s2 = null;
143
144         int i;
145         char c = 0;
146         for (i = 0; i < s.trim().length(); i++) {
147            c = s.charAt(i);
148            if (state == S01) {
149               // S01 = Looking for [>]/[<]/quote/NUM ([>]=S02, [<]=S03, [']=S05, ["]=S06, NUM=S08)
150               if (WS.contains(c)) {
151                  state = S01;
152               } else if (c == '>') {
153                  state = S02;
154                  eq = Equality.GT;
155               } else if (c == '<') {
156                  state = S03;
157                  eq = Equality.LT;
158               } else if (c == '\'') {
159                  state = S05;
160                  mark = i+1;
161               } else if (c == '"') {
162                  state = S06;
163                  mark = i+1;
164               } else if (DT.contains(c)) {
165                  state = S08;
166                  mark = i;
167               } else {
168                  break;
169               }
170            } else if (state == S02) {
171               // S02 = Found [>], looking for [=]/quote/NUM ([=]=S04, [']=S05, ["]=S06, NUM=S08)
172               if (WS.contains(c)) {
173                  state = S02;
174               } else if (c == '=') {
175                  state = S04;
176                  eq = Equality.GTE;
177               } else if (c == '\'') {
178                  state = S05;
179                  mark = i+1;
180               } else if (c == '"') {
181                  state = S06;
182                  mark = i+1;
183               } else if (DT.contains(c)) {
184                  state = S08;
185                  mark = i;
186               } else {
187                  break;
188               }
189            } else if (state == S03) {
190               // S03 = Found [<], looking for [=]/quote/NUM ([=]=S04, [']=S05, ["]=S06, NUM=S08)
191               if (WS.contains(c)) {
192                  state = S03;
193               } else if (c == '=') {
194                  state = S04;
195                  eq = Equality.LTE;
196               } else if (c == '\'') {
197                  state = S05;
198                  mark = i+1;
199               } else if (c == '"') {
200                  state = S06;
201                  mark = i+1;
202               } else if (DT.contains(c)) {
203                  state = S08;
204                  mark = i;
205               } else {
206                  break;
207               }
208            } else if (state == S04) {
209               // S04 = Found [>=] or [<=], looking for quote/NUM ([']=S05, ["]=S06, NUM=S08)
210               if (WS.contains(c)) {
211                  state = S04;
212               } else if (c == '\'') {
213                  state = S05;
214                  mark = i+1;
215               } else if (c == '"') {
216                  state = S06;
217                  mark = i+1;
218               } else if (DT.contains(c)) {
219                  state = S08;
220                  mark = i;
221               } else {
222                  break;
223               }
224            } else if (state == S05) {
225               // S05 = Found ['], looking for ['] ([']=S07)
226               if (c == '\'') {
227                  state = S07;
228                  s1 = s.substring(mark, i);
229               }
230            } else if (state == S06) {
231               // S06 = Found ["], looking for ["] (["]=S07)
232               if (c == '"') {
233                  state = S07;
234                  s1 = s.substring(mark, i);
235               }
236            } else if (state == S07) {
237               // S07 = Found [123"] or [123'], looking for WS (WS=S09)
238               if (WS.contains(c)) {
239                  state = S09;
240               } else if (c == '-') {
241                  state = S10;
242               } else {
243                  break;
244               }
245            } else if (state == S08) {
246               // S08 = Found [1], looking for WS (WS=S09)
247               if (WS.contains(c)) {
248                  state = S09;
249                  s1 = s.substring(mark, i);
250               }
251            } else if (state == S09) {
252               // S09 = Found [2000 ], looking for [-]/[>]/[<]/quote/NUM ([-]=S10, [>]=S02, [<]=S03, [']=S05, ["]=S06, NUM=S08)
253               if (WS.contains(c)) {
254                  state = S09;
255               } else if (c == '-') {
256                  state = S10;
257               } else if (c == '>') {
258                  state = S02;
259                  l.add(new TimestampRange(f, eq, s1));
260                  eq = Equality.GT;
261                  s1 = null;
262               } else if (c == '<') {
263                  state = S03;
264                  l.add(new TimestampRange(f, eq, s1));
265                  eq = Equality.LT;
266                  s1 = null;
267               } else if (c == '\'') {
268                  state = S05;
269                  l.add(new TimestampRange(f, eq, s1));
270                  mark = i+1;
271                  eq = null;
272                  s1 = null;
273               } else if (c == '"') {
274                  state = S06;
275                  l.add(new TimestampRange(f, eq, s1));
276                  mark = i+1;
277                  eq = null;
278                  s1 = null;
279               } else if (DT.contains(c)) {
280                  state = S08;
281                  l.add(new TimestampRange(f, eq, s1));
282                  eq = null;
283                  s1 = null;
284                  mark = i;
285               } else {
286                  break;
287               }
288            } else if (state == S10) {
289               // S10 = Found [2000 -], looking for quote/NUM ([']=S11, ["]=S12, NUM=S13)
290               if (WS.contains(c)) {
291                  state = S10;
292               } else if (c == '\'') {
293                  state = S11;
294                  mark = i+1;
295               } else if (c == '"') {
296                  state = S12;
297                  mark = i+1;
298               } else if (DT.contains(c)) {
299                  state = S13;
300                  mark = i;
301               } else {
302                  break;
303               }
304            } else if (state == S11) {
305               // S11 = Found [2000 - '], looking for ['] ([']=S01)
306               if (c == '\'') {
307                  state = S01;
308                  s2 = s.substring(mark, i);
309                  l.add(new TimestampRange(f, s1, s2));
310                  s1 = null;
311                  s2 = null;
312               }
313            } else if (state == S12) {
314               // S12 = Found [2000 - "], looking for ["] (["]=S01)
315               if (c == '"') {
316                  state = S01;
317                  s2 = s.substring(mark, i);
318                  l.add(new TimestampRange(f, s1, s2));
319                  s1 = null;
320                  s2 = null;
321               }
322            } else /* (state == S13) */ {
323               // S13 = Found [2000 - 2], looking for WS (WS=S01)
324               if (WS.contains(c)) {
325                  state = S01;
326                  s2 = s.substring(mark, i);
327                  l.add(new TimestampRange(f, s1, s2));
328                  s1 = null;
329                  s2 = null;
330               }
331            }
332         }
333
334         if (i != s.length())
335            throw new PatternException("Invalid range pattern ({0}): pattern=[{1}], pos=[{2}], char=[{3}]", state, s, i, c);
336
337         if (state == S01) {
338            // No tokens found.
339         } else if (state == S02 || state == S03 || state == S04 || state == S05 || state == S06 || state == S10 || state == S11 || state == S12) {
340            throw new PatternException("Invalid range pattern (E{0}): {1}", state, s);
341         } else if (state == S07) {
342            l.add(new TimestampRange(f, eq, s1));
343         } else if (state == S08) {
344            s1 = s.substring(mark).trim();
345            l.add(new TimestampRange(f, eq, s1));
346         } else /* (state == S13) */ {
347            s2 = s.substring(mark).trim();
348            l.add(new TimestampRange(f, s1, s2));
349         }
350
351         ranges = l.toArray(new TimestampRange[l.size()]);
352      }
353
354      @Override
355      public boolean matches(ClassMeta<?> cm, Object o) {
356         if (ranges.length == 0)
357            return true;
358
359         Calendar c = null;
360         if (cm.isCalendar())
361            c = (Calendar)o;
362         else {
363            c = Calendar.getInstance();
364            c.setTime((Date)o);
365         }
366         for (TimestampRange range : ranges)
367                if (range.matches(c))
368               return true;
369         return false;
370      }
371   }
372
373   /**
374    * A construct representing a single search range in a single search pattern.
375    * All possible forms of search patterns are boiled down to these timestamp ranges.
376    */
377   private static class TimestampRange {
378      Calendar start;
379      Calendar end;
380
381      public TimestampRange(SimpleDateFormat[] formats, String start, String end) {
382         CalendarP start1 = parseDate(formats, start);
383         CalendarP end1 = parseDate(formats, end);
384         this.start = start1.copy().roll(MILLISECOND, -1).getCalendar();
385         this.end = end1.roll(1).getCalendar();
386      }
387
388      public TimestampRange(SimpleDateFormat[] formats, Equality eq, String singleDate) {
389         CalendarP singleDate1 = parseDate(formats, singleDate);
390         if (eq == Equality.GT) {
391            this.start = singleDate1.roll(1).roll(MILLISECOND, -1).getCalendar();
392            this.end = new CalendarP(new Date(Long.MAX_VALUE), 0).getCalendar();
393         } else if (eq == Equality.LT) {
394            this.start = new CalendarP(new Date(0), 0).getCalendar();
395            this.end = singleDate1.getCalendar();
396         } else if (eq == Equality.GTE) {
397            this.start = singleDate1.roll(MILLISECOND, -1).getCalendar();
398            this.end = new CalendarP(new Date(Long.MAX_VALUE), 0).getCalendar();
399         } else if (eq == Equality.LTE) {
400            this.start = new CalendarP(new Date(0), 0).getCalendar();
401            this.end = singleDate1.roll(1).getCalendar();
402         } else {
403            this.start = singleDate1.copy().roll(MILLISECOND, -1).getCalendar();
404            this.end = singleDate1.roll(1).getCalendar();
405         }
406      }
407
408      public boolean matches(Calendar c) {
409         boolean b = (c.after(start) && c.before(end));
410         return b;
411      }
412   }
413
414   private static int getPrecisionField(String pattern) {
415      if (pattern.indexOf('s') != -1)
416         return SECOND;
417      if (pattern.indexOf('m') != -1)
418         return MINUTE;
419      if (pattern.indexOf('H') != -1)
420         return HOUR_OF_DAY;
421      if (pattern.indexOf('d') != -1)
422         return DAY_OF_MONTH;
423      if (pattern.indexOf('M') != -1)
424         return MONTH;
425      if (pattern.indexOf('y') != -1)
426         return YEAR;
427      return Calendar.MILLISECOND;
428   }
429
430   /**
431    * Parses a timestamp string off the beginning of the string segment 'seg'.
432    * Goes through each possible valid timestamp format until it finds a match.
433    * The position where the parsing left off is stored in pp.
434    *
435    * @param seg The string segment being parsed.
436    * @param pp Where parsing last left off.
437    * @return An object representing a timestamp.
438    */
439   static CalendarP parseDate(SimpleDateFormat[] formats, String seg) {
440      ParsePosition pp = new ParsePosition(0);
441      for (SimpleDateFormat f : formats) {
442         Date d = f.parse(seg, pp);
443         int idx = pp.getIndex();
444         if (idx != 0) {
445            // it only counts if the next character is '-', 'space', or end-of-string.
446            char c = (seg.length() == idx ? 0 : seg.charAt(idx));
447            if (c == 0 || c == '-' || Character.isWhitespace(c))
448               return new CalendarP(d, getPrecisionField(f.toPattern()));
449         }
450      }
451
452      throw new BasicRuntimeException("Invalid date encountered:  ''{0}''", seg);
453   }
454
455   /**
456    * Combines a Calendar with a precision identifier.
457    */
458   private static class CalendarP {
459      public Calendar c;
460      public int precision;
461
462      public CalendarP(Date date, int precision) {
463         c = Calendar.getInstance();
464         c.setTime(date);
465         this.precision = precision;
466      }
467
468      public CalendarP copy() {
469         return new CalendarP(c.getTime(), precision);
470      }
471
472      public CalendarP roll(int field, int amount) {
473         c.add(field, amount);
474         return this;
475      }
476
477      public CalendarP roll(int amount) {
478         return roll(precision, amount);
479      }
480
481      public Calendar getCalendar() {
482         return c;
483      }
484   }
485}