/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * 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
 *
 *     http://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 io.undertow.util;

import io.undertow.UndertowOptions;
import io.undertow.server.HttpServerExchange;

import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

Utility for parsing and generating dates
Author:Stuart Douglas
/** * Utility for parsing and generating dates * * @author Stuart Douglas */
public class DateUtils { private static final Locale LOCALE_US = Locale.US; private static final TimeZone GMT_ZONE = TimeZone.getTimeZone("GMT"); private static final String RFC1123_PATTERN = "EEE, dd MMM yyyy HH:mm:ss z"; private static final AtomicReference<String> cachedDateString = new AtomicReference<>();
Thread local cache of this date format. This is technically a small memory leak, however in practice it is fine, as it will only be used by server threads.

This is the most common date format, which is why we cache it.
/** * Thread local cache of this date format. This is technically a small memory leak, however * in practice it is fine, as it will only be used by server threads. * <p/> * This is the most common date format, which is why we cache it. */
private static final ThreadLocal<SimpleDateFormat> RFC1123_PATTERN_FORMAT = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { SimpleDateFormat df = new SimpleDateFormat(RFC1123_PATTERN, LOCALE_US); return df; } };
Invalidates the current date
/** * Invalidates the current date */
private static final Runnable INVALIDATE_TASK = new Runnable() { @Override public void run() { cachedDateString.set(null); } }; private static final String RFC1036_PATTERN = "EEEEEEEEE, dd-MMM-yy HH:mm:ss z"; private static final String ASCITIME_PATTERN = "EEE MMM d HH:mm:ss yyyyy"; private static final String OLD_COOKIE_PATTERN = "EEE, dd-MMM-yyyy HH:mm:ss z"; private static final String COMMON_LOG_PATTERN = "[dd/MMM/yyyy:HH:mm:ss Z]"; private static final ThreadLocal<SimpleDateFormat> COMMON_LOG_PATTERN_FORMAT = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { SimpleDateFormat df = new SimpleDateFormat(COMMON_LOG_PATTERN, LOCALE_US); return df; } }; private static final ThreadLocal<SimpleDateFormat> OLD_COOKIE_FORMAT = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { SimpleDateFormat df = new SimpleDateFormat(OLD_COOKIE_PATTERN, LOCALE_US); df.setTimeZone(GMT_ZONE); return df; } };
Converts a date to a format suitable for use in a HTTP request
Params:
  • date – The date
Returns:The RFC-1123 formatted date
/** * Converts a date to a format suitable for use in a HTTP request * * @param date The date * @return The RFC-1123 formatted date */
public static String toDateString(final Date date) { SimpleDateFormat df = RFC1123_PATTERN_FORMAT.get(); //we always need to set the time zone //because date format is stupid, and calling parse() can mutate the timezone //see UNDERTOW-458 df.setTimeZone(GMT_ZONE); return df.format(date); } public static String toOldCookieDateString(final Date date) { return OLD_COOKIE_FORMAT.get().format(date); } public static String toCommonLogFormat(final Date date) { return COMMON_LOG_PATTERN_FORMAT.get().format(date); }
Attempts to pass a HTTP date.
Params:
  • date – The date to parse
Returns:The parsed date, or null if parsing failed
/** * Attempts to pass a HTTP date. * * @param date The date to parse * @return The parsed date, or null if parsing failed */
public static Date parseDate(final String date) { /* IE9 sends a superflous lenght parameter after date in the If-Modified-Since header, which needs to be stripped before parsing. */ final int semicolonIndex = date.indexOf(';'); final String trimmedDate = semicolonIndex >= 0 ? date.substring(0, semicolonIndex) : date; ParsePosition pp = new ParsePosition(0); SimpleDateFormat dateFormat = RFC1123_PATTERN_FORMAT.get(); dateFormat.setTimeZone(GMT_ZONE); Date val = dateFormat.parse(trimmedDate, pp); if (val != null && pp.getIndex() == trimmedDate.length()) { return val; } pp = new ParsePosition(0); dateFormat = new SimpleDateFormat(RFC1036_PATTERN, LOCALE_US); dateFormat.setTimeZone(GMT_ZONE); val = dateFormat.parse(trimmedDate, pp); if (val != null && pp.getIndex() == trimmedDate.length()) { return val; } pp = new ParsePosition(0); dateFormat = new SimpleDateFormat(ASCITIME_PATTERN, LOCALE_US); dateFormat.setTimeZone(GMT_ZONE); val = dateFormat.parse(trimmedDate, pp); if (val != null && pp.getIndex() == trimmedDate.length()) { return val; } pp = new ParsePosition(0); dateFormat = new SimpleDateFormat(OLD_COOKIE_PATTERN, LOCALE_US); dateFormat.setTimeZone(GMT_ZONE); val = dateFormat.parse(trimmedDate, pp); if (val != null && pp.getIndex() == trimmedDate.length()) { return val; } return null; }
Handles the if-modified-since header. returns true if the request should proceed, false otherwise
Params:
  • exchange – the exchange
  • lastModified – The last modified date
Returns:
/** * Handles the if-modified-since header. returns true if the request should proceed, false otherwise * * @param exchange the exchange * @param lastModified The last modified date * @return */
public static boolean handleIfModifiedSince(final HttpServerExchange exchange, final Date lastModified) { return handleIfModifiedSince(exchange.getRequestHeaders().getFirst(Headers.IF_MODIFIED_SINCE), lastModified); }
Handles the if-modified-since header. returns true if the request should proceed, false otherwise
Params:
  • modifiedSince – the modified since date
  • lastModified – The last modified date
Returns:
/** * Handles the if-modified-since header. returns true if the request should proceed, false otherwise * * @param modifiedSince the modified since date * @param lastModified The last modified date * @return */
public static boolean handleIfModifiedSince(final String modifiedSince, final Date lastModified) { if (lastModified == null) { return true; } if (modifiedSince == null) { return true; } Date modDate = parseDate(modifiedSince); if (modDate == null) { return true; } return lastModified.getTime() > (modDate.getTime() + 999); //UNDERTOW-341 +999 as there is no millisecond part in the if-modified-since }
Handles the if-unmodified-since header. returns true if the request should proceed, false otherwise
Params:
  • exchange – the exchange
  • lastModified – The last modified date
Returns:
/** * Handles the if-unmodified-since header. returns true if the request should proceed, false otherwise * * @param exchange the exchange * @param lastModified The last modified date * @return */
public static boolean handleIfUnmodifiedSince(final HttpServerExchange exchange, final Date lastModified) { return handleIfUnmodifiedSince(exchange.getRequestHeaders().getFirst(Headers.IF_UNMODIFIED_SINCE), lastModified); }
Handles the if-unmodified-since header. returns true if the request should proceed, false otherwise
Params:
  • modifiedSince – the if unmodified since date
  • lastModified – The last modified date
Returns:
/** * Handles the if-unmodified-since header. returns true if the request should proceed, false otherwise * * @param modifiedSince the if unmodified since date * @param lastModified The last modified date * @return */
public static boolean handleIfUnmodifiedSince(final String modifiedSince, final Date lastModified) { if (lastModified == null) { return true; } if (modifiedSince == null) { return true; } Date modDate = parseDate(modifiedSince); if (modDate == null) { return true; } return lastModified.getTime() < (modDate.getTime() + 999); //UNDERTOW-341 +999 as there is no millisecond part in the if-unmodified-since } public static void addDateHeaderIfRequired(HttpServerExchange exchange) { HeaderMap responseHeaders = exchange.getResponseHeaders(); if (exchange.getConnection().getUndertowOptions().get(UndertowOptions.ALWAYS_SET_DATE, true) && !responseHeaders.contains(Headers.DATE)) { String dateString = getCurrentDateTime(exchange); responseHeaders.put(Headers.DATE, dateString); } } public static String getCurrentDateTime(HttpServerExchange exchange) { String dateString = cachedDateString.get(); if (dateString == null) { //set the time and register a timer to invalidate it //note that this is racey, it does not matter if multiple threads do this //the perf cost of synchronizing would be more than the perf cost of multiple threads running it long realTime = System.currentTimeMillis(); long mod = realTime % 1000; long toGo = 1000 - mod; dateString = DateUtils.toDateString(new Date(realTime)); if (cachedDateString.compareAndSet(null, dateString)) { WorkerUtils.executeAfter(exchange.getIoThread(), INVALIDATE_TASK, toGo, TimeUnit.MILLISECONDS); } } return dateString; } private DateUtils() { } }