001package ca.uhn.fhir.util;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2021 Smile CDR, Inc.
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.Calendar;
027import java.util.Date;
028import java.util.HashMap;
029import java.util.Locale;
030import java.util.Map;
031import java.util.TimeZone;
032
033/**
034 * A utility class for parsing and formatting HTTP dates as used in cookies and
035 * other headers.  This class handles dates as defined by RFC 2616 section
036 * 3.3.1 as well as some other common non-standard formats.
037 * <p>
038 * This class is basically intended to be a high-performance workaround
039 * for the fact that Java SimpleDateFormat is kind of expensive to
040 * create and yet isn't thread safe.
041 * </p>
042 * <p>
043 * This class was adapted from the class with the same name from the Jetty
044 * project, licensed under the terms of the Apache Software License 2.0.
045 * </p>
046 */
047public final class DateUtils {
048
049        /**
050         * GMT TimeZone
051         */
052        public static final TimeZone GMT = TimeZone.getTimeZone("GMT");
053
054        /**
055         * Date format pattern used to parse HTTP date headers in RFC 1123 format.
056         */
057        @SuppressWarnings("WeakerAccess")
058        public static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
059
060        /**
061         * Date format pattern used to parse HTTP date headers in RFC 1036 format.
062         */
063        @SuppressWarnings("WeakerAccess")
064        public static final String PATTERN_RFC1036 = "EEE, dd-MMM-yy HH:mm:ss zzz";
065
066        /**
067         * Date format pattern used to parse HTTP date headers in ANSI C
068         * {@code asctime()} format.
069         */
070        @SuppressWarnings("WeakerAccess")
071        public static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy";
072
073        private static final String PATTERN_INTEGER_DATE = "yyyyMMdd";
074
075        private static final String[] DEFAULT_PATTERNS = new String[]{
076                PATTERN_RFC1123,
077                PATTERN_RFC1036,
078                PATTERN_ASCTIME
079        };
080        private static final Date DEFAULT_TWO_DIGIT_YEAR_START;
081
082        static {
083                final Calendar calendar = Calendar.getInstance();
084                calendar.setTimeZone(GMT);
085                calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
086                calendar.set(Calendar.MILLISECOND, 0);
087                DEFAULT_TWO_DIGIT_YEAR_START = calendar.getTime();
088        }
089
090        /**
091         * This class should not be instantiated.
092         */
093        private DateUtils() {
094        }
095
096        /**
097         * A factory for {@link SimpleDateFormat}s. The instances are stored in a
098         * threadlocal way because SimpleDateFormat is not thread safe as noted in
099         * {@link SimpleDateFormat its javadoc}.
100         */
101        final static class DateFormatHolder {
102
103                private static final ThreadLocal<SoftReference<Map<String, SimpleDateFormat>>> THREADLOCAL_FORMATS = ThreadLocal.withInitial(() -> new SoftReference<>(new HashMap<>()));
104
105                /**
106                 * creates a {@link SimpleDateFormat} for the requested format string.
107                 *
108                 * @param pattern a non-{@code null} format String according to
109                 *                {@link SimpleDateFormat}. The format is not checked against
110                 *                {@code null} since all paths go through
111                 *                {@link DateUtils}.
112                 * @return the requested format. This simple DateFormat should not be used
113                 * to {@link SimpleDateFormat#applyPattern(String) apply} to a
114                 * different pattern.
115                 */
116                static SimpleDateFormat formatFor(final String pattern) {
117                        final SoftReference<Map<String, SimpleDateFormat>> ref = THREADLOCAL_FORMATS.get();
118                        Map<String, SimpleDateFormat> formats = ref.get();
119                        if (formats == null) {
120                                formats = new HashMap<>();
121                                THREADLOCAL_FORMATS.set(
122                                        new SoftReference<>(formats));
123                        }
124
125                        SimpleDateFormat format = formats.get(pattern);
126                        if (format == null) {
127                                format = new SimpleDateFormat(pattern, Locale.US);
128                                format.setTimeZone(TimeZone.getTimeZone("GMT"));
129                                formats.put(pattern, format);
130                        }
131
132                        return format;
133                }
134
135        }
136
137        /**
138         * Parses a date value.  The formats used for parsing the date value are retrieved from
139         * the default http params.
140         *
141         * @param theDateValue the date value to parse
142         * @return the parsed date or null if input could not be parsed
143         */
144        public static Date parseDate(final String theDateValue) {
145                notNull(theDateValue, "Date value");
146                String v = theDateValue;
147                if (v.length() > 1 && v.startsWith("'") && v.endsWith("'")) {
148                        v = v.substring(1, v.length() - 1);
149                }
150
151                for (final String dateFormat : DEFAULT_PATTERNS) {
152                        final SimpleDateFormat dateParser = DateFormatHolder.formatFor(dateFormat);
153                        dateParser.set2DigitYearStart(DEFAULT_TWO_DIGIT_YEAR_START);
154                        final ParsePosition pos = new ParsePosition(0);
155                        final Date result = dateParser.parse(v, pos);
156                        if (pos.getIndex() != 0) {
157                                return result;
158                        }
159                }
160                return null;
161        }
162
163        public static Date getHighestInstantFromDate(Date theDateValue) {
164                Calendar sourceCal = Calendar.getInstance();
165                sourceCal.setTime(theDateValue);
166
167                Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT-12:00"));
168                copyDateAndTrundateTime(sourceCal, cal);
169                return cal.getTime();
170        }
171
172        public static Date getLowestInstantFromDate(Date theDateValue) {
173                Calendar sourceCal = Calendar.getInstance();
174                sourceCal.setTime(theDateValue);
175
176                Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT+14:00"));
177                copyDateAndTrundateTime(sourceCal, cal);
178                return cal.getTime();
179        }
180
181        private static void copyDateAndTrundateTime(Calendar theSourceCal, Calendar theCal) {
182                theCal.set(Calendar.YEAR, theSourceCal.get(Calendar.YEAR));
183                theCal.set(Calendar.MONTH, theSourceCal.get(Calendar.MONTH));
184                theCal.set(Calendar.DAY_OF_MONTH, theSourceCal.get(Calendar.DAY_OF_MONTH));
185                theCal.set(Calendar.HOUR_OF_DAY, 0);
186                theCal.set(Calendar.MINUTE, 0);
187                theCal.set(Calendar.SECOND, 0);
188                theCal.set(Calendar.MILLISECOND, 0);
189        }
190
191        public static int convertDateToDayInteger(final Date theDateValue) {
192                notNull(theDateValue, "Date value");
193                SimpleDateFormat format = new SimpleDateFormat(PATTERN_INTEGER_DATE);
194                String theDateString = format.format(theDateValue);
195                return Integer.parseInt(theDateString);
196        }
197
198        public static String convertDateToIso8601String(final Date theDateValue) {
199                notNull(theDateValue, "Date value");
200                SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
201                return format.format(theDateValue);
202        }
203
204        /**
205         * Formats the given date according to the RFC 1123 pattern.
206         *
207         * @param date The date to format.
208         * @return An RFC 1123 formatted date string.
209         * @see #PATTERN_RFC1123
210         */
211        public static String formatDate(final Date date) {
212                notNull(date, "Date");
213                notNull(PATTERN_RFC1123, "Pattern");
214                final SimpleDateFormat formatter = DateFormatHolder.formatFor(PATTERN_RFC1123);
215                return formatter.format(date);
216        }
217
218        public static <T> T notNull(final T argument, final String name) {
219                if (argument == null) {
220                        throw new IllegalArgumentException(name + " may not be null");
221                }
222                return argument;
223        }
224
225}