001package ca.uhn.fhir.util;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2017 University Health Network
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
023/*
024 * ====================================================================
025 * Licensed to the Apache Software Foundation (ASF) under one
026 * or more contributor license agreements.  See the NOTICE file
027 * distributed with this work for additional information
028 * regarding copyright ownership.  The ASF licenses this file
029 * to you under the Apache License, Version 2.0 (the
030 * "License"); you may not use this file except in compliance
031 * with the License.  You may obtain a copy of the License at
032 *
033 *   http://www.apache.org/licenses/LICENSE-2.0
034 *
035 * Unless required by applicable law or agreed to in writing,
036 * software distributed under the License is distributed on an
037 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
038 * KIND, either express or implied.  See the License for the
039 * specific language governing permissions and limitations
040 * under the License.
041 * ====================================================================
042 *
043 * This software consists of voluntary contributions made by many
044 * individuals on behalf of the Apache Software Foundation.  For more
045 * information on the Apache Software Foundation, please see
046 * <http://www.apache.org/>.
047 *
048 */
049
050import java.lang.ref.SoftReference;
051import java.text.ParsePosition;
052import java.text.SimpleDateFormat;
053import java.util.Calendar;
054import java.util.Date;
055import java.util.HashMap;
056import java.util.Locale;
057import java.util.Map;
058import java.util.TimeZone;
059
060/**
061 * A utility class for parsing and formatting HTTP dates as used in cookies and
062 * other headers.  This class handles dates as defined by RFC 2616 section
063 * 3.3.1 as well as some other common non-standard formats.
064 */
065public final class DateUtils {
066
067    /**
068     * Date format pattern used to parse HTTP date headers in RFC 1123 format.
069     */
070    public static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
071
072    /**
073     * Date format pattern used to parse HTTP date headers in RFC 1036 format.
074     */
075    public static final String PATTERN_RFC1036 = "EEE, dd-MMM-yy HH:mm:ss zzz";
076
077    /**
078     * Date format pattern used to parse HTTP date headers in ANSI C
079     * {@code asctime()} format.
080     */
081    public static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy";
082
083    private static final String[] DEFAULT_PATTERNS = new String[] {
084        PATTERN_RFC1123,
085        PATTERN_RFC1036,
086        PATTERN_ASCTIME
087    };
088
089    private static final Date DEFAULT_TWO_DIGIT_YEAR_START;
090
091    public static final TimeZone GMT = TimeZone.getTimeZone("GMT");
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     * Parses a date value.  The formats used for parsing the date value are retrieved from
103     * the default http params.
104     *
105     * @param dateValue the date value to parse
106     *
107     * @return the parsed date or null if input could not be parsed
108     */
109    public static Date parseDate(final String dateValue) {
110        return parseDate(dateValue, null, null);
111    }
112
113    /**
114     * Parses the date value using the given date formats.
115     *
116     * @param dateValue the date value to parse
117     * @param dateFormats the date formats to use
118     *
119     * @return the parsed date or null if input could not be parsed
120     */
121    public static Date parseDate(final String dateValue, final String[] dateFormats) {
122        return parseDate(dateValue, dateFormats, null);
123    }
124
125    /**
126     * Parses the date value using the given date formats.
127     *
128     * @param dateValue the date value to parse
129     * @param dateFormats the date formats to use
130     * @param startDate During parsing, two digit years will be placed in the range
131     * {@code startDate} to {@code startDate + 100 years}. This value may
132     * be {@code null}. When {@code null} is given as a parameter, year
133     * {@code 2000} will be used.
134     *
135     * @return the parsed date or null if input could not be parsed
136     */
137    public static Date parseDate(
138            final String dateValue,
139            final String[] dateFormats,
140            final Date startDate) {
141        notNull(dateValue, "Date value");
142        final String[] localDateFormats = dateFormats != null ? dateFormats : DEFAULT_PATTERNS;
143        final Date localStartDate = startDate != null ? startDate : DEFAULT_TWO_DIGIT_YEAR_START;
144        String v = dateValue;
145        // trim single quotes around date if present
146        // see issue #5279
147        if (v.length() > 1 && v.startsWith("'") && v.endsWith("'")) {
148            v = v.substring (1, v.length() - 1);
149        }
150
151        for (final String dateFormat : localDateFormats) {
152            final SimpleDateFormat dateParser = DateFormatHolder.formatFor(dateFormat);
153            dateParser.set2DigitYearStart(localStartDate);
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    /**
164     * Formats the given date according to the RFC 1123 pattern.
165     *
166     * @param date The date to format.
167     * @return An RFC 1123 formatted date string.
168     *
169     * @see #PATTERN_RFC1123
170     */
171    public static String formatDate(final Date date) {
172        return formatDate(date, PATTERN_RFC1123);
173    }
174
175    /**
176     * Formats the given date according to the specified pattern.  The pattern
177     * must conform to that used by the {@link SimpleDateFormat simple date
178     * format} class.
179     *
180     * @param date The date to format.
181     * @param pattern The pattern to use for formatting the date.
182     * @return A formatted date string.
183     *
184     * @throws IllegalArgumentException If the given date pattern is invalid.
185     *
186     * @see SimpleDateFormat
187     */
188    public static String formatDate(final Date date, final String pattern) {
189        notNull(date, "Date");
190        notNull(pattern, "Pattern");
191        final SimpleDateFormat formatter = DateFormatHolder.formatFor(pattern);
192        return formatter.format(date);
193    }
194    
195
196    public static <T> T notNull(final T argument, final String name) {
197        if (argument == null) {
198            throw new IllegalArgumentException(name + " may not be null");
199        }
200        return argument;
201    }    
202
203    /**
204     * Clears thread-local variable containing {@link java.text.DateFormat} cache.
205     *
206     * @since 4.3
207     */
208    public static void clearThreadLocal() {
209        DateFormatHolder.clearThreadLocal();
210    }
211
212    /** This class should not be instantiated. */
213    private DateUtils() {
214    }
215
216    /**
217     * A factory for {@link SimpleDateFormat}s. The instances are stored in a
218     * threadlocal way because SimpleDateFormat is not threadsafe as noted in
219     * {@link SimpleDateFormat its javadoc}.
220     *
221     */
222    final static class DateFormatHolder {
223
224        private static final ThreadLocal<SoftReference<Map<String, SimpleDateFormat>>>
225            THREADLOCAL_FORMATS = new ThreadLocal<SoftReference<Map<String, SimpleDateFormat>>>() {
226
227            @Override
228            protected SoftReference<Map<String, SimpleDateFormat>> initialValue() {
229                return new SoftReference<Map<String, SimpleDateFormat>>(
230                        new HashMap<String, SimpleDateFormat>());
231            }
232
233        };
234
235        /**
236         * creates a {@link SimpleDateFormat} for the requested format string.
237         *
238         * @param pattern
239         *            a non-{@code null} format String according to
240         *            {@link SimpleDateFormat}. The format is not checked against
241         *            {@code null} since all paths go through
242         *            {@link DateUtils}.
243         * @return the requested format. This simple dateformat should not be used
244         *         to {@link SimpleDateFormat#applyPattern(String) apply} to a
245         *         different pattern.
246         */
247        public static SimpleDateFormat formatFor(final String pattern) {
248            final SoftReference<Map<String, SimpleDateFormat>> ref = THREADLOCAL_FORMATS.get();
249            Map<String, SimpleDateFormat> formats = ref.get();
250            if (formats == null) {
251                formats = new HashMap<String, SimpleDateFormat>();
252                THREADLOCAL_FORMATS.set(
253                        new SoftReference<Map<String, SimpleDateFormat>>(formats));
254            }
255
256            SimpleDateFormat format = formats.get(pattern);
257            if (format == null) {
258                format = new SimpleDateFormat(pattern, Locale.US);
259                format.setTimeZone(TimeZone.getTimeZone("GMT"));
260                formats.put(pattern, format);
261            }
262
263            return format;
264        }
265
266        public static void clearThreadLocal() {
267            THREADLOCAL_FORMATS.remove();
268        }
269
270    }
271
272}