1 /*
2 * ====================================================================
3 * Licensed to the Apache Software Foundation (ASF) under one
4 * or more contributor license agreements. See the NOTICE file
5 * distributed with this work for additional information
6 * regarding copyright ownership. The ASF licenses this file
7 * to you under the Apache License, Version 2.0 (the
8 * "License"); you may not use this file except in compliance
9 * with the License. You may obtain a copy of the License at
10 *
11 * http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing,
14 * software distributed under the License is distributed on an
15 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 * KIND, either express or implied. See the License for the
17 * specific language governing permissions and limitations
18 * under the License.
19 * ====================================================================
20 *
21 * This software consists of voluntary contributions made by many
22 * individuals on behalf of the Apache Software Foundation. For more
23 * information on the Apache Software Foundation, please see
24 * <http://www.apache.org/>.
25 *
26 */
27
28 package org.apache.hc.client5.http.utils;
29
30 import java.time.DateTimeException;
31 import java.time.Instant;
32 import java.time.LocalDateTime;
33 import java.time.ZoneId;
34 import java.time.ZoneOffset;
35 import java.time.format.DateTimeFormatter;
36 import java.time.format.DateTimeFormatterBuilder;
37 import java.util.Date;
38 import java.util.Locale;
39 import java.util.TimeZone;
40
41 import org.apache.hc.core5.http.Header;
42 import org.apache.hc.core5.http.MessageHeaders;
43 import org.apache.hc.core5.util.Args;
44
45 /**
46 * A utility class for parsing and formatting HTTP dates as used in cookies and
47 * other headers.
48 *
49 * @since 4.3
50 */
51 public final class DateUtils {
52
53 /**
54 * @deprecated use {@link #INTERNET_MESSAGE_FORMAT}
55 */
56 @Deprecated
57 public static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
58 public static final String INTERNET_MESSAGE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
59
60 /**
61 * Date formatter used to parse HTTP date headers in the Internet Message Format
62 * specified by the HTTP protocol.
63 *
64 * @since 5.2
65 */
66 public static final DateTimeFormatter FORMATTER_RFC1123 = new DateTimeFormatterBuilder()
67 .parseLenient()
68 .parseCaseInsensitive()
69 .appendPattern(INTERNET_MESSAGE_FORMAT)
70 .toFormatter(Locale.ENGLISH);
71
72 /**
73 * Date format pattern used to parse HTTP date headers in RFC 1036 format.
74 */
75 public static final String PATTERN_RFC1036 = "EEE, dd-MMM-yy HH:mm:ss zzz";
76
77 /**
78 * Date formatter used to parse HTTP date headers in RFC 1036 format.
79 *
80 * @since 5.2
81 */
82 public static final DateTimeFormatter FORMATTER_RFC1036 = new DateTimeFormatterBuilder()
83 .parseLenient()
84 .parseCaseInsensitive()
85 .appendPattern(PATTERN_RFC1036)
86 .toFormatter(Locale.ENGLISH);
87
88 /**
89 * Date format pattern used to parse HTTP date headers in ANSI C
90 * {@code asctime()} format.
91 */
92 public static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy";
93
94 /**
95 * Date formatter used to parse HTTP date headers in in ANSI C {@code asctime()} format.
96 *
97 * @since 5.2
98 */
99 public static final DateTimeFormatter FORMATTER_ASCTIME = new DateTimeFormatterBuilder()
100 .parseLenient()
101 .parseCaseInsensitive()
102 .appendPattern(PATTERN_ASCTIME)
103 .toFormatter(Locale.ENGLISH);
104
105 /**
106 * Standard date formatters: {@link #FORMATTER_RFC1123}, {@link #FORMATTER_RFC1036}, {@link #FORMATTER_ASCTIME}.
107 *
108 * @since 5.2
109 */
110 public static final DateTimeFormatter[] STANDARD_PATTERNS = new DateTimeFormatter[] {
111 FORMATTER_RFC1123,
112 FORMATTER_RFC1036,
113 FORMATTER_ASCTIME
114 };
115
116 static final ZoneId GMT_ID = ZoneId.of("GMT");
117
118 /**
119 * @since 5.2
120 */
121 public static Date toDate(final Instant instant) {
122 return instant != null ? new Date(instant.toEpochMilli()) : null;
123 }
124
125 /**
126 * @since 5.2
127 */
128 public static Instant toInstant(final Date date) {
129 return date != null ? Instant.ofEpochMilli(date.getTime()) : null;
130 }
131
132 /**
133 * @since 5.2
134 */
135 public static LocalDateTime toUTC(final Instant instant) {
136 return instant != null ? instant.atZone(ZoneOffset.UTC).toLocalDateTime() : null;
137 }
138
139 /**
140 * @since 5.2
141 */
142 public static LocalDateTime toUTC(final Date date) {
143 return toUTC(toInstant(date));
144 }
145
146 /**
147 * Parses the date value using the given date/time formats.
148 * <p>This method can handle strings without time-zone information by failing gracefully, in which case
149 * it returns {@code null}.</p>
150 *
151 * @param dateValue the instant value to parse
152 * @param dateFormatters the date/time formats to use
153 *
154 * @return the parsed instant or null if input could not be parsed
155 *
156 * @since 5.2
157 */
158 public static Instant parseDate(final String dateValue, final DateTimeFormatter... dateFormatters) {
159 Args.notNull(dateValue, "Date value");
160 String v = dateValue;
161 // trim single quotes around date if present
162 // see issue #5279
163 if (v.length() > 1 && v.startsWith("'") && v.endsWith("'")) {
164 v = v.substring (1, v.length() - 1);
165 }
166
167 for (final DateTimeFormatter dateFormatter : dateFormatters) {
168 try {
169 return Instant.from(dateFormatter.parse(v));
170 } catch (final DateTimeException ignore) {
171 }
172 }
173 return null;
174 }
175
176 /**
177 * Parses the instant value using the standard date/time formats ({@link #PATTERN_RFC1123},
178 * {@link #PATTERN_RFC1036}, {@link #PATTERN_ASCTIME}).
179 *
180 * @param dateValue the instant value to parse
181 *
182 * @return the parsed instant or null if input could not be parsed
183 *
184 * @since 5.2
185 */
186 public static Instant parseStandardDate(final String dateValue) {
187 return parseDate(dateValue, STANDARD_PATTERNS);
188 }
189
190 /**
191 * Parses an instant value from a header with the given name.
192 *
193 * @param headers message headers
194 * @param headerName header name
195 *
196 * @return the parsed instant or null if input could not be parsed
197 *
198 * @since 5.2
199 */
200 public static Instant parseStandardDate(final MessageHeaders headers, final String headerName) {
201 if (headers == null) {
202 return null;
203 }
204 final Header header = headers.getFirstHeader(headerName);
205 if (header == null) {
206 return null;
207 }
208 return parseStandardDate(header.getValue());
209 }
210
211 /**
212 * Formats the given instant according to the RFC 1123 pattern.
213 *
214 * @param instant Instant to format.
215 * @return An RFC 1123 formatted instant string.
216 *
217 * @see #PATTERN_RFC1123
218 *
219 * @since 5.2
220 */
221 public static String formatStandardDate(final Instant instant) {
222 return formatDate(instant, FORMATTER_RFC1123);
223 }
224
225 /**
226 * Formats the given date according to the specified pattern.
227 *
228 * @param instant Instant to format.
229 * @param dateTimeFormatter The pattern to use for formatting the instant.
230 * @return A formatted instant string.
231 *
232 * @throws IllegalArgumentException If the given date pattern is invalid.
233 *
234 * @since 5.2
235 */
236 public static String formatDate(final Instant instant, final DateTimeFormatter dateTimeFormatter) {
237 Args.notNull(instant, "Instant");
238 Args.notNull(dateTimeFormatter, "DateTimeFormatter");
239 return dateTimeFormatter.format(instant.atZone(GMT_ID));
240 }
241
242 /**
243 * @deprecated This attribute is no longer supported as a part of the public API.
244 */
245 @Deprecated
246 public static final TimeZone GMT = TimeZone.getTimeZone("GMT");
247
248 /**
249 * Parses a date value. The formats used for parsing the date value are retrieved from
250 * the default http params.
251 *
252 * @param dateValue the date value to parse
253 *
254 * @return the parsed date or null if input could not be parsed
255 *
256 * @deprecated Use {@link #parseStandardDate(String)}
257 */
258 @Deprecated
259 public static Date parseDate(final String dateValue) {
260 return parseDate(dateValue, null, null);
261 }
262
263 /**
264 * Parses a date value from a header with the given name.
265 *
266 * @param headers message headers
267 * @param headerName header name
268 *
269 * @return the parsed date or null if input could not be parsed
270 *
271 * @since 5.0
272 *
273 * @deprecated Use {@link #parseStandardDate(MessageHeaders, String)}
274 */
275 @Deprecated
276 public static Date parseDate(final MessageHeaders headers, final String headerName) {
277 return toDate(parseStandardDate(headers, headerName));
278 }
279
280 /**
281 * Tests if the first message is after (newer) than second one
282 * using the given message header for comparison.
283 *
284 * @param message1 the first message
285 * @param message2 the second message
286 * @param headerName header name
287 *
288 * @return {@code true} if both messages contain a header with the given name
289 * and the value of the header from the first message is newer that of
290 * the second message.
291 *
292 * @since 5.0
293 *
294 * @deprecated This method is no longer supported as a part of the public API.
295 */
296 @Deprecated
297 public static boolean isAfter(
298 final MessageHeaders message1,
299 final MessageHeaders message2,
300 final String headerName) {
301 if (message1 != null && message2 != null) {
302 final Header dateHeader1 = message1.getFirstHeader(headerName);
303 if (dateHeader1 != null) {
304 final Header dateHeader2 = message2.getFirstHeader(headerName);
305 if (dateHeader2 != null) {
306 final Date date1 = parseDate(dateHeader1.getValue());
307 if (date1 != null) {
308 final Date date2 = parseDate(dateHeader2.getValue());
309 if (date2 != null) {
310 return date1.after(date2);
311 }
312 }
313 }
314 }
315 }
316 return false;
317 }
318
319 /**
320 * Tests if the first message is before (older) than the second one
321 * using the given message header for comparison.
322 *
323 * @param message1 the first message
324 * @param message2 the second message
325 * @param headerName header name
326 *
327 * @return {@code true} if both messages contain a header with the given name
328 * and the value of the header from the first message is older that of
329 * the second message.
330 *
331 * @since 5.0
332 *
333 * @deprecated This method is no longer supported as a part of the public API.
334 */
335 @Deprecated
336 public static boolean isBefore(
337 final MessageHeaders message1,
338 final MessageHeaders message2,
339 final String headerName) {
340 if (message1 != null && message2 != null) {
341 final Header dateHeader1 = message1.getFirstHeader(headerName);
342 if (dateHeader1 != null) {
343 final Header dateHeader2 = message2.getFirstHeader(headerName);
344 if (dateHeader2 != null) {
345 final Date date1 = parseDate(dateHeader1.getValue());
346 if (date1 != null) {
347 final Date date2 = parseDate(dateHeader2.getValue());
348 if (date2 != null) {
349 return date1.before(date2);
350 }
351 }
352 }
353 }
354 }
355 return false;
356 }
357
358 /**
359 * Parses the date value using the given date/time formats.
360 *
361 * @param dateValue the date value to parse
362 * @param dateFormats the date/time formats to use
363 *
364 * @return the parsed date or null if input could not be parsed
365 *
366 * @deprecated Use {@link #parseDate(String, DateTimeFormatter...)}
367 */
368 @Deprecated
369 public static Date parseDate(final String dateValue, final String[] dateFormats) {
370 return parseDate(dateValue, dateFormats, null);
371 }
372
373 /**
374 * Parses the date value using the given date/time formats.
375 *
376 * @param dateValue the date value to parse
377 * @param dateFormats the date/time formats to use
378 * @param startDate During parsing, two digit years will be placed in the range
379 * {@code startDate} to {@code startDate + 100 years}. This value may
380 * be {@code null}. When {@code null} is given as a parameter, year
381 * {@code 2000} will be used.
382 *
383 * @return the parsed date or null if input could not be parsed
384 *
385 * @deprecated Use {@link #parseDate(String, DateTimeFormatter...)}
386 */
387 @Deprecated
388 public static Date parseDate(
389 final String dateValue,
390 final String[] dateFormats,
391 final Date startDate) {
392 final DateTimeFormatter[] dateTimeFormatters;
393 if (dateFormats != null) {
394 dateTimeFormatters = new DateTimeFormatter[dateFormats.length];
395 for (int i = 0; i < dateFormats.length; i++) {
396 dateTimeFormatters[i] = new DateTimeFormatterBuilder()
397 .parseLenient()
398 .parseCaseInsensitive()
399 .appendPattern(dateFormats[i])
400 .toFormatter();
401 }
402 } else {
403 dateTimeFormatters = STANDARD_PATTERNS;
404 }
405 return toDate(parseDate(dateValue, dateTimeFormatters));
406 }
407
408 /**
409 * Formats the given date according to the RFC 1123 pattern.
410 *
411 * @param date The date to format.
412 * @return An RFC 1123 formatted date string.
413 *
414 * @see #PATTERN_RFC1123
415 *
416 * @deprecated Use {@link #formatStandardDate(Instant)}
417 */
418 @Deprecated
419 public static String formatDate(final Date date) {
420 return formatStandardDate(toInstant(date));
421 }
422
423 /**
424 * Formats the given date according to the specified pattern.
425 *
426 * @param date The date to format.
427 * @param pattern The pattern to use for formatting the date.
428 * @return A formatted date string.
429 *
430 * @throws IllegalArgumentException If the given date pattern is invalid.
431 *
432 * @deprecated Use {@link #formatDate(Instant, DateTimeFormatter)}
433 */
434 @Deprecated
435 public static String formatDate(final Date date, final String pattern) {
436 Args.notNull(date, "Date");
437 Args.notNull(pattern, "Pattern");
438 return DateTimeFormatter.ofPattern(pattern).format(toInstant(date).atZone(GMT_ID));
439 }
440
441 /**
442 * Clears thread-local variable containing {@link java.text.DateFormat} cache.
443 *
444 * @since 4.3
445 *
446 * @deprecated Noop method. Do not use.
447 */
448 @Deprecated
449 public static void clearThreadLocal() {
450 }
451
452 /** This class should not be instantiated. */
453 private DateUtils() {
454 }
455
456 }