001package ca.uhn.fhir.util; 002 003/* 004 * #%L 005 * HAPI FHIR - Core Library 006 * %% 007 * Copyright (C) 2014 - 2021 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.ParsePosition; 025import java.text.SimpleDateFormat; 026import java.util.Calendar; 027import java.util.Date; 028import java.util.HashMap; 029import java.util.Locale; 030import java.util.Map; 031import java.util.TimeZone; 032 033/** 034 * A utility class for parsing and formatting HTTP dates as used in cookies and 035 * other headers. This class handles dates as defined by RFC 2616 section 036 * 3.3.1 as well as some other common non-standard formats. 037 * <p> 038 * This class is basically intended to be a high-performance workaround 039 * for the fact that Java SimpleDateFormat is kind of expensive to 040 * create and yet isn't thread safe. 041 * </p> 042 * <p> 043 * This class was adapted from the class with the same name from the Jetty 044 * project, licensed under the terms of the Apache Software License 2.0. 045 * </p> 046 */ 047public final class DateUtils { 048 049 /** 050 * GMT TimeZone 051 */ 052 public static final TimeZone GMT = TimeZone.getTimeZone("GMT"); 053 054 /** 055 * Date format pattern used to parse HTTP date headers in RFC 1123 format. 056 */ 057 @SuppressWarnings("WeakerAccess") 058 public static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz"; 059 060 /** 061 * Date format pattern used to parse HTTP date headers in RFC 1036 format. 062 */ 063 @SuppressWarnings("WeakerAccess") 064 public static final String PATTERN_RFC1036 = "EEE, dd-MMM-yy HH:mm:ss zzz"; 065 066 /** 067 * Date format pattern used to parse HTTP date headers in ANSI C 068 * {@code asctime()} format. 069 */ 070 @SuppressWarnings("WeakerAccess") 071 public static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy"; 072 073 private static final String PATTERN_INTEGER_DATE = "yyyyMMdd"; 074 075 private static final String[] DEFAULT_PATTERNS = new String[]{ 076 PATTERN_RFC1123, 077 PATTERN_RFC1036, 078 PATTERN_ASCTIME 079 }; 080 private static final Date DEFAULT_TWO_DIGIT_YEAR_START; 081 082 static { 083 final Calendar calendar = Calendar.getInstance(); 084 calendar.setTimeZone(GMT); 085 calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0); 086 calendar.set(Calendar.MILLISECOND, 0); 087 DEFAULT_TWO_DIGIT_YEAR_START = calendar.getTime(); 088 } 089 090 /** 091 * This class should not be instantiated. 092 */ 093 private DateUtils() { 094 } 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 final static class DateFormatHolder { 102 103 private static final ThreadLocal<SoftReference<Map<String, SimpleDateFormat>>> THREADLOCAL_FORMATS = ThreadLocal.withInitial(() -> new SoftReference<>(new HashMap<>())); 104 105 /** 106 * creates a {@link SimpleDateFormat} for the requested format string. 107 * 108 * @param pattern a non-{@code null} format String according to 109 * {@link SimpleDateFormat}. The format is not checked against 110 * {@code null} since all paths go through 111 * {@link DateUtils}. 112 * @return the requested format. This simple DateFormat should not be used 113 * to {@link SimpleDateFormat#applyPattern(String) apply} to a 114 * different pattern. 115 */ 116 static SimpleDateFormat formatFor(final String pattern) { 117 final SoftReference<Map<String, SimpleDateFormat>> ref = THREADLOCAL_FORMATS.get(); 118 Map<String, SimpleDateFormat> formats = ref.get(); 119 if (formats == null) { 120 formats = new HashMap<>(); 121 THREADLOCAL_FORMATS.set( 122 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 /** 138 * Parses a date value. The formats used for parsing the date value are retrieved from 139 * the default http params. 140 * 141 * @param theDateValue the date value to parse 142 * @return the parsed date or null if input could not be parsed 143 */ 144 public static Date parseDate(final String theDateValue) { 145 notNull(theDateValue, "Date value"); 146 String v = theDateValue; 147 if (v.length() > 1 && v.startsWith("'") && v.endsWith("'")) { 148 v = v.substring(1, v.length() - 1); 149 } 150 151 for (final String dateFormat : DEFAULT_PATTERNS) { 152 final SimpleDateFormat dateParser = DateFormatHolder.formatFor(dateFormat); 153 dateParser.set2DigitYearStart(DEFAULT_TWO_DIGIT_YEAR_START); 154 final ParsePosition pos = new ParsePosition(0); 155 final Date result = dateParser.parse(v, pos); 156 if (pos.getIndex() != 0) { 157 return result; 158 } 159 } 160 return null; 161 } 162 163 public static Date getHighestInstantFromDate(Date theDateValue) { 164 Calendar sourceCal = Calendar.getInstance(); 165 sourceCal.setTime(theDateValue); 166 167 Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT-12:00")); 168 copyDateAndTrundateTime(sourceCal, cal); 169 return cal.getTime(); 170 } 171 172 public static Date getLowestInstantFromDate(Date theDateValue) { 173 Calendar sourceCal = Calendar.getInstance(); 174 sourceCal.setTime(theDateValue); 175 176 Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT+14:00")); 177 copyDateAndTrundateTime(sourceCal, cal); 178 return cal.getTime(); 179 } 180 181 private static void copyDateAndTrundateTime(Calendar theSourceCal, Calendar theCal) { 182 theCal.set(Calendar.YEAR, theSourceCal.get(Calendar.YEAR)); 183 theCal.set(Calendar.MONTH, theSourceCal.get(Calendar.MONTH)); 184 theCal.set(Calendar.DAY_OF_MONTH, theSourceCal.get(Calendar.DAY_OF_MONTH)); 185 theCal.set(Calendar.HOUR_OF_DAY, 0); 186 theCal.set(Calendar.MINUTE, 0); 187 theCal.set(Calendar.SECOND, 0); 188 theCal.set(Calendar.MILLISECOND, 0); 189 } 190 191 public static int convertDateToDayInteger(final Date theDateValue) { 192 notNull(theDateValue, "Date value"); 193 SimpleDateFormat format = new SimpleDateFormat(PATTERN_INTEGER_DATE); 194 String theDateString = format.format(theDateValue); 195 return Integer.parseInt(theDateString); 196 } 197 198 public static String convertDateToIso8601String(final Date theDateValue) { 199 notNull(theDateValue, "Date value"); 200 SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); 201 return format.format(theDateValue); 202 } 203 204 /** 205 * Formats the given date according to the RFC 1123 pattern. 206 * 207 * @param date The date to format. 208 * @return An RFC 1123 formatted date string. 209 * @see #PATTERN_RFC1123 210 */ 211 public static String formatDate(final Date date) { 212 notNull(date, "Date"); 213 notNull(PATTERN_RFC1123, "Pattern"); 214 final SimpleDateFormat formatter = DateFormatHolder.formatFor(PATTERN_RFC1123); 215 return formatter.format(date); 216 } 217 218 public static <T> T notNull(final T argument, final String name) { 219 if (argument == null) { 220 throw new IllegalArgumentException(name + " may not be null"); 221 } 222 return argument; 223 } 224 225}