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