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}