001package ca.uhn.fhir.util;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2020 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        /**
045         * GMT TimeZone
046         */
047        public static final TimeZone GMT = TimeZone.getTimeZone("GMT");
048
049        /**
050         * Date format pattern used to parse HTTP date headers in RFC 1123 format.
051         */
052        @SuppressWarnings("WeakerAccess")
053        public static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
054
055        /**
056         * Date format pattern used to parse HTTP date headers in RFC 1036 format.
057         */
058        @SuppressWarnings("WeakerAccess")
059        public static final String PATTERN_RFC1036 = "EEE, dd-MMM-yy HH:mm:ss zzz";
060
061        /**
062         * Date format pattern used to parse HTTP date headers in ANSI C
063         * {@code asctime()} format.
064         */
065        @SuppressWarnings("WeakerAccess")
066        public static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy";
067
068        private static final String[] DEFAULT_PATTERNS = new String[]{
069                PATTERN_RFC1123,
070                PATTERN_RFC1036,
071                PATTERN_ASCTIME
072        };
073        private static final Date DEFAULT_TWO_DIGIT_YEAR_START;
074
075        static {
076                final Calendar calendar = Calendar.getInstance();
077                calendar.setTimeZone(GMT);
078                calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
079                calendar.set(Calendar.MILLISECOND, 0);
080                DEFAULT_TWO_DIGIT_YEAR_START = calendar.getTime();
081        }
082
083        /**
084         * This class should not be instantiated.
085         */
086        private DateUtils() {
087        }
088
089        /**
090         * A factory for {@link SimpleDateFormat}s. The instances are stored in a
091         * threadlocal way because SimpleDateFormat is not thread safe as noted in
092         * {@link SimpleDateFormat its javadoc}.
093         */
094        final static class DateFormatHolder {
095
096                private static final ThreadLocal<SoftReference<Map<String, SimpleDateFormat>>> THREADLOCAL_FORMATS = ThreadLocal.withInitial(() -> new SoftReference<>(new HashMap<>()));
097
098                /**
099                 * creates a {@link SimpleDateFormat} for the requested format string.
100                 *
101                 * @param pattern a non-{@code null} format String according to
102                 *                {@link SimpleDateFormat}. The format is not checked against
103                 *                {@code null} since all paths go through
104                 *                {@link DateUtils}.
105                 * @return the requested format. This simple DateFormat should not be used
106                 * to {@link SimpleDateFormat#applyPattern(String) apply} to a
107                 * different pattern.
108                 */
109                static SimpleDateFormat formatFor(final String pattern) {
110                        final SoftReference<Map<String, SimpleDateFormat>> ref = THREADLOCAL_FORMATS.get();
111                        Map<String, SimpleDateFormat> formats = ref.get();
112                        if (formats == null) {
113                                formats = new HashMap<>();
114                                THREADLOCAL_FORMATS.set(
115                                        new SoftReference<>(formats));
116                        }
117
118                        SimpleDateFormat format = formats.get(pattern);
119                        if (format == null) {
120                                format = new SimpleDateFormat(pattern, Locale.US);
121                                format.setTimeZone(TimeZone.getTimeZone("GMT"));
122                                formats.put(pattern, format);
123                        }
124
125                        return format;
126                }
127
128        }
129
130        /**
131         * Parses a date value.  The formats used for parsing the date value are retrieved from
132         * the default http params.
133         *
134         * @param theDateValue the date value to parse
135         * @return the parsed date or null if input could not be parsed
136         */
137        public static Date parseDate(final String theDateValue) {
138                notNull(theDateValue, "Date value");
139                String v = theDateValue;
140                if (v.length() > 1 && v.startsWith("'") && v.endsWith("'")) {
141                        v = v.substring(1, v.length() - 1);
142                }
143
144                for (final String dateFormat : DEFAULT_PATTERNS) {
145                        final SimpleDateFormat dateParser = DateFormatHolder.formatFor(dateFormat);
146                        dateParser.set2DigitYearStart(DEFAULT_TWO_DIGIT_YEAR_START);
147                        final ParsePosition pos = new ParsePosition(0);
148                        final Date result = dateParser.parse(v, pos);
149                        if (pos.getIndex() != 0) {
150                                return result;
151                        }
152                }
153                return null;
154        }
155
156        /**
157         * Formats the given date according to the RFC 1123 pattern.
158         *
159         * @param date The date to format.
160         * @return An RFC 1123 formatted date string.
161         * @see #PATTERN_RFC1123
162         */
163        public static String formatDate(final Date date) {
164                notNull(date, "Date");
165                notNull(PATTERN_RFC1123, "Pattern");
166                final SimpleDateFormat formatter = DateFormatHolder.formatFor(PATTERN_RFC1123);
167                return formatter.format(date);
168        }
169
170        public static <T> T notNull(final T argument, final String name) {
171                if (argument == null) {
172                        throw new IllegalArgumentException(name + " may not be null");
173                }
174                return argument;
175        }
176
177}