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