| // *************************************************************************************************************************** |
| // * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * |
| // * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * |
| // * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * |
| // * with the License. You may obtain a copy of the License at * |
| // * * |
| // * http://www.apache.org/licenses/LICENSE-2.0 * |
| // * * |
| // * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * |
| // * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * |
| // * specific language governing permissions and limitations under the License. * |
| // *************************************************************************************************************************** |
| package org.apache.juneau.pojotools; |
| |
| import static java.util.Calendar.*; |
| import static org.apache.juneau.internal.StateMachineState.*; |
| |
| import java.text.*; |
| import java.util.*; |
| |
| import org.apache.juneau.*; |
| import org.apache.juneau.internal.*; |
| |
| /** |
| * TODO |
| */ |
| public class TimeMatcherFactory extends MatcherFactory { |
| |
| /** |
| * Default reusable matcher. |
| */ |
| public static final TimeMatcherFactory DEFAULT = new TimeMatcherFactory(); |
| |
| private final SimpleDateFormat[] formats; |
| |
| /** |
| * Constructor. |
| */ |
| protected TimeMatcherFactory() { |
| this.formats = getTimestampFormats(); |
| } |
| |
| /** |
| * TODO |
| * |
| * @return TODO |
| */ |
| protected SimpleDateFormat[] getTimestampFormats() { |
| String[] s = getTimestampFormatStrings(); |
| SimpleDateFormat[] a = new SimpleDateFormat[s.length]; |
| for (int i = 0; i < s.length; i++) |
| a[i] = new SimpleDateFormat(s[i]); |
| return a; |
| } |
| |
| /** |
| * TODO |
| * |
| * @return TODO |
| */ |
| protected String[] getTimestampFormatStrings() { |
| return new String[]{ |
| "yyyy-MM-dd'T'HH:mm:ss", |
| "yyyy-MM-dd'T'HH:mm", |
| "yyyy-MM-dd'T'HH", |
| "yyyy-MM-dd", |
| "yyyy-MM", |
| "yyyy" |
| }; |
| } |
| |
| @Override |
| public boolean canMatch(ClassMeta<?> cm) { |
| return cm.isDateOrCalendar(); |
| } |
| |
| @Override |
| public Matcher create(String pattern) { |
| return new TimeMatcher(formats, pattern); |
| } |
| |
| /** |
| * A construct representing a single search pattern. |
| */ |
| private static class TimeMatcher extends Matcher { |
| |
| private static final AsciiSet |
| DT = AsciiSet.create("0123456789-:T./"), |
| WS = AsciiSet.create(" \t"); |
| |
| TimestampRange[] ranges; |
| List<TimestampRange> l = new LinkedList<>(); |
| |
| public TimeMatcher(SimpleDateFormat[] f, String s) { |
| |
| // Possible patterns: |
| // >2000, <2000, >=2000, <=2000, > 2000, 2000 - 2001, '2000', >'2000', '2000'-'2001', '2000' - '2001' |
| |
| // Possible states: |
| // S01 = Looking for [<]/[>]/quote/NUM ([>]=S02, [<]=S03, [']=S05, ["]=S06, NUM=S08) |
| // S02 = Found [>], looking for [=]/quote/NUM ([=]=S04, [']=S05, ["]=S06, NUM=S08) |
| // S03 = Found [<], looking for [=]/quote/NUM ([=]=S04, [']=S05, ["]=S06, NUM=S08) |
| // S04 = Found [>=] or [<=], looking for quote/NUM ([']=S05, ["]=S06, NUM=S08) |
| // S05 = Found ['], looking for ['] ([']=S01) |
| // S06 = Found ["], looking for ["] (["]=S01) |
| // S07 = Found [123"] or [123'], looking for WS (WS=S09) |
| // S08 = Found [2], looking for WS (WS=S09) |
| // S09 = Found [2000 ], looking for [-]/quote/NUM ([-]=S10, [']=S11, ["]=S12, NUM=S13) |
| // S10 = Found [2000 -], looking for quote/NUM ([']=S11, ["]=S12, NUM=S13) |
| // S11 = Found [2000 - '], looking for ['] ([']=S01) |
| // S12 = Found [2000 - "], looking for ["] (["]=S01) |
| // S13 = Found [2000 - 2], looking for WS (WS=S01) |
| |
| StateMachineState state = S01; |
| int mark = 0; |
| Equality eq = Equality.NONE; |
| String s1 = null, s2 = null; |
| |
| int i; |
| char c = 0; |
| for (i = 0; i < s.trim().length(); i++) { |
| c = s.charAt(i); |
| if (state == S01) { |
| // S01 = Looking for [>]/[<]/quote/NUM ([>]=S02, [<]=S03, [']=S05, ["]=S06, NUM=S08) |
| if (WS.contains(c)) { |
| state = S01; |
| } else if (c == '>') { |
| state = S02; |
| eq = Equality.GT; |
| } else if (c == '<') { |
| state = S03; |
| eq = Equality.LT; |
| } else if (c == '\'') { |
| state = S05; |
| mark = i+1; |
| } else if (c == '"') { |
| state = S06; |
| mark = i+1; |
| } else if (DT.contains(c)) { |
| state = S08; |
| mark = i; |
| } else { |
| break; |
| } |
| } else if (state == S02) { |
| // S02 = Found [>], looking for [=]/quote/NUM ([=]=S04, [']=S05, ["]=S06, NUM=S08) |
| if (WS.contains(c)) { |
| state = S02; |
| } else if (c == '=') { |
| state = S04; |
| eq = Equality.GTE; |
| } else if (c == '\'') { |
| state = S05; |
| mark = i+1; |
| } else if (c == '"') { |
| state = S06; |
| mark = i+1; |
| } else if (DT.contains(c)) { |
| state = S08; |
| mark = i; |
| } else { |
| break; |
| } |
| } else if (state == S03) { |
| // S03 = Found [<], looking for [=]/quote/NUM ([=]=S04, [']=S05, ["]=S06, NUM=S08) |
| if (WS.contains(c)) { |
| state = S03; |
| } else if (c == '=') { |
| state = S04; |
| eq = Equality.LTE; |
| } else if (c == '\'') { |
| state = S05; |
| mark = i+1; |
| } else if (c == '"') { |
| state = S06; |
| mark = i+1; |
| } else if (DT.contains(c)) { |
| state = S08; |
| mark = i; |
| } else { |
| break; |
| } |
| } else if (state == S04) { |
| // S04 = Found [>=] or [<=], looking for quote/NUM ([']=S05, ["]=S06, NUM=S08) |
| if (WS.contains(c)) { |
| state = S04; |
| } else if (c == '\'') { |
| state = S05; |
| mark = i+1; |
| } else if (c == '"') { |
| state = S06; |
| mark = i+1; |
| } else if (DT.contains(c)) { |
| state = S08; |
| mark = i; |
| } else { |
| break; |
| } |
| } else if (state == S05) { |
| // S05 = Found ['], looking for ['] ([']=S07) |
| if (c == '\'') { |
| state = S07; |
| s1 = s.substring(mark, i); |
| } |
| } else if (state == S06) { |
| // S06 = Found ["], looking for ["] (["]=S07) |
| if (c == '"') { |
| state = S07; |
| s1 = s.substring(mark, i); |
| } |
| } else if (state == S07) { |
| // S07 = Found [123"] or [123'], looking for WS (WS=S09) |
| if (WS.contains(c)) { |
| state = S09; |
| } else if (c == '-') { |
| state = S10; |
| } else { |
| break; |
| } |
| } else if (state == S08) { |
| // S08 = Found [1], looking for WS (WS=S09) |
| if (WS.contains(c)) { |
| state = S09; |
| s1 = s.substring(mark, i); |
| } |
| } else if (state == S09) { |
| // S09 = Found [2000 ], looking for [-]/[>]/[<]/quote/NUM ([-]=S10, [>]=S02, [<]=S03, [']=S05, ["]=S06, NUM=S08) |
| if (WS.contains(c)) { |
| state = S09; |
| } else if (c == '-') { |
| state = S10; |
| } else if (c == '>') { |
| state = S02; |
| l.add(new TimestampRange(f, eq, s1)); |
| eq = Equality.GT; |
| s1 = null; |
| } else if (c == '<') { |
| state = S03; |
| l.add(new TimestampRange(f, eq, s1)); |
| eq = Equality.LT; |
| s1 = null; |
| } else if (c == '\'') { |
| state = S05; |
| l.add(new TimestampRange(f, eq, s1)); |
| mark = i+1; |
| eq = null; |
| s1 = null; |
| } else if (c == '"') { |
| state = S06; |
| l.add(new TimestampRange(f, eq, s1)); |
| mark = i+1; |
| eq = null; |
| s1 = null; |
| } else if (DT.contains(c)) { |
| state = S08; |
| l.add(new TimestampRange(f, eq, s1)); |
| eq = null; |
| s1 = null; |
| mark = i; |
| } else { |
| break; |
| } |
| } else if (state == S10) { |
| // S10 = Found [2000 -], looking for quote/NUM ([']=S11, ["]=S12, NUM=S13) |
| if (WS.contains(c)) { |
| state = S10; |
| } else if (c == '\'') { |
| state = S11; |
| mark = i+1; |
| } else if (c == '"') { |
| state = S12; |
| mark = i+1; |
| } else if (DT.contains(c)) { |
| state = S13; |
| mark = i; |
| } else { |
| break; |
| } |
| } else if (state == S11) { |
| // S11 = Found [2000 - '], looking for ['] ([']=S01) |
| if (c == '\'') { |
| state = S01; |
| s2 = s.substring(mark, i); |
| l.add(new TimestampRange(f, s1, s2)); |
| s1 = null; |
| s2 = null; |
| } |
| } else if (state == S12) { |
| // S12 = Found [2000 - "], looking for ["] (["]=S01) |
| if (c == '"') { |
| state = S01; |
| s2 = s.substring(mark, i); |
| l.add(new TimestampRange(f, s1, s2)); |
| s1 = null; |
| s2 = null; |
| } |
| } else /* (state == S13) */ { |
| // S13 = Found [2000 - 2], looking for WS (WS=S01) |
| if (WS.contains(c)) { |
| state = S01; |
| s2 = s.substring(mark, i); |
| l.add(new TimestampRange(f, s1, s2)); |
| s1 = null; |
| s2 = null; |
| } |
| } |
| } |
| |
| if (i != s.length()) |
| throw new PatternException("Invalid range pattern ({0}): pattern=[{1}], pos=[{2}], char=[{3}]", state, s, i, c); |
| |
| if (state == S01) { |
| // No tokens found. |
| } else if (state == S02 || state == S03 || state == S04 || state == S05 || state == S06 || state == S10 || state == S11 || state == S12) { |
| throw new PatternException("Invalid range pattern (E{0}): {1}", state, s); |
| } else if (state == S07) { |
| l.add(new TimestampRange(f, eq, s1)); |
| } else if (state == S08) { |
| s1 = s.substring(mark).trim(); |
| l.add(new TimestampRange(f, eq, s1)); |
| } else /* (state == S13) */ { |
| s2 = s.substring(mark).trim(); |
| l.add(new TimestampRange(f, s1, s2)); |
| } |
| |
| ranges = l.toArray(new TimestampRange[l.size()]); |
| } |
| |
| @Override |
| public boolean matches(ClassMeta<?> cm, Object o) { |
| if (ranges.length == 0) return true; |
| |
| Calendar c = null; |
| if (cm.isCalendar()) |
| c = (Calendar)o; |
| else { |
| c = Calendar.getInstance(); |
| c.setTime((Date)o); |
| } |
| for (int i = 0; i < ranges.length; i++) |
| if (ranges[i].matches(c)) |
| return true; |
| return false; |
| } |
| } |
| |
| /** |
| * A construct representing a single search range in a single search pattern. |
| * All possible forms of search patterns are boiled down to these timestamp ranges. |
| */ |
| private static class TimestampRange { |
| Calendar start; |
| Calendar end; |
| |
| public TimestampRange(SimpleDateFormat[] formats, String start, String end) { |
| CalendarP start1 = parseDate(formats, start); |
| CalendarP end1 = parseDate(formats, end); |
| this.start = start1.copy().roll(MILLISECOND, -1).getCalendar(); |
| this.end = end1.roll(1).getCalendar(); |
| } |
| |
| public TimestampRange(SimpleDateFormat[] formats, Equality eq, String singleDate) { |
| CalendarP singleDate1 = parseDate(formats, singleDate); |
| if (eq == Equality.GT) { |
| this.start = singleDate1.roll(1).roll(MILLISECOND, -1).getCalendar(); |
| this.end = new CalendarP(new Date(Long.MAX_VALUE), 0).getCalendar(); |
| } else if (eq == Equality.LT) { |
| this.start = new CalendarP(new Date(0), 0).getCalendar(); |
| this.end = singleDate1.getCalendar(); |
| } else if (eq == Equality.GTE) { |
| this.start = singleDate1.roll(MILLISECOND, -1).getCalendar(); |
| this.end = new CalendarP(new Date(Long.MAX_VALUE), 0).getCalendar(); |
| } else if (eq == Equality.LTE) { |
| this.start = new CalendarP(new Date(0), 0).getCalendar(); |
| this.end = singleDate1.roll(1).getCalendar(); |
| } else { |
| this.start = singleDate1.copy().roll(MILLISECOND, -1).getCalendar(); |
| this.end = singleDate1.roll(1).getCalendar(); |
| } |
| } |
| |
| public boolean matches(Calendar c) { |
| boolean b = (c.after(start) && c.before(end)); |
| return b; |
| } |
| } |
| |
| private static int getPrecisionField(String pattern) { |
| if (pattern.indexOf('s') != -1) |
| return SECOND; |
| if (pattern.indexOf('m') != -1) |
| return MINUTE; |
| if (pattern.indexOf('H') != -1) |
| return HOUR_OF_DAY; |
| if (pattern.indexOf('d') != -1) |
| return DAY_OF_MONTH; |
| if (pattern.indexOf('M') != -1) |
| return MONTH; |
| if (pattern.indexOf('y') != -1) |
| return YEAR; |
| return Calendar.MILLISECOND; |
| } |
| |
| /** |
| * Parses a timestamp string off the beginning of the string segment 'seg'. |
| * Goes through each possible valid timestamp format until it finds a match. |
| * The position where the parsing left off is stored in pp. |
| * |
| * @param seg The string segment being parsed. |
| * @param pp Where parsing last left off. |
| * @return An object representing a timestamp. |
| */ |
| static CalendarP parseDate(SimpleDateFormat[] formats, String seg) { |
| ParsePosition pp = new ParsePosition(0); |
| for (int i = 0; i < formats.length; i++) { |
| SimpleDateFormat f = formats[i]; |
| Date d = f.parse(seg, pp); |
| int idx = pp.getIndex(); |
| if (idx != 0) { |
| // it only counts if the next character is '-', 'space', or end-of-string. |
| char c = (seg.length() == idx ? 0 : seg.charAt(idx)); |
| if (c == 0 || c == '-' || Character.isWhitespace(c)) |
| return new CalendarP(d, getPrecisionField(f.toPattern())); |
| } |
| } |
| |
| throw new FormattedRuntimeException("Invalid date encountered: ''{0}''", seg); |
| } |
| |
| /** |
| * Combines a Calendar with a precision identifier. |
| */ |
| private static class CalendarP { |
| public Calendar c; |
| public int precision; |
| |
| public CalendarP(Date date, int precision) { |
| c = Calendar.getInstance(); |
| c.setTime(date); |
| this.precision = precision; |
| } |
| |
| public CalendarP copy() { |
| return new CalendarP(c.getTime(), precision); |
| } |
| |
| public CalendarP roll(int field, int amount) { |
| c.add(field, amount); |
| return this; |
| } |
| |
| public CalendarP roll(int amount) { |
| return roll(precision, amount); |
| } |
| |
| public Calendar getCalendar() { |
| return c; |
| } |
| } |
| } |