1 package emissary.util;
2
3 import emissary.config.ConfigEntry;
4 import emissary.config.ConfigUtil;
5 import emissary.config.Configurator;
6
7 import jakarta.annotation.Nullable;
8 import org.apache.commons.collections4.CollectionUtils;
9 import org.apache.commons.lang3.StringUtils;
10 import org.slf4j.Logger;
11 import org.slf4j.LoggerFactory;
12
13 import java.io.IOException;
14 import java.time.DateTimeException;
15 import java.time.LocalDate;
16 import java.time.LocalDateTime;
17 import java.time.OffsetDateTime;
18 import java.time.ZoneId;
19 import java.time.ZonedDateTime;
20 import java.time.format.DateTimeFormatter;
21 import java.time.format.DateTimeFormatterBuilder;
22 import java.time.format.DateTimeParseException;
23 import java.time.temporal.TemporalAccessor;
24 import java.util.Collections;
25 import java.util.List;
26 import java.util.Objects;
27 import java.util.regex.Matcher;
28 import java.util.regex.Pattern;
29
30 import static java.util.stream.Collectors.toList;
31
32
33
34
35
36
37
38
39
40 public final class FlexibleDateTimeParser {
41
42
43 private static final Logger logger = LoggerFactory.getLogger(FlexibleDateTimeParser.class);
44
45
46 private static final String CFG_FORMAT_MAIN = "FORMAT_DATETIME_MAIN";
47 private static final String CFG_FORMAT_EXTRA = "FORMAT_DATETIME_EXTRA";
48 private static final String CFG_TIMEZONE = "TIMEZONE";
49 private static final String CFG_REMOVE_REGEX = "REMOVE_REGEX";
50 private static final String CFG_EXTRA_TEXT_REMOVE_REGEX = "EXTRA_TEXT_REMOVE_REGEX";
51 private static final String DEFAULT_TIMEZONE = "GMT";
52 private static final String SPACE = " ";
53 private static final String EMPTY = "";
54
55
56 private static final Pattern REPLACE = Pattern.compile("\t+|[ ]+", Pattern.DOTALL);
57
58
59
60
61
62 private static final Pattern remove;
63
64
65
66
67
68 private static final Pattern extraTextRemove;
69
70
71 private static final ZoneId timezone;
72
73
74 private static final List<DateTimeFormatter> dateFormatsMain;
75
76
77 private static final List<DateTimeFormatter> dateFormatsExtra;
78
79
80 static {
81 try {
82
83 Configurator configurator = ConfigUtil.getConfigInfo(FlexibleDateTimeParser.class);
84 timezone = setupTimezone(configurator.findStringEntry(CFG_TIMEZONE, DEFAULT_TIMEZONE));
85
86 List<ConfigEntry> configEntriesMain = configurator.findStringMatchEntries(CFG_FORMAT_MAIN);
87 dateFormatsMain = setupDateFormats(configEntriesMain, getConfigFormats(configEntriesMain));
88
89 List<ConfigEntry> configEntriesExtra = configurator.findStringMatchEntries(CFG_FORMAT_EXTRA);
90 dateFormatsExtra = setupDateFormats(configEntriesExtra, getConfigFormats(configEntriesExtra));
91
92 String removeRegex = configurator.findStringEntry(CFG_REMOVE_REGEX, "<.+?>$|=0D$|\\(|\\)|\"|\\[|]|\\W+$|^\\W+");
93 remove = Pattern.compile(removeRegex, Pattern.DOTALL);
94
95
96 String extraTextRemoveRegex = configurator.findStringEntry(CFG_EXTRA_TEXT_REMOVE_REGEX, "((\\+|-)\\d{4}).*$");
97 extraTextRemove = Pattern.compile(extraTextRemoveRegex);
98 } catch (IOException e) {
99 throw new IllegalArgumentException("Could not configure parser!!", e);
100 }
101 }
102
103
104
105
106
107
108 public static ZoneId getTimezone() {
109 return timezone;
110 }
111
112
113
114
115
116
117
118 public static ZonedDateTime parse(final String dateString) {
119 return parse(dateString, false);
120 }
121
122
123
124
125
126
127
128
129
130 public static ZonedDateTime parse(final String dateString, boolean tryExtensiveParsing) {
131 ZonedDateTime zdt = parseToZonedDateTime(dateString, tryExtensiveParsing);
132
133 if (zdt != null || !tryExtensiveParsing) {
134 return zdt;
135 } else {
136
137 return lastDitchParsingEffort(dateString);
138 }
139 }
140
141
142
143
144
145
146
147
148 public static ZonedDateTime parse(final String dateString, final DateTimeFormatter format) {
149 return parse(dateString, Collections.singletonList(format));
150 }
151
152
153
154
155
156
157
158
159 @Nullable
160 public static ZonedDateTime parse(final String dateString, final List<DateTimeFormatter> formats) {
161 String cleanedDateString = cleanDateString(dateString);
162
163 if (StringUtils.isBlank(cleanedDateString) || CollectionUtils.isEmpty(formats)) {
164 return null;
165 }
166
167 for (DateTimeFormatter formatter : formats) {
168 if (formatter == null) {
169 continue;
170 }
171
172 try {
173
174 TemporalAccessor accessor =
175 formatter.parseBest(cleanedDateString, ZonedDateTime::from, OffsetDateTime::from, LocalDateTime::from, LocalDate::from);
176 if (accessor instanceof ZonedDateTime) {
177 return (ZonedDateTime) accessor;
178 } else if (accessor instanceof OffsetDateTime) {
179 return ((OffsetDateTime) accessor).atZoneSameInstant(timezone);
180 } else if (accessor instanceof LocalDateTime) {
181 return ((LocalDateTime) accessor).atZone(timezone);
182 } else if (accessor instanceof LocalDate) {
183 return ((LocalDate) accessor).atStartOfDay(timezone);
184 }
185
186 } catch (NullPointerException | IllegalArgumentException | DateTimeParseException e) {
187
188 logger.trace("Error parsing date {} with format {}", dateString, formatter);
189 }
190 }
191 return null;
192 }
193
194
195
196
197
198
199
200
201
202
203
204
205
206 @Nullable
207 static ZonedDateTime lastDitchParsingEffort(final String date) {
208
209
210
211 Matcher matcher = extraTextRemove.matcher(date);
212 if (matcher.find()) {
213 String secondChanceDate = matcher.replaceAll(matcher.group(1));
214
215 return parseToZonedDateTime(secondChanceDate, true);
216 }
217 return null;
218 }
219
220
221
222
223
224
225
226
227
228 private static ZonedDateTime parseToZonedDateTime(final String dateString, boolean tryExtensiveParsing) {
229 ZonedDateTime zdt = parse(dateString, dateFormatsMain);
230
231
232 if (!tryExtensiveParsing || zdt != null) {
233 return zdt;
234 }
235 zdt = parse(dateString, dateFormatsExtra);
236 return zdt;
237 }
238
239
240
241
242
243
244
245 private static ZoneId setupTimezone(final String configTimezone) {
246 try {
247 if (StringUtils.isNotBlank(configTimezone)) {
248
249 return ZoneId.of(configTimezone);
250 }
251 } catch (DateTimeException e) {
252 logger.error("Error parsing timezone {}, using default {}", configTimezone, timezone, e);
253 }
254
255 return ZoneId.of(DEFAULT_TIMEZONE);
256 }
257
258
259
260
261
262
263
264
265 private static List<DateTimeFormatter> setupDateFormats(final List<ConfigEntry> configEntries, final List<DateTimeFormatter> dateTimeFormats) {
266 List<DateTimeFormatter> dateFormats;
267 if (CollectionUtils.isNotEmpty(dateTimeFormats)) {
268 dateFormats = Collections.unmodifiableList(dateTimeFormats);
269 logger.debug("Created successfully. Created {} of {} formats from config", dateFormats.size(), configEntries.size());
270 return dateFormats;
271 } else {
272 logger.error("Could not create with configured variables");
273 throw new IllegalArgumentException("No date/time formats configured!!");
274 }
275 }
276
277
278
279
280
281
282
283 @Nullable
284 private static List<DateTimeFormatter> getConfigFormats(final List<ConfigEntry> configEntries) {
285 if (CollectionUtils.isEmpty(configEntries)) {
286 return null;
287 }
288 return configEntries.stream().map(FlexibleDateTimeParser::getFormatter).filter(Objects::nonNull).collect(toList());
289 }
290
291
292
293
294
295
296
297 @Nullable
298 private static DateTimeFormatter getFormatter(ConfigEntry entry) {
299 try {
300 return new DateTimeFormatterBuilder().parseCaseInsensitive().appendPattern(entry.getValue()).toFormatter();
301 } catch (IllegalArgumentException e) {
302
303 logger.error("Error parsing pattern [{}]: {}", entry.getValue(), e.getLocalizedMessage());
304 }
305 return null;
306 }
307
308
309
310
311
312
313
314 private static String cleanDateString(final String date) {
315 if (StringUtils.isBlank(date)) {
316 return date;
317 }
318
319
320 String cleanedDateString = StringUtils.substring(date, 0, 100);
321 cleanedDateString = REPLACE.matcher(cleanedDateString).replaceAll(SPACE);
322 cleanedDateString = remove.matcher(cleanedDateString).replaceAll(EMPTY);
323
324 return StringUtils.trimToNull(cleanedDateString);
325 }
326
327
328
329
330 private FlexibleDateTimeParser() {}
331
332 }