001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.internal; 018 019import static org.apache.juneau.common.utils.ThrowableUtils.*; 020 021import java.lang.ref.*; 022import java.text.*; 023import java.time.format.*; 024import java.util.*; 025 026import org.apache.juneau.common.utils.*; 027import org.apache.juneau.reflect.*; 028 029import jakarta.xml.bind.*; 030 031/** 032 * A utility class for parsing and formatting HTTP dates as used in cookies and other headers. 033 * 034 * <p> 035 * This class handles dates as defined by RFC 2616 section 3.3.1 as well as some other common non-standard formats. 036 * 037 * <p> 038 * This class was copied from HttpClient 4.3. 039 * 040 * <h5 class='section'>See Also:</h5><ul> 041 * </ul> 042 */ 043public class DateUtils { 044 045 /** 046 * Date format pattern used to parse HTTP date headers in RFC 1123 format. 047 */ 048 public static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz"; 049 050 /** 051 * Date format pattern used to parse HTTP date headers in RFC 1036 format. 052 */ 053 public static final String PATTERN_RFC1036 = "EEE, dd-MMM-yy HH:mm:ss zzz"; 054 055 /** 056 * Date format pattern used to parse HTTP date headers in ANSI C <c>asctime()</c> format. 057 */ 058 public static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy"; 059 private static final TimeZone GMT = TimeZone.getTimeZone("GMT"); 060 static { 061 final Calendar calendar = Calendar.getInstance(); 062 calendar.setTimeZone(GMT); 063 calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0); 064 calendar.set(Calendar.MILLISECOND, 0); 065 } 066 067 /** 068 * Parses an ISO8601 string and converts it to a {@link Calendar}. 069 * 070 * @param s The string to parse. 071 * @return The parsed value, or <jk>null</jk> if the string was <jk>null</jk> or empty. 072 */ 073 public static Calendar parseISO8601Calendar(String s) { 074 if (Utils.isEmpty(s)) 075 return null; 076 return DatatypeConverter.parseDateTime(toValidISO8601DT(s)); 077 } 078 079 /** 080 * Formats the given date according to the specified pattern. 081 * 082 * <p> 083 * The pattern must conform to that used by the {@link SimpleDateFormat simple date format} class. 084 * 085 * @param date The date to format. 086 * @param pattern The pattern to use for formatting the date. 087 * @return A formatted date string. 088 * @throws IllegalArgumentException If the given date pattern is invalid. 089 * @see SimpleDateFormat 090 */ 091 public static String formatDate(final Date date, final String pattern) { 092 final SimpleDateFormat formatter = DateFormatHolder.formatFor(pattern); 093 return formatter.format(date); 094 } 095 096 /** 097 * Clears thread-local variable containing {@link java.text.DateFormat} cache. 098 */ 099 public static void clearThreadLocal() { 100 DateFormatHolder.clearThreadLocal(); 101 } 102 103 /** 104 * A factory for {@link SimpleDateFormat}s. 105 * 106 * <p> 107 * The instances are stored in a thread-local way because SimpleDateFormat is not thread-safe as noted in 108 * {@link SimpleDateFormat its javadoc}. 109 */ 110 static class DateFormatHolder { 111 private static final ThreadLocal<SoftReference<Map<String,SimpleDateFormat>>> THREADLOCAL_FORMATS = 112 new ThreadLocal<>() { 113 @Override 114 protected SoftReference<Map<String,SimpleDateFormat>> initialValue() { 115 Map<String,SimpleDateFormat> m = new HashMap<>(); 116 return new SoftReference<>(m); 117 } 118 }; 119 120 /** 121 * Creates a {@link SimpleDateFormat} for the requested format string. 122 * 123 * @param pattern 124 * A non-<c>null</c> format String according to {@link SimpleDateFormat}. 125 * The format is not checked against <c>null</c> since all paths go through {@link DateUtils}. 126 * @return 127 * The requested format. 128 * This simple date-format should not be used to {@link SimpleDateFormat#applyPattern(String) apply} to a 129 * different pattern. 130 */ 131 public static SimpleDateFormat formatFor(final String pattern) { 132 final SoftReference<Map<String,SimpleDateFormat>> ref = THREADLOCAL_FORMATS.get(); 133 Map<String,SimpleDateFormat> formats = ref.get(); 134 if (formats == null) { 135 formats = new HashMap<>(); 136 THREADLOCAL_FORMATS.set(new SoftReference<>(formats)); 137 } 138 SimpleDateFormat format = formats.get(pattern); 139 if (format == null) { 140 format = new SimpleDateFormat(pattern, Locale.US); 141 format.setTimeZone(TimeZone.getTimeZone("GMT")); 142 formats.put(pattern, format); 143 } 144 return format; 145 } 146 147 public static void clearThreadLocal() { 148 THREADLOCAL_FORMATS.remove(); 149 } 150 } 151 152 /** 153 * Pads out an ISO8601 string so that it can be parsed using {@link DatatypeConverter#parseDateTime(String)}. 154 * 155 * <ul> 156 * <li><js>"2001-07-04T15:30:45-05:00"</js> -> <js>"2001-07-04T15:30:45-05:00"</js> 157 * <li><js>"2001-07-04T15:30:45Z"</js> -> <js>"2001-07-04T15:30:45Z"</js> 158 * <li><js>"2001-07-04T15:30:45.1Z"</js> -> <js>"2001-07-04T15:30:45.1Z"</js> 159 * <li><js>"2001-07-04T15:30Z"</js> -> <js>"2001-07-04T15:30:00Z"</js> 160 * <li><js>"2001-07-04T15:30"</js> -> <js>"2001-07-04T15:30:00"</js> 161 * <li><js>"2001-07-04"</js> -> <li><js>"2001-07-04T00:00:00"</js> 162 * <li><js>"2001-07"</js> -> <js>"2001-07-01T00:00:00"</js> 163 * <li><js>"2001"</js> -> <js>"2001-01-01T00:00:00"</js> 164 * </ul> 165 * 166 * @param in The string to pad. 167 * @return The padded string. 168 */ 169 public static String toValidISO8601DT(String in) { 170 171 // "2001-07-04T15:30:45Z" 172 final int 173 S1 = 1, // Looking for - 174 S2 = 2, // Found -, looking for - 175 S3 = 3, // Found -, looking for T 176 S4 = 4, // Found T, looking for : 177 S5 = 5, // Found :, looking for : 178 S6 = 6; // Found : 179 180 int state = 1; 181 boolean needsT = false; 182 for (int i = 0; i < in.length(); i++) { 183 char c = in.charAt(i); 184 if (state == S1) { 185 if (c == '-') 186 state = S2; 187 } else if (state == S2) { 188 if (c == '-') 189 state = S3; 190 } else if (state == S3) { 191 if (c == 'T') 192 state = S4; 193 if (c == ' ') { 194 state = S4; 195 needsT = true; 196 } 197 } else if (state == S4) { 198 if (c == ':') 199 state = S5; 200 } else if (state == S5) { 201 if (c == ':') 202 state = S6; 203 } 204 } 205 206 if (needsT) 207 in = in.replace(' ', 'T'); 208 switch(state) { 209 case S1: return in + "-01-01T00:00:00"; 210 case S2: return in + "-01T00:00:00"; 211 case S3: return in + "T00:00:00"; 212 case S4: return in + ":00:00"; 213 case S5: return in + ":00"; 214 default: return in; 215 } 216 } 217 218 /** 219 * Returns a {@link DateTimeFormatter} using either a pattern or predefined pattern name. 220 * 221 * @param pattern The pattern (e.g. <js>"yyyy-MM-dd"</js>) or pattern name (e.g. <js>"ISO_INSTANT"</js>). 222 * @return The formatter. 223 */ 224 public static DateTimeFormatter getFormatter(String pattern) { 225 if (Utils.isEmpty(pattern)) 226 return DateTimeFormatter.ISO_INSTANT; 227 try { 228 FieldInfo fi = ClassInfo.of(DateTimeFormatter.class).getPublicField(x -> x.isStatic() && x.hasName(pattern)); 229 if (fi != null) 230 return (DateTimeFormatter)fi.inner().get(null); 231 return DateTimeFormatter.ofPattern(pattern); 232 } catch (IllegalArgumentException | IllegalAccessException e) { 233 throw asRuntimeException(e); 234 } 235 } 236}