001package ca.uhn.fhir.util;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2019 University Health Network
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import java.lang.ref.SoftReference;
024import java.text.ParsePosition;
025import java.text.SimpleDateFormat;
026import java.util.*;
027
028/**
029 * A utility class for parsing and formatting HTTP dates as used in cookies and
030 * other headers.  This class handles dates as defined by RFC 2616 section
031 * 3.3.1 as well as some other common non-standard formats.
032 * <p>
033 * This class is basically intended to be a high-performance workaround
034 * for the fact that Java SimpleDateFormat is kind of expensive to
035 * create and yet isn't thread safe.
036 * </p>
037 * <p>
038 * This class was adapted from the class with the same name from the Jetty
039 * project, licensed under the terms of the Apache Software License 2.0.
040 * </p>
041 */
042public final class DateUtils {
043
044        public static final TimeZone GMT = TimeZone.getTimeZone("GMT");
045        /**
046         * Date format pattern used to parse HTTP date headers in RFC 1123 format.
047         */
048        private static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
049        /**
050         * Date format pattern used to parse HTTP date headers in RFC 1036 format.
051         */
052        private static final String PATTERN_RFC1036 = "EEE, dd-MMM-yy HH:mm:ss zzz";
053        /**
054         * Date format pattern used to parse HTTP date headers in ANSI C
055         * {@code asctime()} format.
056         */
057        private static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy";
058        private static final String[] DEFAULT_PATTERNS = new String[]{
059                PATTERN_RFC1123,
060                PATTERN_RFC1036,
061                PATTERN_ASCTIME
062        };
063        private static final Date DEFAULT_TWO_DIGIT_YEAR_START;
064
065        static {
066                final Calendar calendar = Calendar.getInstance();
067                calendar.setTimeZone(GMT);
068                calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
069                calendar.set(Calendar.MILLISECOND, 0);
070                DEFAULT_TWO_DIGIT_YEAR_START = calendar.getTime();
071        }
072
073        /**
074         * This class should not be instantiated.
075         */
076        private DateUtils() {
077        }
078
079        /**
080         * A factory for {@link SimpleDateFormat}s. The instances are stored in a
081         * threadlocal way because SimpleDateFormat is not thread safe as noted in
082         * {@link SimpleDateFormat its javadoc}.
083         */
084        final static class DateFormatHolder {
085
086                private static final ThreadLocal<SoftReference<Map<String, SimpleDateFormat>>> THREADLOCAL_FORMATS = ThreadLocal.withInitial(() -> new SoftReference<>(new HashMap<>()));
087
088                /**
089                 * creates a {@link SimpleDateFormat} for the requested format string.
090                 *
091                 * @param pattern a non-{@code null} format String according to
092                 *                {@link SimpleDateFormat}. The format is not checked against
093                 *                {@code null} since all paths go through
094                 *                {@link DateUtils}.
095                 * @return the requested format. This simple DateFormat should not be used
096                 * to {@link SimpleDateFormat#applyPattern(String) apply} to a
097                 * different pattern.
098                 */
099                static SimpleDateFormat formatFor(final String pattern) {
100                        final SoftReference<Map<String, SimpleDateFormat>> ref = THREADLOCAL_FORMATS.get();
101                        Map<String, SimpleDateFormat> formats = ref.get();
102                        if (formats == null) {
103                                formats = new HashMap<>();
104                                THREADLOCAL_FORMATS.set(
105                                        new SoftReference<>(formats));
106                        }
107
108                        SimpleDateFormat format = formats.get(pattern);
109                        if (format == null) {
110                                format = new SimpleDateFormat(pattern, Locale.US);
111                                format.setTimeZone(TimeZone.getTimeZone("GMT"));
112                                formats.put(pattern, format);
113                        }
114
115                        return format;
116                }
117
118        }
119
120        /**
121         * Parses a date value.  The formats used for parsing the date value are retrieved from
122         * the default http params.
123         *
124         * @param theDateValue the date value to parse
125         * @return the parsed date or null if input could not be parsed
126         */
127        public static Date parseDate(final String theDateValue) {
128                notNull(theDateValue, "Date value");
129                String v = theDateValue;
130                if (v.length() > 1 && v.startsWith("'") && v.endsWith("'")) {
131                        v = v.substring(1, v.length() - 1);
132                }
133
134                for (final String dateFormat : DEFAULT_PATTERNS) {
135                        final SimpleDateFormat dateParser = DateFormatHolder.formatFor(dateFormat);
136                        dateParser.set2DigitYearStart(DEFAULT_TWO_DIGIT_YEAR_START);
137                        final ParsePosition pos = new ParsePosition(0);
138                        final Date result = dateParser.parse(v, pos);
139                        if (pos.getIndex() != 0) {
140                                return result;
141                        }
142                }
143                return null;
144        }
145
146        /**
147         * Formats the given date according to the RFC 1123 pattern.
148         *
149         * @param date The date to format.
150         * @return An RFC 1123 formatted date string.
151         * @see #PATTERN_RFC1123
152         */
153        public static String formatDate(final Date date) {
154                notNull(date, "Date");
155                notNull(PATTERN_RFC1123, "Pattern");
156                final SimpleDateFormat formatter = DateFormatHolder.formatFor(PATTERN_RFC1123);
157                return formatter.format(date);
158        }
159
160        public static <T> T notNull(final T argument, final String name) {
161                if (argument == null) {
162                        throw new IllegalArgumentException(name + " may not be null");
163                }
164                return argument;
165        }
166
167}