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