FastHttpDateFormat.java

/*
 * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.security.web.savedrequest;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.TimeZone;

import org.jspecify.annotations.Nullable;

/**
 * Utility class to generate HTTP dates.
 * <p>
 * This class is based on code in Apache Tomcat.
 *
 * @author Remy Maucherat
 * @author Andrey Grebnev
 */
public final class FastHttpDateFormat {

	/**
	 * HTTP date format.
	 */
	protected static final SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);

	/**
	 * The set of SimpleDateFormat formats to use in <code>getDateHeader()</code>.
	 */
	protected static final SimpleDateFormat[] formats = {
			new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US),
			new SimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US),
			new SimpleDateFormat("EEE MMMM d HH:mm:ss yyyy", Locale.US) };

	/**
	 * GMT time zone - all HTTP dates are on GMT
	 */
	protected static final TimeZone gmtZone = TimeZone.getTimeZone("GMT");

	static {
		format.setTimeZone(gmtZone);
		formats[0].setTimeZone(gmtZone);
		formats[1].setTimeZone(gmtZone);
		formats[2].setTimeZone(gmtZone);
	}

	/**
	 * Instant on which the currentDate object was generated.
	 */
	protected static long currentDateGenerated = 0L;

	/**
	 * Current formatted date.
	 */
	protected static @Nullable String currentDate = null;

	/**
	 * Formatter cache.
	 */
	protected static final HashMap<Long, String> formatCache = new HashMap<>();

	/**
	 * Parser cache.
	 */
	protected static final HashMap<String, Long> parseCache = new HashMap<>();

	private FastHttpDateFormat() {
	}

	/**
	 * Formats a specified date to HTTP format. If local format is not <code>null</code>,
	 * it's used instead.
	 * @param value Date value to format
	 * @param threadLocalformat The format to use (or <code>null</code> -- then HTTP
	 * format will be used)
	 * @return Formatted date
	 */
	public static String formatDate(long value, DateFormat threadLocalformat) {
		String cachedDate = null;
		Long longValue = value;
		try {
			cachedDate = formatCache.get(longValue);
		}
		catch (Exception ex) {
		}
		if (cachedDate != null) {
			return cachedDate;
		}
		String newDate;
		Date dateValue = new Date(value);
		if (threadLocalformat != null) {
			newDate = threadLocalformat.format(dateValue);
			synchronized (formatCache) {
				updateCache(formatCache, longValue, newDate);
			}
		}
		else {
			synchronized (formatCache) {
				newDate = format.format(dateValue);
				updateCache(formatCache, longValue, newDate);
			}
		}
		return newDate;
	}

	/**
	 * Gets the current date in HTTP format.
	 * @return Current date in HTTP format
	 */
	public static @Nullable String getCurrentDate() {
		long now = System.currentTimeMillis();
		if ((now - currentDateGenerated) > 1000) {
			synchronized (format) {
				if ((now - currentDateGenerated) > 1000) {
					currentDateGenerated = now;
					currentDate = format.format(new Date(now));
				}
			}
		}
		return currentDate;
	}

	/**
	 * Parses date with given formatters.
	 * @param value The string to parse
	 * @param formats Array of formats to use
	 * @return Parsed date (or <code>null</code> if no formatter mached)
	 */
	private static @Nullable Long internalParseDate(String value, DateFormat[] formats) {
		Date date = null;
		for (int i = 0; (date == null) && (i < formats.length); i++) {
			try {
				date = formats[i].parse(value);
			}
			catch (ParseException ex) {
			}
		}
		if (date == null) {
			return null;
		}
		return date.getTime();
	}

	/**
	 * Tries to parse the given date as an HTTP date. If local format list is not
	 * <code>null</code>, it's used instead.
	 * @param value The string to parse
	 * @param threadLocalformats Array of formats to use for parsing. If <code>null</code>
	 * , HTTP formats are used.
	 * @return Parsed date (or -1 if error occurred)
	 */
	public static long parseDate(String value, DateFormat[] threadLocalformats) {
		Long cachedDate = null;
		try {
			cachedDate = parseCache.get(value);
		}
		catch (Exception ex) {
		}
		if (cachedDate != null) {
			return cachedDate;
		}
		Long date;
		if (threadLocalformats != null) {
			date = internalParseDate(value, threadLocalformats);
			synchronized (parseCache) {
				updateCache(parseCache, value, date);
			}
		}
		else {
			synchronized (parseCache) {
				date = internalParseDate(value, formats);
				updateCache(parseCache, value, date);
			}
		}
		return (date != null) ? date : -1L;
	}

	/**
	 * Updates cache.
	 * @param cache Cache to be updated
	 * @param key Key to be updated
	 * @param value New value
	 */
	@SuppressWarnings("unchecked")
	private static void updateCache(HashMap cache, Object key, @Nullable Object value) {
		if (value == null) {
			return;
		}
		if (cache.size() > 1000) {
			cache.clear();
		}
		cache.put(key, value);
	}

}