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