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}