/** * Copyright 2005-2014 Restlet * * The contents of this file are subject to the terms of one of the following * open source licenses: Apache 2.0 or or EPL 1.0 (the "Licenses"). You can * select the license that you prefer but you may not use this file except in * compliance with one of these Licenses. * * You can obtain a copy of the Apache 2.0 license at * http://www.opensource.org/licenses/apache-2.0 * * You can obtain a copy of the EPL 1.0 license at * http://www.opensource.org/licenses/eclipse-1.0 * * See the Licenses for the specific language governing permissions and * limitations under the Licenses. * * Alternatively, you can obtain a royalty free commercial license with less * limitations, transferable or non-transferable, directly at * http://restlet.com/products/restlet-framework * * Restlet is a registered trademark of Restlet S.A.S. */ package org.restlet.engine.util; import java.text.DateFormat; import java.text.DecimalFormat; import java.text.FieldPosition; import java.text.ParseException; import java.text.ParsePosition; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.SimpleTimeZone; import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * This class handles Internet date/time strings in accordance with RFC 3339. It * provides static methods to convert from various Java constructs (long, Date, * and Calendar) to RFC 3339 format strings and to parse these strings back into * the same Java constructs. * <p> * In addition to the static utility methods, this class also wraps a Calendar * object allowing this class to be used as a value object in place of a Java * construct. * <p> * Strings are parsed in accordance with the RFC 3339 format: * * <pre> * YYYY-MM-DD(T|t|\s)hh:mm:ss[.ddd][tzd] * </pre> * * The <code>tzd</code> represents the time zone designator and is either an * upper or lower case 'Z' indicating UTC or a signed <code>hh:mm</code> offset. * * @author Frank Hellwig (frank@hellwig.org) */ public class InternetDateFormat extends DateFormat { private static volatile DecimalFormat df2 = new DecimalFormat("00"); private static volatile DecimalFormat df4 = new DecimalFormat("0000"); /** The Regex pattern to match. */ private static volatile Pattern pattern; private static final long serialVersionUID = 1L; /** * A time zone with zero offset and no DST. */ public static final TimeZone UTC = new SimpleTimeZone(0, "Z"); static { String reDate = "(\\d{4})-(\\d{2})-(\\d{2})"; String reTime = "(\\d{2}):(\\d{2}):(\\d{2})(\\.\\d+)?"; String reZone = "(?:([zZ])|(?:(\\+|\\-)(\\d{2}):(\\d{2})))"; String re = reDate + "[tT\\s]" + reTime + reZone; pattern = Pattern.compile(re); } /** * Returns the current date and time as an RFC 3339 date/time string using * the UTC (Z) time zone. * * @return an RFC 3339 date/time string (does not include milliseconds) */ public static String now() { return now(UTC); } /** * Returns the current date and time as an RFC 3339 date/time string using * the specified time zone. * * @param zone * the time zone to use * @return an RFC 3339 date/time string (does not include milliseconds) */ public static String now(TimeZone zone) { return toString(System.currentTimeMillis(), zone); } /** * Our private parse utility that parses the string, clears the calendar, * and then sets the fields. * * @param s * the string to parse * @param cal * the calendar object to populate * @throws IllegalArgumentException * if the string is not a valid RFC 3339 date/time string */ private static void parse(String s, Calendar cal) { Matcher m = pattern.matcher(s); if (!m.matches()) { throw new IllegalArgumentException("Invalid date/time: " + s); } cal.clear(); cal.set(Calendar.YEAR, Integer.parseInt(m.group(1))); cal.set(Calendar.MONTH, Integer.parseInt(m.group(2)) - 1); cal.set(Calendar.DAY_OF_MONTH, Integer.parseInt(m.group(3))); cal.set(Calendar.HOUR_OF_DAY, Integer.parseInt(m.group(4))); cal.set(Calendar.MINUTE, Integer.parseInt(m.group(5))); cal.set(Calendar.SECOND, Integer.parseInt(m.group(6))); if (m.group(7) != null) { float fraction = Float.parseFloat(m.group(7)); cal.set(Calendar.MILLISECOND, (int) (fraction * 1000F)); } if (m.group(8) != null) { cal.setTimeZone(new SimpleTimeZone(0, "Z")); } else { int sign = m.group(9).equals("-") ? -1 : 1; int tzhour; tzhour = Integer.parseInt(m.group(10)); int tzminute = Integer.parseInt(m.group(11)); int offset = sign * ((tzhour * 60) + tzminute); String id = Integer.toString(offset); cal.setTimeZone(new SimpleTimeZone(offset * 60000, id)); } } /** * Parses an RFC 3339 date/time string to a Calendar object. * * @param s * the string to parse * @return the Calendar object * @throws IllegalArgumentException * if the string is not a valid RFC 3339 date/time string */ public static Calendar parseCalendar(String s) { Calendar cal = new GregorianCalendar(); parse(s, cal); return cal; } /** * Parses an RFC 3339 date/time string to a Date object. * * @param s * the string to parse * @return the Date object * @throws IllegalArgumentException * if the string is not a valid RFC 3339 date/time string */ public static Date parseDate(String s) { Calendar cal = new GregorianCalendar(); parse(s, cal); return cal.getTime(); } /** * Parses an RFC 3339 date/time string to a millisecond time value. * * @param s * the string to parse * @return the millisecond time value * @throws IllegalArgumentException * if the string is not a valid RFC 3339 date/time string */ public static long parseTime(String s) { Calendar cal = new GregorianCalendar(); parse(s, cal); return cal.getTimeInMillis(); } /** * Converts the specified Calendar object to an RFC 3339 date/time string. * Unlike the toString methods for Date and long, no additional variant of * this method taking a time zone is provided since the time zone is built * into the Calendar object. * * @param cal * the Calendar object * @return an RFC 3339 date/time string (does not include milliseconds) */ public static String toString(Calendar cal) { StringBuilder buf = new StringBuilder(); buf.append(df4.format(cal.get(Calendar.YEAR))); buf.append("-"); buf.append(df2.format(cal.get(Calendar.MONTH) + 1)); buf.append("-"); buf.append(df2.format(cal.get(Calendar.DAY_OF_MONTH))); buf.append("T"); buf.append(df2.format(cal.get(Calendar.HOUR_OF_DAY))); buf.append(":"); buf.append(df2.format(cal.get(Calendar.MINUTE))); buf.append(":"); buf.append(df2.format(cal.get(Calendar.SECOND))); int ms = cal.get(Calendar.MILLISECOND); if (ms != 0) { buf.append(".").append((int) (ms / 10F)); } int tzminute = (cal.get(Calendar.ZONE_OFFSET) + cal .get(Calendar.DST_OFFSET)) / 60000; if (tzminute == 0) { buf.append("Z"); } else { if (tzminute < 0) { tzminute = -tzminute; buf.append("-"); } else { buf.append("+"); } int tzhour = tzminute / 60; tzminute -= tzhour * 60; buf.append(df2.format(tzhour)); buf.append(":"); buf.append(df2.format(tzminute)); } return buf.toString(); } /** * Converts the specified Date object to an RFC 3339 date/time string using * the UTC (Z) time zone. * * @param date * the Date object * @return an RFC 3339 date/time string (does not include milliseconds) */ public static String toString(Date date) { return toString(date, UTC); } /** * Converts the specified Date object to an RFC 3339 date/time string using * the specified time zone. * * @param date * the Date object * @param zone * the time zone to use * @return an RFC 3339 date/time string (does not include milliseconds) */ public static String toString(Date date, TimeZone zone) { InternetDateFormat dt = new InternetDateFormat(date, zone); return dt.toString(); } /** * Converts the specified millisecond time value to an RFC 3339 date/time * string using the UTC (Z) time zone. * * @param time * the millisecond time value * @return an RFC 3339 date/time string (does not include milliseconds) */ public static String toString(long time) { return toString(time, UTC); } /** * Converts the specified millisecond time value to an RFC 3339 date/time * string using the specified time zone. * * @param time * the millisecond time value * @param zone * the time zone to use * @return an RFC 3339 date/time string (does not include milliseconds) */ public static String toString(long time, TimeZone zone) { InternetDateFormat dt = new InternetDateFormat(time, zone); return dt.toString(); } /** * Creates a new InternetDateFormat object from the specified Date object * using the UTC (Z) time zone. * * @param date * the Date object * @return the InternetDateFormat object */ public static InternetDateFormat valueOf(Date date) { return new InternetDateFormat(date); } /** * Creates a new InternetDateFormat object from the specified Date object * using the specified time zone. * * @param date * the Date object * @param zone * the time zone to use * @return the InternetDateFormat object */ public static InternetDateFormat valueOf(Date date, TimeZone zone) { return new InternetDateFormat(date, zone); } /** * Creates a new InternetDateFormat object from the specified millisecond * time value using the UTC (Z) time zone. * * @param time * the millisecond time value * @return the InternetDateFormat object */ public static InternetDateFormat valueOf(long time) { return new InternetDateFormat(time); } /** * Creates a new InternetDateFormat object from the specified millisecond * time value using the specified time zone. * * @param time * the millisecond time value * @param zone * the time zone to use * @return the InternetDateFormat object */ public static InternetDateFormat valueOf(long time, TimeZone zone) { return new InternetDateFormat(time, zone); } /** * Creates a new InternetDateFormat object by parsing an RFC 3339 date/time * string. * * @param s * the string to parse * @return the InternetDateFormat object * @throws IllegalArgumentException * if the string is not a valid RFC 3339 date/time string */ public static InternetDateFormat valueOf(String s) { return new InternetDateFormat(s); } /** * The Calendar object that allows this class to act as a value holder. */ private Calendar cal; /** * Creates a new InternetDateFormat object set to the current time using the * UTC (Z) time zone. */ public InternetDateFormat() { this(UTC); } /** * Creates a new InternetDateFormat object initialized from a Calendar * object. The specified calendar object is cloned thereby isolating this * InternetDateFormat object from any changes made to the specified calendar * object after calling this constructor. * * @param cal * the Calendar object */ public InternetDateFormat(Calendar cal) { this.cal = (Calendar) cal.clone(); } /** * Creates a new InternetDateFormat object initialized from a Date object * using the UTC (Z) time zone. * * @param date * the Date object */ public InternetDateFormat(Date date) { this(date, UTC); } /** * Creates a new InternetDateFormat object initialized from a Date object * using the specified time zone. * * @param date * the Date object * @param zone * the time zone to use */ public InternetDateFormat(Date date, TimeZone zone) { cal = new GregorianCalendar(zone); cal.setTime(date); } /** * Creates a new InternetDateFormat object initialized from a millisecond * time value using the UTC (Z) time zone. * * @param time * the millisecond time value */ public InternetDateFormat(long time) { this(time, UTC); } /** * Creates a new InternetDateFormat object initialized from a millisecond * time value using the specified time zone. * * @param time * the millisecond time value * @param zone * the time zone to use */ public InternetDateFormat(long time, TimeZone zone) { cal = new GregorianCalendar(zone); cal.setTimeInMillis(time); } /** * Creates a new InternetDateFormat object by parsing an RFC 3339 date/time * string. * * @param s * the string to parse * @throws IllegalArgumentException * if the string is not a valid RFC 3339 date/time string */ public InternetDateFormat(String s) { cal = parseCalendar(s); } /** * Creates a new InternetDateFormat object set to the current time using the * specified time zone. * * @param zone * the time zone to use */ public InternetDateFormat(TimeZone zone) { cal = new GregorianCalendar(zone); } @Override public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) { return toAppendTo.append(valueOf(date)); } /** * Gets the Calendar object wrapped by this InternetDateFormat object. * * @return the cloned Calendar object */ public Calendar getCalendar() { return (Calendar) cal.clone(); } /** * Gets the value of this InternetDateFormat object as a Date object. * * @return the Date object */ public Date getDate() { return cal.getTime(); } /** * Gets the value of this InternetDateFormat object as millisecond time * value. * * @return the millisecond time value */ public long getTime() { return cal.getTimeInMillis(); } @Override public Date parse(String source) throws ParseException { return parse(source, (ParsePosition) null); } @Override public Date parse(String source, ParsePosition pos) { return parseDate(source); } /** * Converts this InternetDateFormat object to an RFC 3339 date/time string. * * @return an RFC 3339 date/time string (does not include milliseconds) */ public String toString() { return toString(cal); } }