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=>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 * 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}