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 com.google.common.base.Preconditions;
024import org.apache.commons.lang3.StringUtils;
025import org.apache.commons.lang3.tuple.ImmutablePair;
026import org.apache.commons.lang3.tuple.Pair;
027
028import java.lang.ref.SoftReference;
029import java.text.ParseException;
030import java.text.ParsePosition;
031import java.text.SimpleDateFormat;
032import java.time.LocalDateTime;
033import java.time.Month;
034import java.time.YearMonth;
035import java.time.format.DateTimeFormatter;
036import java.time.temporal.ChronoField;
037import java.time.temporal.TemporalAccessor;
038import java.time.temporal.TemporalField;
039import java.util.Calendar;
040import java.util.Date;
041import java.util.HashMap;
042import java.util.Locale;
043import java.util.Map;
044import java.util.Objects;
045import java.util.Optional;
046import java.util.TimeZone;
047
048/**
049 * A utility class for parsing and formatting HTTP dates as used in cookies and
050 * other headers.  This class handles dates as defined by RFC 2616 section
051 * 3.3.1 as well as some other common non-standard formats.
052 * <p>
053 * This class is basically intended to be a high-performance workaround
054 * for the fact that Java SimpleDateFormat is kind of expensive to
055 * create and yet isn't thread safe.
056 * </p>
057 * <p>
058 * This class was adapted from the class with the same name from the Jetty
059 * project, licensed under the terms of the Apache Software License 2.0.
060 * </p>
061 */
062public final class DateUtils {
063
064        /**
065         * GMT TimeZone
066         */
067        public static final TimeZone GMT = TimeZone.getTimeZone("GMT");
068
069        /**
070         * Date format pattern used to parse HTTP date headers in RFC 1123 format.
071         */
072        @SuppressWarnings("WeakerAccess")
073        public static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
074
075        /**
076         * Date format pattern used to parse HTTP date headers in RFC 1036 format.
077         */
078        @SuppressWarnings("WeakerAccess")
079        public static final String PATTERN_RFC1036 = "EEE, dd-MMM-yy HH:mm:ss zzz";
080
081        /**
082         * Date format pattern used to parse HTTP date headers in ANSI C
083         * {@code asctime()} format.
084         */
085        @SuppressWarnings("WeakerAccess")
086        public static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy";
087
088        private static final String PATTERN_INTEGER_DATE = "yyyyMMdd";
089
090        private static final String[] DEFAULT_PATTERNS = new String[] {PATTERN_RFC1123, PATTERN_RFC1036, PATTERN_ASCTIME};
091        private static final Date DEFAULT_TWO_DIGIT_YEAR_START;
092
093        static {
094                final Calendar calendar = Calendar.getInstance();
095                calendar.setTimeZone(GMT);
096                calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
097                calendar.set(Calendar.MILLISECOND, 0);
098                DEFAULT_TWO_DIGIT_YEAR_START = calendar.getTime();
099        }
100
101        /**
102         * This class should not be instantiated.
103         */
104        private DateUtils() {}
105
106        /**
107         * Calculate a LocalDateTime with any missing date/time data points defaulting to the earliest values (ex 0 for hour)
108         * from a TemporalAccessor or empty if it doesn't contain a year.
109         *
110         * @param theTemporalAccessor The TemporalAccessor containing date/time information
111         * @return A LocalDateTime or empty
112         */
113        public static Optional<LocalDateTime> extractLocalDateTimeForRangeStartOrEmpty(
114                        TemporalAccessor theTemporalAccessor) {
115                if (theTemporalAccessor.isSupported(ChronoField.YEAR)) {
116                        final int year = theTemporalAccessor.get(ChronoField.YEAR);
117                        final Month month = Month.of(getTimeUnitIfSupported(theTemporalAccessor, ChronoField.MONTH_OF_YEAR, 1));
118                        final int day = getTimeUnitIfSupported(theTemporalAccessor, ChronoField.DAY_OF_MONTH, 1);
119                        final int hour = getTimeUnitIfSupported(theTemporalAccessor, ChronoField.HOUR_OF_DAY, 0);
120                        final int minute = getTimeUnitIfSupported(theTemporalAccessor, ChronoField.MINUTE_OF_HOUR, 0);
121                        final int seconds = getTimeUnitIfSupported(theTemporalAccessor, ChronoField.SECOND_OF_MINUTE, 0);
122
123                        return Optional.of(LocalDateTime.of(year, month, day, hour, minute, seconds));
124                }
125
126                return Optional.empty();
127        }
128
129        /**
130         * Calculate a LocalDateTime with any missing date/time data points defaulting to the latest values (ex 23 for hour)
131         * from a TemporalAccessor or empty if it doesn't contain a year.
132         *
133         * @param theTemporalAccessor The TemporalAccessor containing date/time information
134         * @return A LocalDateTime or empty
135         */
136        public static Optional<LocalDateTime> extractLocalDateTimeForRangeEndOrEmpty(TemporalAccessor theTemporalAccessor) {
137                if (theTemporalAccessor.isSupported(ChronoField.YEAR)) {
138                        final int year = theTemporalAccessor.get(ChronoField.YEAR);
139                        final Month month = Month.of(getTimeUnitIfSupported(theTemporalAccessor, ChronoField.MONTH_OF_YEAR, 12));
140                        final int day = getTimeUnitIfSupported(
141                                        theTemporalAccessor,
142                                        ChronoField.DAY_OF_MONTH,
143                                        YearMonth.of(year, month).atEndOfMonth().getDayOfMonth());
144                        final int hour = getTimeUnitIfSupported(theTemporalAccessor, ChronoField.HOUR_OF_DAY, 23);
145                        final int minute = getTimeUnitIfSupported(theTemporalAccessor, ChronoField.MINUTE_OF_HOUR, 59);
146                        final int seconds = getTimeUnitIfSupported(theTemporalAccessor, ChronoField.SECOND_OF_MINUTE, 59);
147
148                        return Optional.of(LocalDateTime.of(year, month, day, hour, minute, seconds));
149                }
150
151                return Optional.empty();
152        }
153
154        /**
155         * With the provided DateTimeFormatter, parse a date time String or return empty if the String doesn't correspond
156         * to the formatter.
157         *
158         * @param theDateTimeString A date/time String in some date format
159         * @param theSupportedDateTimeFormatter The DateTimeFormatter we expect corresponds to the String
160         * @return The parsed TemporalAccessor or empty
161         */
162        public static Optional<TemporalAccessor> parseDateTimeStringIfValid(
163                        String theDateTimeString, DateTimeFormatter theSupportedDateTimeFormatter) {
164                Objects.requireNonNull(theSupportedDateTimeFormatter);
165                Preconditions.checkArgument(StringUtils.isNotBlank(theDateTimeString));
166
167                try {
168                        return Optional.of(theSupportedDateTimeFormatter.parse(theDateTimeString));
169                } catch (Exception exception) {
170                        return Optional.empty();
171                }
172        }
173
174        private static int getTimeUnitIfSupported(
175                        TemporalAccessor theTemporalAccessor, TemporalField theTemporalField, int theDefaultValue) {
176                return getTimeUnitIfSupportedOrEmpty(theTemporalAccessor, theTemporalField)
177                                .orElse(theDefaultValue);
178        }
179
180        private static Optional<Integer> getTimeUnitIfSupportedOrEmpty(
181                        TemporalAccessor theTemporalAccessor, TemporalField theTemporalField) {
182                if (theTemporalAccessor.isSupported(theTemporalField)) {
183                        return Optional.of(theTemporalAccessor.get(theTemporalField));
184                }
185
186                return Optional.empty();
187        }
188
189        /**
190         * A factory for {@link SimpleDateFormat}s. The instances are stored in a
191         * threadlocal way because SimpleDateFormat is not thread safe as noted in
192         * {@link SimpleDateFormat its javadoc}.
193         */
194        static final class DateFormatHolder {
195
196                private static final ThreadLocal<SoftReference<Map<String, SimpleDateFormat>>> THREADLOCAL_FORMATS =
197                                ThreadLocal.withInitial(() -> new SoftReference<>(new HashMap<>()));
198
199                /**
200                 * creates a {@link SimpleDateFormat} for the requested format string.
201                 *
202                 * @param pattern a non-{@code null} format String according to
203                 *                {@link SimpleDateFormat}. The format is not checked against
204                 *                {@code null} since all paths go through
205                 *                {@link DateUtils}.
206                 * @return the requested format. This simple DateFormat should not be used
207                 * to {@link SimpleDateFormat#applyPattern(String) apply} to a
208                 * different pattern.
209                 */
210                static SimpleDateFormat formatFor(final String pattern) {
211                        final SoftReference<Map<String, SimpleDateFormat>> ref = THREADLOCAL_FORMATS.get();
212                        Map<String, SimpleDateFormat> formats = ref.get();
213                        if (formats == null) {
214                                formats = new HashMap<>();
215                                THREADLOCAL_FORMATS.set(new SoftReference<>(formats));
216                        }
217
218                        SimpleDateFormat format = formats.get(pattern);
219                        if (format == null) {
220                                format = new SimpleDateFormat(pattern, Locale.US);
221                                format.setTimeZone(TimeZone.getTimeZone("GMT"));
222                                formats.put(pattern, format);
223                        }
224
225                        return format;
226                }
227        }
228
229        /**
230         * Parses a date value.  The formats used for parsing the date value are retrieved from
231         * the default http params.
232         *
233         * @param theDateValue the date value to parse
234         * @return the parsed date or null if input could not be parsed
235         */
236        public static Date parseDate(final String theDateValue) {
237                notNull(theDateValue, "Date value");
238                String v = theDateValue;
239                if (v.length() > 1 && v.startsWith("'") && v.endsWith("'")) {
240                        v = v.substring(1, v.length() - 1);
241                }
242
243                for (final String dateFormat : DEFAULT_PATTERNS) {
244                        final SimpleDateFormat dateParser = DateFormatHolder.formatFor(dateFormat);
245                        dateParser.set2DigitYearStart(DEFAULT_TWO_DIGIT_YEAR_START);
246                        final ParsePosition pos = new ParsePosition(0);
247                        final Date result = dateParser.parse(v, pos);
248                        if (pos.getIndex() != 0) {
249                                return result;
250                        }
251                }
252                return null;
253        }
254
255        public static Date getHighestInstantFromDate(Date theDateValue) {
256                Calendar sourceCal = Calendar.getInstance();
257                sourceCal.setTime(theDateValue);
258
259                Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT-12:00"));
260                copyDateAndTrundateTime(sourceCal, cal);
261                return cal.getTime();
262        }
263
264        public static Date getLowestInstantFromDate(Date theDateValue) {
265                Calendar sourceCal = Calendar.getInstance();
266                sourceCal.setTime(theDateValue);
267
268                Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT+14:00"));
269                copyDateAndTrundateTime(sourceCal, cal);
270                return cal.getTime();
271        }
272
273        private static void copyDateAndTrundateTime(Calendar theSourceCal, Calendar theCal) {
274                theCal.set(Calendar.YEAR, theSourceCal.get(Calendar.YEAR));
275                theCal.set(Calendar.MONTH, theSourceCal.get(Calendar.MONTH));
276                theCal.set(Calendar.DAY_OF_MONTH, theSourceCal.get(Calendar.DAY_OF_MONTH));
277                theCal.set(Calendar.HOUR_OF_DAY, 0);
278                theCal.set(Calendar.MINUTE, 0);
279                theCal.set(Calendar.SECOND, 0);
280                theCal.set(Calendar.MILLISECOND, 0);
281        }
282
283        public static int convertDateToDayInteger(final Date theDateValue) {
284                notNull(theDateValue, "Date value");
285                SimpleDateFormat format = new SimpleDateFormat(PATTERN_INTEGER_DATE);
286                String theDateString = format.format(theDateValue);
287                return Integer.parseInt(theDateString);
288        }
289
290        public static String convertDateToIso8601String(final Date theDateValue) {
291                notNull(theDateValue, "Date value");
292                SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
293                return format.format(theDateValue);
294        }
295
296        /**
297         * Formats the given date according to the RFC 1123 pattern.
298         *
299         * @param date The date to format.
300         * @return An RFC 1123 formatted date string.
301         * @see #PATTERN_RFC1123
302         */
303        public static String formatDate(final Date date) {
304                notNull(date, "Date");
305                notNull(PATTERN_RFC1123, "Pattern");
306                final SimpleDateFormat formatter = DateFormatHolder.formatFor(PATTERN_RFC1123);
307                return formatter.format(date);
308        }
309
310        public static <T> T notNull(final T argument, final String name) {
311                if (argument == null) {
312                        throw new IllegalArgumentException(Msg.code(1783) + name + " may not be null");
313                }
314                return argument;
315        }
316
317        /**
318         * Convert an incomplete date e.g. 2020 or 2020-01 to a complete date with lower
319         * bound to the first day of the year/month, and upper bound to the last day of
320         * the year/month
321         *
322         *  e.g. 2020 to 2020-01-01 (left), 2020-12-31 (right)
323         *  2020-02 to 2020-02-01 (left), 2020-02-29 (right)
324         *
325         * @param theIncompleteDateStr 2020 or 2020-01
326         * @return a pair of complete date, left is lower bound, and right is upper bound
327         */
328        public static Pair<String, String> getCompletedDate(String theIncompleteDateStr) {
329
330                if (StringUtils.isBlank(theIncompleteDateStr)) return new ImmutablePair<>(null, null);
331
332                String lbStr, upStr;
333                // YYYY only, return the last day of the year
334                if (theIncompleteDateStr.length() == 4) {
335                        lbStr = theIncompleteDateStr + "-01-01"; // first day of the year
336                        upStr = theIncompleteDateStr + "-12-31"; // last day of the year
337                        return new ImmutablePair<>(lbStr, upStr);
338                }
339
340                // Not YYYY-MM, no change
341                if (theIncompleteDateStr.length() != 7) return new ImmutablePair<>(theIncompleteDateStr, theIncompleteDateStr);
342
343                // YYYY-MM Only
344                Date lb;
345                try {
346                        // first day of the month
347                        lb = new SimpleDateFormat("yyyy-MM-dd").parse(theIncompleteDateStr + "-01");
348                } catch (ParseException e) {
349                        return new ImmutablePair<>(theIncompleteDateStr, theIncompleteDateStr);
350                }
351
352                // last day of the month
353                Calendar calendar = Calendar.getInstance();
354                calendar.setTime(lb);
355
356                calendar.add(Calendar.MONTH, 1);
357                calendar.set(Calendar.DAY_OF_MONTH, 1);
358                calendar.add(Calendar.DATE, -1);
359
360                Date ub = calendar.getTime();
361
362                lbStr = new SimpleDateFormat("yyyy-MM-dd").format(lb);
363                upStr = new SimpleDateFormat("yyyy-MM-dd").format(ub);
364
365                return new ImmutablePair<>(lbStr, upStr);
366        }
367
368        public static Date getEndOfDay(Date theDate) {
369
370                Calendar cal = Calendar.getInstance();
371                cal.setTime(theDate);
372                cal.set(Calendar.HOUR_OF_DAY, cal.getMaximum(Calendar.HOUR_OF_DAY));
373                cal.set(Calendar.MINUTE, cal.getMaximum(Calendar.MINUTE));
374                cal.set(Calendar.SECOND, cal.getMaximum(Calendar.SECOND));
375                cal.set(Calendar.MILLISECOND, cal.getMaximum(Calendar.MILLISECOND));
376                return cal.getTime();
377        }
378}