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=>2011"</js>,<js>"property=>=2011"</js>,<js>"property=<2011"</js>,<js>"property=<=2011"</js> - Open-ended ranges 044 * <li><js>"property=>2011"</js>,<js>"property=>=2011"</js>,<js>"property=<2011"</js>,<js>"property=<=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}