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}