001/*
002 * #%L
003 * HAPI FHIR - Core Library
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.util;
021
022import ca.uhn.fhir.i18n.Msg;
023import org.apache.commons.lang3.StringUtils;
024import org.apache.commons.lang3.tuple.ImmutablePair;
025import org.apache.commons.lang3.tuple.Pair;
026
027import java.lang.ref.SoftReference;
028import java.text.ParseException;
029import java.text.ParsePosition;
030import java.text.SimpleDateFormat;
031import java.util.Calendar;
032import java.util.Date;
033import java.util.HashMap;
034import java.util.Locale;
035import java.util.Map;
036import java.util.TimeZone;
037
038/**
039 * A utility class for parsing and formatting HTTP dates as used in cookies and
040 * other headers.  This class handles dates as defined by RFC 2616 section
041 * 3.3.1 as well as some other common non-standard formats.
042 * <p>
043 * This class is basically intended to be a high-performance workaround
044 * for the fact that Java SimpleDateFormat is kind of expensive to
045 * create and yet isn't thread safe.
046 * </p>
047 * <p>
048 * This class was adapted from the class with the same name from the Jetty
049 * project, licensed under the terms of the Apache Software License 2.0.
050 * </p>
051 */
052public final class DateUtils {
053
054        /**
055         * GMT TimeZone
056         */
057        public static final TimeZone GMT = TimeZone.getTimeZone("GMT");
058
059        /**
060         * Date format pattern used to parse HTTP date headers in RFC 1123 format.
061         */
062        @SuppressWarnings("WeakerAccess")
063        public static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
064
065        /**
066         * Date format pattern used to parse HTTP date headers in RFC 1036 format.
067         */
068        @SuppressWarnings("WeakerAccess")
069        public static final String PATTERN_RFC1036 = "EEE, dd-MMM-yy HH:mm:ss zzz";
070
071        /**
072         * Date format pattern used to parse HTTP date headers in ANSI C
073         * {@code asctime()} format.
074         */
075        @SuppressWarnings("WeakerAccess")
076        public static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy";
077
078        private static final String PATTERN_INTEGER_DATE = "yyyyMMdd";
079
080        private static final String[] DEFAULT_PATTERNS = new String[] {PATTERN_RFC1123, PATTERN_RFC1036, PATTERN_ASCTIME};
081        private static final Date DEFAULT_TWO_DIGIT_YEAR_START;
082
083        static {
084                final Calendar calendar = Calendar.getInstance();
085                calendar.setTimeZone(GMT);
086                calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
087                calendar.set(Calendar.MILLISECOND, 0);
088                DEFAULT_TWO_DIGIT_YEAR_START = calendar.getTime();
089        }
090
091        /**
092         * This class should not be instantiated.
093         */
094        private DateUtils() {}
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        static final class DateFormatHolder {
102
103                private static final ThreadLocal<SoftReference<Map<String, SimpleDateFormat>>> THREADLOCAL_FORMATS =
104                                ThreadLocal.withInitial(() -> new SoftReference<>(new HashMap<>()));
105
106                /**
107                 * creates a {@link SimpleDateFormat} for the requested format string.
108                 *
109                 * @param pattern a non-{@code null} format String according to
110                 *                {@link SimpleDateFormat}. The format is not checked against
111                 *                {@code null} since all paths go through
112                 *                {@link DateUtils}.
113                 * @return the requested format. This simple DateFormat should not be used
114                 * to {@link SimpleDateFormat#applyPattern(String) apply} to a
115                 * different pattern.
116                 */
117                static SimpleDateFormat formatFor(final String pattern) {
118                        final SoftReference<Map<String, SimpleDateFormat>> ref = THREADLOCAL_FORMATS.get();
119                        Map<String, SimpleDateFormat> formats = ref.get();
120                        if (formats == null) {
121                                formats = new HashMap<>();
122                                THREADLOCAL_FORMATS.set(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         * Parses a date value.  The formats used for parsing the date value are retrieved from
138         * the default http params.
139         *
140         * @param theDateValue the date value to parse
141         * @return the parsed date or null if input could not be parsed
142         */
143        public static Date parseDate(final String theDateValue) {
144                notNull(theDateValue, "Date value");
145                String v = theDateValue;
146                if (v.length() > 1 && v.startsWith("'") && v.endsWith("'")) {
147                        v = v.substring(1, v.length() - 1);
148                }
149
150                for (final String dateFormat : DEFAULT_PATTERNS) {
151                        final SimpleDateFormat dateParser = DateFormatHolder.formatFor(dateFormat);
152                        dateParser.set2DigitYearStart(DEFAULT_TWO_DIGIT_YEAR_START);
153                        final ParsePosition pos = new ParsePosition(0);
154                        final Date result = dateParser.parse(v, pos);
155                        if (pos.getIndex() != 0) {
156                                return result;
157                        }
158                }
159                return null;
160        }
161
162        public static Date getHighestInstantFromDate(Date theDateValue) {
163                Calendar sourceCal = Calendar.getInstance();
164                sourceCal.setTime(theDateValue);
165
166                Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT-12:00"));
167                copyDateAndTrundateTime(sourceCal, cal);
168                return cal.getTime();
169        }
170
171        public static Date getLowestInstantFromDate(Date theDateValue) {
172                Calendar sourceCal = Calendar.getInstance();
173                sourceCal.setTime(theDateValue);
174
175                Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT+14:00"));
176                copyDateAndTrundateTime(sourceCal, cal);
177                return cal.getTime();
178        }
179
180        private static void copyDateAndTrundateTime(Calendar theSourceCal, Calendar theCal) {
181                theCal.set(Calendar.YEAR, theSourceCal.get(Calendar.YEAR));
182                theCal.set(Calendar.MONTH, theSourceCal.get(Calendar.MONTH));
183                theCal.set(Calendar.DAY_OF_MONTH, theSourceCal.get(Calendar.DAY_OF_MONTH));
184                theCal.set(Calendar.HOUR_OF_DAY, 0);
185                theCal.set(Calendar.MINUTE, 0);
186                theCal.set(Calendar.SECOND, 0);
187                theCal.set(Calendar.MILLISECOND, 0);
188        }
189
190        public static int convertDateToDayInteger(final Date theDateValue) {
191                notNull(theDateValue, "Date value");
192                SimpleDateFormat format = new SimpleDateFormat(PATTERN_INTEGER_DATE);
193                String theDateString = format.format(theDateValue);
194                return Integer.parseInt(theDateString);
195        }
196
197        public static String convertDateToIso8601String(final Date theDateValue) {
198                notNull(theDateValue, "Date value");
199                SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
200                return format.format(theDateValue);
201        }
202
203        /**
204         * Formats the given date according to the RFC 1123 pattern.
205         *
206         * @param date The date to format.
207         * @return An RFC 1123 formatted date string.
208         * @see #PATTERN_RFC1123
209         */
210        public static String formatDate(final Date date) {
211                notNull(date, "Date");
212                notNull(PATTERN_RFC1123, "Pattern");
213                final SimpleDateFormat formatter = DateFormatHolder.formatFor(PATTERN_RFC1123);
214                return formatter.format(date);
215        }
216
217        public static <T> T notNull(final T argument, final String name) {
218                if (argument == null) {
219                        throw new IllegalArgumentException(Msg.code(1783) + name + " may not be null");
220                }
221                return argument;
222        }
223
224        /**
225         * Convert an incomplete date e.g. 2020 or 2020-01 to a complete date with lower
226         * bound to the first day of the year/month, and upper bound to the last day of
227         * the year/month
228         *
229         *  e.g. 2020 to 2020-01-01 (left), 2020-12-31 (right)
230         *  2020-02 to 2020-02-01 (left), 2020-02-29 (right)
231         *
232         * @param theIncompleteDateStr 2020 or 2020-01
233         * @return a pair of complete date, left is lower bound, and right is upper bound
234         */
235        public static Pair<String, String> getCompletedDate(String theIncompleteDateStr) {
236
237                if (StringUtils.isBlank(theIncompleteDateStr)) return new ImmutablePair<>(null, null);
238
239                String lbStr, upStr;
240                // YYYY only, return the last day of the year
241                if (theIncompleteDateStr.length() == 4) {
242                        lbStr = theIncompleteDateStr + "-01-01"; // first day of the year
243                        upStr = theIncompleteDateStr + "-12-31"; // last day of the year
244                        return new ImmutablePair<>(lbStr, upStr);
245                }
246
247                // Not YYYY-MM, no change
248                if (theIncompleteDateStr.length() != 7) return new ImmutablePair<>(theIncompleteDateStr, theIncompleteDateStr);
249
250                // YYYY-MM Only
251                Date lb;
252                try {
253                        // first day of the month
254                        lb = new SimpleDateFormat("yyyy-MM-dd").parse(theIncompleteDateStr + "-01");
255                } catch (ParseException e) {
256                        return new ImmutablePair<>(theIncompleteDateStr, theIncompleteDateStr);
257                }
258
259                // last day of the month
260                Calendar calendar = Calendar.getInstance();
261                calendar.setTime(lb);
262
263                calendar.add(Calendar.MONTH, 1);
264                calendar.set(Calendar.DAY_OF_MONTH, 1);
265                calendar.add(Calendar.DATE, -1);
266
267                Date ub = calendar.getTime();
268
269                lbStr = new SimpleDateFormat("yyyy-MM-dd").format(lb);
270                upStr = new SimpleDateFormat("yyyy-MM-dd").format(ub);
271
272                return new ImmutablePair<>(lbStr, upStr);
273        }
274
275        public static Date getEndOfDay(Date theDate) {
276
277                Calendar cal = Calendar.getInstance();
278                cal.setTime(theDate);
279                cal.set(Calendar.HOUR_OF_DAY, cal.getMaximum(Calendar.HOUR_OF_DAY));
280                cal.set(Calendar.MINUTE, cal.getMaximum(Calendar.MINUTE));
281                cal.set(Calendar.SECOND, cal.getMaximum(Calendar.SECOND));
282                cal.set(Calendar.MILLISECOND, cal.getMaximum(Calendar.MILLISECOND));
283                return cal.getTime();
284        }
285}