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}