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.time.temporal.ChronoField.*;
020import static org.apache.juneau.commons.lang.StateEnum.*;
021
022import java.time.*;
023import java.util.*;
024
025import org.apache.juneau.*;
026import org.apache.juneau.commons.lang.*;
027import org.apache.juneau.commons.time.*;
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    * A construct representing a single search pattern.
056    */
057   private static class TimeMatcher extends AbstractMatcher {
058
059      // @formatter:off
060      private static final AsciiSet
061         DT = AsciiSet.of("0123456789-:T./"),
062         WS = AsciiSet.of(" \t");
063      // @formatter:on
064
065      TimestampRange[] ranges;
066      List<TimestampRange> l = new LinkedList<>();
067
068      public TimeMatcher(String s) {
069
070         // Possible patterns:
071         // >2000, <2000, >=2000, <=2000, > 2000, 2000 - 2001, '2000', >'2000', '2000'-'2001', '2000' - '2001'
072
073         // Possible states:
074         // S1 = Looking for [<]/[>]/quote/NUM ([>]=S2, [<]=S3, [']=S5, ["]=S6, NUM=S8)
075         // S2 = Found [>], looking for [=]/quote/NUM ([=]=S4, [']=S5, ["]=S6, NUM=S8)
076         // S3 = Found [<], looking for [=]/quote/NUM ([=]=S4, [']=S5, ["]=S6, NUM=S8)
077         // S4 = Found [>=] or [<=], looking for quote/NUM ([']=S5, ["]=S6, NUM=S8)
078         // S5 = Found ['], looking for ['] ([']=S1)
079         // S6 = Found ["], looking for ["] (["]=S1)
080         // S7 = Found [123"] or [123'], looking for WS (WS=S9)
081         // S8 = Found [2], looking for WS (WS=S9)
082         // S9 = Found [2000 ], looking for [-]/quote/NUM ([-]=S10, [']=S11, ["]=S12, NUM=S13)
083         // S10 = Found [2000 -], looking for quote/NUM ([']=S11, ["]=S12, NUM=S13)
084         // S11 = Found [2000 - '], looking for ['] ([']=S1)
085         // S12 = Found [2000 - "], looking for ["] (["]=S1)
086         // S13 = Found [2000 - 2], looking for WS (WS=S1)
087
088         var state = S1;
089         var mark = 0;
090         var eq = Equality.NONE;
091         var s1 = (String)null;
092         var s2 = (String)null;
093
094         int i;
095         char c = 0;
096         for (i = 0; i < s.trim().length(); i++) {
097            c = s.charAt(i);
098            if (state == S1) {
099               // S1 = Looking for [>]/[<]/quote/NUM ([>]=S2, [<]=S3, [']=S5, ["]=S6, NUM=S8)
100               if (WS.contains(c)) {
101                  state = S1;
102               } else if (c == '>') {
103                  state = S2;
104                  eq = Equality.GT;
105               } else if (c == '<') {
106                  state = S3;
107                  eq = Equality.LT;
108               } else if (c == '\'') {
109                  state = S5;
110                  mark = i + 1;
111               } else if (c == '"') {
112                  state = S6;
113                  mark = i + 1;
114               } else if (DT.contains(c)) {
115                  state = S8;
116                  mark = i;
117               } else {
118                  break;
119               }
120            } else if (state == S2) {
121               // S2 = Found [>], looking for [=]/quote/NUM ([=]=S4, [']=S5, ["]=S6, NUM=S8)
122               if (WS.contains(c)) {
123                  state = S2;
124               } else if (c == '=') {
125                  state = S4;
126                  eq = Equality.GTE;
127               } else if (c == '\'') {
128                  state = S5;
129                  mark = i + 1;
130               } else if (c == '"') {
131                  state = S6;
132                  mark = i + 1;
133               } else if (DT.contains(c)) {
134                  state = S8;
135                  mark = i;
136               } else {
137                  break;
138               }
139            } else if (state == S3) {
140               // S3 = Found [<], looking for [=]/quote/NUM ([=]=S4, [']=S5, ["]=S6, NUM=S8)
141               if (WS.contains(c)) {
142                  state = S3;
143               } else if (c == '=') {
144                  state = S4;
145                  eq = Equality.LTE;
146               } else if (c == '\'') {
147                  state = S5;
148                  mark = i + 1;
149               } else if (c == '"') {
150                  state = S6;
151                  mark = i + 1;
152               } else if (DT.contains(c)) {
153                  state = S8;
154                  mark = i;
155               } else {
156                  break;
157               }
158            } else if (state == S4) {
159               // S4 = Found [>=] or [<=], looking for quote/NUM ([']=S5, ["]=S6, NUM=S8)
160               if (WS.contains(c)) {
161                  state = S4;
162               } else if (c == '\'') {
163                  state = S5;
164                  mark = i + 1;
165               } else if (c == '"') {
166                  state = S6;
167                  mark = i + 1;
168               } else if (DT.contains(c)) {
169                  state = S8;
170                  mark = i;
171               } else {
172                  break;
173               }
174            } else if (state == S5) {
175               // S5 = Found ['], looking for ['] ([']=S7)
176               if (c == '\'') {
177                  state = S7;
178                  s1 = s.substring(mark, i);
179               }
180            } else if (state == S6) {
181               // S6 = Found ["], looking for ["] (["]=S7)
182               if (c == '"') {
183                  state = S7;
184                  s1 = s.substring(mark, i);
185               }
186            } else if (state == S7) {
187               // S7 = Found [123"] or [123'], looking for WS (WS=S9)
188               if (WS.contains(c)) {
189                  state = S9;
190               } else if (c == '-') {
191                  state = S10;
192               } else {
193                  break;
194               }
195            } else if (state == S8) {
196               // S8 = Found [1], looking for WS (WS=S9)
197               if (WS.contains(c)) {
198                  state = S9;
199                  s1 = s.substring(mark, i);
200               }
201            } else if (state == S9) {
202               // S9 = Found [2000 ], looking for [-]/[>]/[<]/quote/NUM ([-]=S10, [>]=S2, [<]=S3, [']=S5, ["]=S6, NUM=S8)
203               if (WS.contains(c)) {
204                  state = S9;
205               } else if (c == '-') {
206                  state = S10;
207               } else if (c == '>') {
208                  state = S2;
209                  l.add(new TimestampRange(eq, s1));
210                  eq = Equality.GT;
211                  s1 = null;
212               } else if (c == '<') {
213                  state = S3;
214                  l.add(new TimestampRange(eq, s1));
215                  eq = Equality.LT;
216                  s1 = null;
217               } else if (c == '\'') {
218                  state = S5;
219                  l.add(new TimestampRange(eq, s1));
220                  mark = i + 1;
221                  eq = null;
222                  s1 = null;
223               } else if (c == '"') {
224                  state = S6;
225                  l.add(new TimestampRange(eq, s1));
226                  mark = i + 1;
227                  eq = null;
228                  s1 = null;
229               } else if (DT.contains(c)) {
230                  state = S8;
231                  l.add(new TimestampRange(eq, s1));
232                  eq = null;
233                  s1 = null;
234                  mark = i;
235               } else {
236                  break;
237               }
238            } else if (state == S10) {
239               // S10 = Found [2000 -], looking for quote/NUM ([']=S11, ["]=S12, NUM=S13)
240               if (WS.contains(c)) {
241                  state = S10;
242               } else if (c == '\'') {
243                  state = S11;
244                  mark = i + 1;
245               } else if (c == '"') {
246                  state = S12;
247                  mark = i + 1;
248               } else if (DT.contains(c)) {
249                  state = S13;
250                  mark = i;
251               } else {
252                  break;
253               }
254            } else if (state == S11) {
255               // S11 = Found [2000 - '], looking for ['] ([']=S1)
256               if (c == '\'') {
257                  state = S1;
258                  s2 = s.substring(mark, i);
259                  l.add(new TimestampRange(s1, s2));
260                  s1 = null;
261                  s2 = null;
262               }
263            } else if (state == S12) {
264               // S12 = Found [2000 - "], looking for ["] (["]=S1)
265               if (c == '"') {
266                  state = S1;
267                  s2 = s.substring(mark, i);
268                  l.add(new TimestampRange(s1, s2));
269                  s1 = null;
270                  s2 = null;
271               }
272            } else /* (state == S13) */ {
273               // S13 = Found [2000 - 2], looking for WS (WS=S1)
274               if (WS.contains(c)) {
275                  state = S1;
276                  s2 = s.substring(mark, i);
277                  l.add(new TimestampRange(s1, s2));
278                  s1 = null;
279                  s2 = null;
280               }
281            }
282         }
283
284         if (i != s.length())
285            throw new PatternException("Invalid range pattern ({0}): pattern=[{1}], pos=[{2}], char=[{3}]", state, s, i, c);
286
287         if (state == S1) {
288            // No tokens found.
289         } else if (state == S2 || state == S3 || state == S4 || state == S5 || state == S6 || state == S10 || state == S11 || state == S12) {
290            throw new PatternException("Invalid range pattern (E{0}): {1}", state, s);
291         } else if (state == S7) {
292            l.add(new TimestampRange(eq, s1));
293         } else if (state == S8) {
294            s1 = s.substring(mark).trim();
295            l.add(new TimestampRange(eq, s1));
296         } else /* (state == S13) */ {
297            s2 = s.substring(mark).trim();
298            l.add(new TimestampRange(s1, s2));
299         }
300
301         ranges = l.toArray(new TimestampRange[l.size()]);
302      }
303
304      @Override
305      public boolean matches(ClassMeta<?> cm, Object o) {
306         if (ranges.length == 0)
307            return true;
308
309         var zdt = (ZonedDateTime)null;
310         if (cm.isCalendar()) {
311            var c = (Calendar)o;
312            zdt = c.toInstant().atZone(c.getTimeZone().toZoneId());
313         } else {
314            var date = (Date)o;
315            zdt = date.toInstant().atZone(ZoneId.systemDefault());
316         }
317         for (var range : ranges)
318            if (range.matches(zdt))
319               return true;
320         return false;
321      }
322   }
323
324   /**
325    * A construct representing a single search range in a single search pattern.
326    * All possible forms of search patterns are boiled down to these timestamp ranges.
327    */
328   private static class TimestampRange {
329      ZonedDateTime start;
330      ZonedDateTime end;
331
332      public TimestampRange(Equality eq, String singleDate) {
333         var singleDate1 = GranularZonedDateTime.of(singleDate);
334         if (eq == Equality.GT) {
335            this.start = singleDate1.roll(1).roll(MILLI_OF_SECOND, -1).getZonedDateTime();
336            this.end = Instant.ofEpochMilli(Long.MAX_VALUE).atZone(ZoneId.systemDefault());
337         } else if (eq == Equality.LT) {
338            this.start = Instant.ofEpochMilli(0).atZone(ZoneId.systemDefault());
339            this.end = singleDate1.getZonedDateTime();
340         } else if (eq == Equality.GTE) {
341            this.start = singleDate1.roll(MILLI_OF_SECOND, -1).getZonedDateTime();
342            this.end = Instant.ofEpochMilli(Long.MAX_VALUE).atZone(ZoneId.systemDefault());
343         } else if (eq == Equality.LTE) {
344            this.start = Instant.ofEpochMilli(0).atZone(ZoneId.systemDefault());
345            this.end = singleDate1.roll(1).getZonedDateTime();
346         } else {
347            this.start = singleDate1.copy().roll(MILLI_OF_SECOND, -1).getZonedDateTime();
348            this.end = singleDate1.roll(1).getZonedDateTime();
349         }
350      }
351
352      public TimestampRange(String start, String end) {
353         var start1 = GranularZonedDateTime.of(start);
354         var end1 = GranularZonedDateTime.of(end);
355         this.start = start1.copy().roll(MILLI_OF_SECOND, -1).getZonedDateTime();
356         this.end = end1.roll(1).getZonedDateTime();
357      }
358
359      public boolean matches(ZonedDateTime zdt) {
360         return zdt.isAfter(start) && zdt.isBefore(end);
361      }
362   }
363
364   /**
365    * Default reusable matcher.
366    */
367   public static final TimeMatcherFactory DEFAULT = new TimeMatcherFactory();
368
369   /**
370    * Constructor.
371    */
372   protected TimeMatcherFactory() {}
373
374   @Override
375   public boolean canMatch(ClassMeta<?> cm) {
376      return cm.isDateOrCalendar();
377   }
378
379   @Override
380   public AbstractMatcher create(String pattern) {
381      return new TimeMatcher(pattern);
382   }
383}