| // *************************************************************************************************************************** |
| // * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * |
| // * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * |
| // * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * |
| // * with the License. You may obtain a copy of the License at * |
| // * * |
| // * http://www.apache.org/licenses/LICENSE-2.0 * |
| // * * |
| // * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * |
| // * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * |
| // * specific language governing permissions and limitations under the License. * |
| // *************************************************************************************************************************** |
| package org.apache.juneau.internal; |
| |
| import static org.apache.juneau.internal.StringUtils.*; |
| |
| import java.lang.ref.*; |
| import java.text.*; |
| import java.time.format.*; |
| import java.util.*; |
| |
| import javax.xml.bind.*; |
| |
| import org.apache.juneau.reflect.*; |
| |
| /** |
| * A utility class for parsing and formatting HTTP dates as used in cookies and other headers. |
| * |
| * <p> |
| * This class handles dates as defined by RFC 2616 section 3.3.1 as well as some other common non-standard formats. |
| * |
| * <p> |
| * This class was copied from HttpClient 4.3. |
| */ |
| public final class DateUtils { |
| |
| /** |
| * Date format pattern used to parse HTTP date headers in RFC 1123 format. |
| */ |
| public static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz"; |
| |
| /** |
| * Date format pattern used to parse HTTP date headers in RFC 1036 format. |
| */ |
| public static final String PATTERN_RFC1036 = "EEE, dd-MMM-yy HH:mm:ss zzz"; |
| |
| /** |
| * Date format pattern used to parse HTTP date headers in ANSI C <c>asctime()</c> format. |
| */ |
| public static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy"; |
| private static final String[] DEFAULT_PATTERNS = new String[] { PATTERN_RFC1123, PATTERN_RFC1036, PATTERN_ASCTIME }; |
| private static final Date DEFAULT_TWO_DIGIT_YEAR_START; |
| private static final TimeZone GMT = TimeZone.getTimeZone("GMT"); |
| static { |
| final Calendar calendar = Calendar.getInstance(); |
| calendar.setTimeZone(GMT); |
| calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0); |
| calendar.set(Calendar.MILLISECOND, 0); |
| DEFAULT_TWO_DIGIT_YEAR_START = calendar.getTime(); |
| } |
| |
| /** |
| * Parses a date value. |
| * |
| * <p> |
| * The formats used for parsing the date value are retrieved from the default http params. |
| * |
| * @param dateValue the date value to parse |
| * @return the parsed date or null if input could not be parsed |
| */ |
| public static Date parseDate(final String dateValue) { |
| return parseDate(dateValue, null, null); |
| } |
| |
| /** |
| * Parses the date value using the given date formats. |
| * |
| * @param dateValue the date value to parse |
| * @param dateFormats the date formats to use |
| * @return the parsed date or null if input could not be parsed |
| */ |
| public static Date parseDate(final String dateValue, final String[] dateFormats) { |
| return parseDate(dateValue, dateFormats, null); |
| } |
| |
| /** |
| * Parses the date value using the given date formats. |
| * |
| * @param dateValue the date value to parse |
| * @param dateFormats the date formats to use |
| * @param startDate |
| * During parsing, two digit years will be placed in the range <c>startDate</c> to |
| * <c>startDate + 100 years</c>. This value may be <c>null</c>. When |
| * <c>null</c> is given as a parameter, year <c>2000</c> will be used. |
| * @return the parsed date or null if input could not be parsed |
| */ |
| public static Date parseDate(final String dateValue, final String[] dateFormats, final Date startDate) { |
| final String[] localDateFormats = dateFormats != null ? dateFormats : DEFAULT_PATTERNS; |
| final Date localStartDate = startDate != null ? startDate : DEFAULT_TWO_DIGIT_YEAR_START; |
| String v = dateValue; |
| // trim single quotes around date if present |
| // see issue #5279 |
| if (v.length() > 1 && v.startsWith("'") && v.endsWith("'")) { |
| v = v.substring(1, v.length() - 1); |
| } |
| for (final String dateFormat : localDateFormats) { |
| final SimpleDateFormat dateParser = DateFormatHolder.formatFor(dateFormat); |
| dateParser.set2DigitYearStart(localStartDate); |
| final ParsePosition pos = new ParsePosition(0); |
| final Date result = dateParser.parse(v, pos); |
| if (pos.getIndex() != 0) { |
| return result; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Parses an ISO8601 string and converts it to a {@link Calendar}. |
| * |
| * @param s The string to parse. |
| * @return The parsed value, or <jk>null</jk> if the string was <jk>null</jk> or empty. |
| */ |
| public static Calendar parseISO8601Calendar(String s) { |
| if (isEmpty(s)) |
| return null; |
| return DatatypeConverter.parseDateTime(toValidISO8601DT(s)); |
| } |
| |
| /** |
| * Parses an ISO8601 string and converts it to a {@link Date}. |
| * |
| * @param s The string to parse. |
| * @return The parsed value, or <jk>null</jk> if the string was <jk>null</jk> or empty. |
| */ |
| public static Date parseISO8601(String s) { |
| if (isEmpty(s)) |
| return null; |
| return DatatypeConverter.parseDateTime(toValidISO8601DT(s)).getTime(); |
| } |
| |
| /** |
| * Formats the given date according to the RFC 1123 pattern. |
| * |
| * @param date The date to format. |
| * @return An RFC 1123 formatted date string. |
| * @see #PATTERN_RFC1123 |
| */ |
| public static String formatDate(final Date date) { |
| return formatDate(date, PATTERN_RFC1123); |
| } |
| |
| /** |
| * Formats the given date according to the specified pattern. |
| * |
| * <p> |
| * The pattern must conform to that used by the {@link SimpleDateFormat simple date format} class. |
| * |
| * @param date The date to format. |
| * @param pattern The pattern to use for formatting the date. |
| * @return A formatted date string. |
| * @throws IllegalArgumentException If the given date pattern is invalid. |
| * @see SimpleDateFormat |
| */ |
| public static String formatDate(final Date date, final String pattern) { |
| final SimpleDateFormat formatter = DateFormatHolder.formatFor(pattern); |
| return formatter.format(date); |
| } |
| |
| /** |
| * Clears thread-local variable containing {@link java.text.DateFormat} cache. |
| */ |
| public static void clearThreadLocal() { |
| DateFormatHolder.clearThreadLocal(); |
| } |
| |
| /** |
| * A factory for {@link SimpleDateFormat}s. |
| * |
| * <p> |
| * The instances are stored in a thread-local way because SimpleDateFormat is not thread-safe as noted in |
| * {@link SimpleDateFormat its javadoc}. |
| */ |
| static final class DateFormatHolder { |
| private static final ThreadLocal<SoftReference<Map<String,SimpleDateFormat>>> THREADLOCAL_FORMATS = |
| new ThreadLocal<SoftReference<Map<String,SimpleDateFormat>>>() { |
| @Override |
| protected SoftReference<Map<String,SimpleDateFormat>> initialValue() { |
| Map<String,SimpleDateFormat> m = new HashMap<>(); |
| return new SoftReference<>(m); |
| } |
| }; |
| |
| /** |
| * Creates a {@link SimpleDateFormat} for the requested format string. |
| * |
| * @param pattern |
| * A non-<c>null</c> format String according to {@link SimpleDateFormat}. |
| * The format is not checked against <c>null</c> since all paths go through {@link DateUtils}. |
| * @return |
| * The requested format. |
| * This simple date-format should not be used to {@link SimpleDateFormat#applyPattern(String) apply} to a |
| * different pattern. |
| */ |
| public static SimpleDateFormat formatFor(final String pattern) { |
| final SoftReference<Map<String,SimpleDateFormat>> ref = THREADLOCAL_FORMATS.get(); |
| Map<String,SimpleDateFormat> formats = ref.get(); |
| if (formats == null) { |
| formats = new HashMap<>(); |
| THREADLOCAL_FORMATS.set(new SoftReference<>(formats)); |
| } |
| SimpleDateFormat format = formats.get(pattern); |
| if (format == null) { |
| format = new SimpleDateFormat(pattern, Locale.US); |
| format.setTimeZone(TimeZone.getTimeZone("GMT")); |
| formats.put(pattern, format); |
| } |
| return format; |
| } |
| |
| public static void clearThreadLocal() { |
| THREADLOCAL_FORMATS.remove(); |
| } |
| } |
| |
| /** |
| * Pads out an ISO8601 string so that it can be parsed using {@link DatatypeConverter#parseDateTime(String)}. |
| * |
| * <ul> |
| * <li><js>"2001-07-04T15:30:45-05:00"</js> --> <js>"2001-07-04T15:30:45-05:00"</js> |
| * <li><js>"2001-07-04T15:30:45Z"</js> --> <js>"2001-07-04T15:30:45Z"</js> |
| * <li><js>"2001-07-04T15:30:45.1Z"</js> --> <js>"2001-07-04T15:30:45.1Z"</js> |
| * <li><js>"2001-07-04T15:30Z"</js> --> <js>"2001-07-04T15:30:00Z"</js> |
| * <li><js>"2001-07-04T15:30"</js> --> <js>"2001-07-04T15:30:00"</js> |
| * <li><js>"2001-07-04"</js> --> <li><js>"2001-07-04T00:00:00"</js> |
| * <li><js>"2001-07"</js> --> <js>"2001-07-01T00:00:00"</js> |
| * <li><js>"2001"</js> --> <js>"2001-01-01T00:00:00"</js> |
| * </ul> |
| * |
| * @param in The string to pad. |
| * @return The padded string. |
| */ |
| public static final String toValidISO8601DT(String in) { |
| |
| // "2001-07-04T15:30:45Z" |
| final int |
| S1 = 1, // Looking for - |
| S2 = 2, // Found -, looking for - |
| S3 = 3, // Found -, looking for T |
| S4 = 4, // Found T, looking for : |
| S5 = 5, // Found :, looking for : |
| S6 = 6; // Found : |
| |
| int state = 1; |
| boolean needsT = false; |
| for (int i = 0; i < in.length(); i++) { |
| char c = in.charAt(i); |
| if (state == S1) { |
| if (c == '-') |
| state = S2; |
| } else if (state == S2) { |
| if (c == '-') |
| state = S3; |
| } else if (state == S3) { |
| if (c == 'T') |
| state = S4; |
| if (c == ' ') { |
| state = S4; |
| needsT = true; |
| } |
| } else if (state == S4) { |
| if (c == ':') |
| state = S5; |
| } else if (state == S5) { |
| if (c == ':') |
| state = S6; |
| } |
| } |
| |
| if (needsT) |
| in = in.replace(' ', 'T'); |
| switch(state) { |
| case S1: return in + "-01-01T00:00:00"; |
| case S2: return in + "-01T00:00:00"; |
| case S3: return in + "T00:00:00"; |
| case S4: return in + ":00:00"; |
| case S5: return in + ":00"; |
| default: return in; |
| } |
| } |
| |
| /** |
| * Returns a {@link DateTimeFormatter} using either a pattern or predefined pattern name. |
| * |
| * @param pattern The pattern (e.g. <js>"yyyy-MM-dd"</js>) or pattern name (e.g. <js>"ISO_INSTANT"</js>). |
| * @return The formatter. |
| */ |
| public static DateTimeFormatter getFormatter(String pattern) { |
| if (isEmpty(pattern)) |
| return DateTimeFormatter.ISO_INSTANT; |
| try { |
| FieldInfo fi = ClassInfo.of(DateTimeFormatter.class).getStaticPublicField(pattern); |
| if (fi != null) |
| return (DateTimeFormatter)fi.inner().get(null); |
| return DateTimeFormatter.ofPattern(pattern); |
| } catch (IllegalArgumentException | IllegalAccessException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| } |