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 java.lang.ref.SoftReference;
024import java.text.ParseException;
025import java.text.ParsePosition;
026import java.text.SimpleDateFormat;
027import java.util.Calendar;
028import java.util.Date;
029import java.util.HashMap;
030import java.util.Locale;
031import java.util.Map;
032import java.util.TimeZone;
033
034import org.apache.commons.lang3.StringUtils;
035import org.apache.commons.lang3.tuple.ImmutablePair;
036import org.apache.commons.lang3.tuple.Pair;
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[]{
081                PATTERN_RFC1123,
082                PATTERN_RFC1036,
083                PATTERN_ASCTIME
084        };
085        private static final Date DEFAULT_TWO_DIGIT_YEAR_START;
086
087        static {
088                final Calendar calendar = Calendar.getInstance();
089                calendar.setTimeZone(GMT);
090                calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
091                calendar.set(Calendar.MILLISECOND, 0);
092                DEFAULT_TWO_DIGIT_YEAR_START = calendar.getTime();
093        }
094
095        /**
096         * This class should not be instantiated.
097         */
098        private DateUtils() {
099        }
100
101        /**
102         * A factory for {@link SimpleDateFormat}s. The instances are stored in a
103         * threadlocal way because SimpleDateFormat is not thread safe as noted in
104         * {@link SimpleDateFormat its javadoc}.
105         */
106        final static class DateFormatHolder {
107
108                private static final ThreadLocal<SoftReference<Map<String, SimpleDateFormat>>> THREADLOCAL_FORMATS = ThreadLocal.withInitial(() -> new SoftReference<>(new HashMap<>()));
109
110                /**
111                 * creates a {@link SimpleDateFormat} for the requested format string.
112                 *
113                 * @param pattern a non-{@code null} format String according to
114                 *                {@link SimpleDateFormat}. The format is not checked against
115                 *                {@code null} since all paths go through
116                 *                {@link DateUtils}.
117                 * @return the requested format. This simple DateFormat should not be used
118                 * to {@link SimpleDateFormat#applyPattern(String) apply} to a
119                 * different pattern.
120                 */
121                static SimpleDateFormat formatFor(final String pattern) {
122                        final SoftReference<Map<String, SimpleDateFormat>> ref = THREADLOCAL_FORMATS.get();
123                        Map<String, SimpleDateFormat> formats = ref.get();
124                        if (formats == null) {
125                                formats = new HashMap<>();
126                                THREADLOCAL_FORMATS.set(
127                                        new SoftReference<>(formats));
128                        }
129
130                        SimpleDateFormat format = formats.get(pattern);
131                        if (format == null) {
132                                format = new SimpleDateFormat(pattern, Locale.US);
133                                format.setTimeZone(TimeZone.getTimeZone("GMT"));
134                                formats.put(pattern, format);
135                        }
136
137                        return format;
138                }
139
140        }
141
142        /**
143         * Parses a date value.  The formats used for parsing the date value are retrieved from
144         * the default http params.
145         *
146         * @param theDateValue the date value to parse
147         * @return the parsed date or null if input could not be parsed
148         */
149        public static Date parseDate(final String theDateValue) {
150                notNull(theDateValue, "Date value");
151                String v = theDateValue;
152                if (v.length() > 1 && v.startsWith("'") && v.endsWith("'")) {
153                        v = v.substring(1, v.length() - 1);
154                }
155
156                for (final String dateFormat : DEFAULT_PATTERNS) {
157                        final SimpleDateFormat dateParser = DateFormatHolder.formatFor(dateFormat);
158                        dateParser.set2DigitYearStart(DEFAULT_TWO_DIGIT_YEAR_START);
159                        final ParsePosition pos = new ParsePosition(0);
160                        final Date result = dateParser.parse(v, pos);
161                        if (pos.getIndex() != 0) {
162                                return result;
163                        }
164                }
165                return null;
166        }
167
168        public static Date getHighestInstantFromDate(Date theDateValue) {
169                Calendar sourceCal = Calendar.getInstance();
170                sourceCal.setTime(theDateValue);
171
172                Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT-12:00"));
173                copyDateAndTrundateTime(sourceCal, cal);
174                return cal.getTime();
175        }
176
177        public static Date getLowestInstantFromDate(Date theDateValue) {
178                Calendar sourceCal = Calendar.getInstance();
179                sourceCal.setTime(theDateValue);
180
181                Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT+14:00"));
182                copyDateAndTrundateTime(sourceCal, cal);
183                return cal.getTime();
184        }
185
186        private static void copyDateAndTrundateTime(Calendar theSourceCal, Calendar theCal) {
187                theCal.set(Calendar.YEAR, theSourceCal.get(Calendar.YEAR));
188                theCal.set(Calendar.MONTH, theSourceCal.get(Calendar.MONTH));
189                theCal.set(Calendar.DAY_OF_MONTH, theSourceCal.get(Calendar.DAY_OF_MONTH));
190                theCal.set(Calendar.HOUR_OF_DAY, 0);
191                theCal.set(Calendar.MINUTE, 0);
192                theCal.set(Calendar.SECOND, 0);
193                theCal.set(Calendar.MILLISECOND, 0);
194        }
195
196        public static int convertDateToDayInteger(final Date theDateValue) {
197                notNull(theDateValue, "Date value");
198                SimpleDateFormat format = new SimpleDateFormat(PATTERN_INTEGER_DATE);
199                String theDateString = format.format(theDateValue);
200                return Integer.parseInt(theDateString);
201        }
202
203        public static String convertDateToIso8601String(final Date theDateValue) {
204                notNull(theDateValue, "Date value");
205                SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
206                return format.format(theDateValue);
207        }
208
209        /**
210         * Formats the given date according to the RFC 1123 pattern.
211         *
212         * @param date The date to format.
213         * @return An RFC 1123 formatted date string.
214         * @see #PATTERN_RFC1123
215         */
216        public static String formatDate(final Date date) {
217                notNull(date, "Date");
218                notNull(PATTERN_RFC1123, "Pattern");
219                final SimpleDateFormat formatter = DateFormatHolder.formatFor(PATTERN_RFC1123);
220                return formatter.format(date);
221        }
222
223        public static <T> T notNull(final T argument, final String name) {
224                if (argument == null) {
225                        throw new IllegalArgumentException(name + " may not be null");
226                }
227                return argument;
228        }
229                
230        /**
231         * Convert an incomplete date e.g. 2020 or 2020-01 to a complete date with lower
232         * bound to the first day of the year/month, and upper bound to the last day of
233         * the year/month
234         * 
235         *  e.g. 2020 to 2020-01-01 (left), 2020-12-31 (right)
236         *  2020-02 to 2020-02-01 (left), 2020-02-29 (right)
237         *
238         * @param theIncompleteDateStr 2020 or 2020-01
239         * @return a pair of complete date, left is lower bound, and right is upper bound
240         */
241        public static Pair<String, String> getCompletedDate(String theIncompleteDateStr) {
242                
243                if (StringUtils.isBlank(theIncompleteDateStr)) 
244                        return new ImmutablePair<String, String>(null, null);
245                        
246                String lbStr, upStr;
247                // YYYY only, return the last day of the year
248                if (theIncompleteDateStr.length() == 4)  {
249                        lbStr = theIncompleteDateStr + "-01-01"; // first day of the year
250                        upStr = theIncompleteDateStr + "-12-31"; // last day of the year
251                        return new ImmutablePair<String, String>(lbStr, upStr);
252                }
253                
254                // Not YYYY-MM, no change
255                if (theIncompleteDateStr.length() != 7)
256                        return new ImmutablePair<String, String>(theIncompleteDateStr, theIncompleteDateStr);
257                
258                // YYYY-MM Only
259                Date lb=null;
260                try {
261                        // first day of the month
262                        lb = new SimpleDateFormat("yyyy-MM-dd").parse(theIncompleteDateStr+"-01");   
263                } catch (ParseException e) {
264                        return new ImmutablePair<String, String>(theIncompleteDateStr, theIncompleteDateStr);
265                }
266                
267                // last day of the month
268            Calendar calendar = Calendar.getInstance();  
269            calendar.setTime(lb);  
270
271            calendar.add(Calendar.MONTH, 1);  
272            calendar.set(Calendar.DAY_OF_MONTH, 1);  
273            calendar.add(Calendar.DATE, -1);  
274
275            Date ub = calendar.getTime();  
276
277            lbStr = new SimpleDateFormat("yyyy-MM-dd").format(lb);  
278            upStr = new SimpleDateFormat("yyyy-MM-dd").format(ub);  
279                
280                return new ImmutablePair<String, String>(lbStr, upStr);
281        }
282        
283        public static Date getEndOfDay(Date theDate) {
284
285                Calendar cal = Calendar.getInstance();
286                cal.setTime(theDate);
287                cal.set(Calendar.HOUR_OF_DAY, cal.getMaximum(Calendar.HOUR_OF_DAY));
288                cal.set(Calendar.MINUTE, cal.getMaximum(Calendar.MINUTE));
289                cal.set(Calendar.SECOND, cal.getMaximum(Calendar.SECOND));
290                cal.set(Calendar.MILLISECOND, cal.getMaximum(Calendar.MILLISECOND));
291                return cal.getTime();
292        }
293}