blob: aa945139ddf92540b259d83391b03d68ac6949e6 [file] [log] [blame]
// ***************************************************************************************************************************
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
// * to you 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 org.apache.juneau.utils;
import static org.apache.juneau.internal.DateUtils.*;
import static org.apache.juneau.internal.StringUtils.*;
import java.text.*;
import java.util.*;
import java.util.concurrent.*;
import javax.xml.bind.*;
/**
* Utility class for converting {@link Calendar} and {@link Date} objects to common serialized forms.
*/
public class CalendarUtils {
private static final TimeZone GMT = TimeZone.getTimeZone("GMT");
/**
* Valid conversion formats.
*/
public static enum Format {
/**
* Transform to ISO8601 date-time-local strings.
*
* <h5 class='section'>Example Output:</h5>
* <ul>
* <li><js>"2001-07-04T15:30:45"</js>
* </ul>
*
* <h5 class='topic'>Example input:</h5>
* <ul>
* <li><js>"2001-07-04T15:30:45"</js>
* <li><js>"2001-07-04T15:30:45.1"</js>
* <li><js>"2001-07-04T15:30"</js>
* <li><js>"2001-07-04"</js>
* <li><js>"2001-07"</js>
* <li><js>"2001"</js>
* </ul>
*/
ISO8601_DTL,
/**
* Transform to ISO8601 date-time strings.
*
* <h5 class='section'>Example Output:</h5>
* <ul>
* <li><js>"2001-07-04T15:30:45-05:00"</js>
* <li><js>"2001-07-04T15:30:45Z"</js>
* </ul>
*
* <h5 class='topic'>Example input:</h5>
* <ul>
* <li><js>"2001-07-04T15:30:45-05:00"</js>
* <li><js>"2001-07-04T15:30:45Z"</js>
* <li><js>"2001-07-04T15:30:45.1Z"</js>
* <li><js>"2001-07-04T15:30Z"</js>
* <li><js>"2001-07-04"</js>
* <li><js>"2001-07"</js>
* <li><js>"2001"</js>
* </ul>
*/
ISO8601_DT,
/**
* Same as {@link CalendarUtils.Format#ISO8601_DT}, except always serializes in GMT.
*
* <h5 class='section'>Example Output:</h5>
* <js>"2001-07-04T15:30:45Z"</js>
*/
ISO8601_DTZ,
/**
* Same as {@link CalendarUtils.Format#ISO8601_DT} except serializes to millisecond precision.
*
* <h5 class='section'>Example Output:</h5>
* <js>"2001-07-04T15:30:45.123Z"</js>
*/
ISO8601_DTP,
/**
* Same as {@link CalendarUtils.Format#ISO8601_DTZ} except serializes to millisecond precision.
*
* <h5 class='section'>Example Output:</h5>
* <js>"2001-07-04T15:30:45.123"</js>
*/
ISO8601_DTPZ,
/**
* ISO8601 date only.
*
* <h5 class='section'>Example Output:</h5>
* <js>"2001-07-04"</js>
*/
ISO8601_D,
/**
* Transform to {@link String Strings} using the {@code Date.toString()} method.
*
* <h5 class='section'>Example Output:</h5>
* <ul>
* <li><js>"Wed Jul 04 15:30:45 EST 2001"</js>
* </ul>
*/
TO_STRING,
/**
* Transform to RFC2822 date-time strings.
*
* <h5 class='section'>Example Output:</h5>
* <ul>
* <li><js>"Sat, 03 Mar 2001 10:11:12 +0000"</js> <jc>// en_US</jc>
* <li><js>"土, 03 3 2001 10:11:12 +0000"</js> <jc>// ja_JP</jc>
* <li><js>"토, 03 3월 2001 10:11:12 +0000"</js> <jc>// ko_KR</jc>
* </ul>
*/
RFC2822_DT,
/**
* Same as {@link CalendarUtils.Format#RFC2822_DT}, except always serializes in GMT.
*
* <h5 class='section'>Example Output:</h5>
* <ul>
* <li><js>"Sat, 03 Mar 2001 10:11:12 GMT"</js> <jc>// en_US</jc>
* <li><js>"土, 03 3 2001 10:11:12 GMT"</js> <jc>// ja_JP</jc>
* <li><js>"토, 03 3월 2001 10:11:12 GMT"</js> <jc>// ko_KR</jc>
* </ul>
*/
RFC2822_DTZ,
/**
* Transform to RFC2822 date strings.
*
* <h5 class='section'>Example Output:</h5>
* <ul>
* <li><js>"03 Mar 2001"</js> <jc>// en_US</jc>
* <li><js>"03 3 2001"</js> <jc>// ja_JP</jc>
* <li><js>"03 3월 2001"</js> <jc>// ko_KR</jc>
* </ul>
*/
RFC2822_D,
/**
* Transform to simple <js>"yyyy/MM/dd HH:mm:ss"</js> date-time strings.
*
* <h5 class='section'>Example Output:</h5>
* <ul>
* <li><js>"2001/03/03 10:11:12"</js>
* </ul>
*/
SIMPLE_DT,
/**
* Transform to simple <js>"yyyy/MM/dd"</js> date strings.
*
* <h5 class='section'>Example Output:</h5>
* <ul>
* <li><js>"2001/03/03"</js>
* </ul>
*/
SIMPLE_D,
/**
* Transform to simple <js>"HH:mm:ss"</js> time strings.
*
* <h5 class='section'>Example Output:</h5>
* <ul>
* <li><js>"10:11:12"</js>
* </ul>
*/
SIMPLE_T,
/**
* Transform to {@link DateFormat#FULL} date strings.
*
* <h5 class='section'>Example Output:</h5>
* <ul>
* <li><js>"Saturday, March 3, 2001"</js> <jc>// en_US</jc>
* <li><js>"2001年3月3日"</js> <jc>// ja_JP</jc>
* <li><js>"2001년 3월 3일 토요일"</js> <jc>// ko_KR</jc>
* </ul>
*/
FULL_D,
/**
* Transform to {@link DateFormat#LONG} date strings.
*
* <h5 class='section'>Example Output:</h5>
* <ul>
* <li><js>"March 3, 2001"</js> <jc>// en_US</jc>
* <li><js>"2001/03/03"</js> <jc>// ja_JP</jc>
* <li><js>"2001년 3월 3일 (토)"</js> <jc>// ko_KR</jc>
* </ul>
*/
LONG_D,
/**
* Transform to {@link DateFormat#MEDIUM} date strings.
*
* <h5 class='section'>Example Output:</h5>
* <ul>
* <li><js>"Mar 3, 2001"</js> <jc>// en_US</jc>
* <li><js>"2001/03/03"</js> <jc>// ja_JP</jc>
* <li><js>"2001. 3. 3"</js> <jc>// ko_KR</jc>
* </ul>
*/
MEDIUM_D,
/**
* Transform to {@link DateFormat#SHORT} date strings.
*
* <h5 class='section'>Example Output:</h5>
* <ul>
* <li><js>"3/3/01"</js> <jc>// en_US</jc>
* <li><js>"01/03/03"</js> <jc>// ja_JP</jc>
* <li><js>"01. 3. 3"</js> <jc>// ko_KR</jc>
* </ul>
*/
SHORT_D,
/**
* Transform to {@link DateFormat#FULL} time strings.
*
* <h5 class='section'>Example Output:</h5>
* <ul>
* <li><js>"10:11:12 AM GMT"</js> <jc>// en_US</jc>
* <li><js>"10時11分12秒 GMT"</js> <jc>// ja_JP</jc>
* <li><js>"오전 10시 11분 12초 GMT"</js> <jc>// ko_KR</jc>
* </ul>
*/
FULL_T,
/**
* Transform to {@link DateFormat#LONG} time strings.
*
* <h5 class='section'>Example Output:</h5>
* <ul>
* <li><js>"10:11:12 AM GMT"</js> <jc>// en_US</jc>
* <li><js>"10:11:12 GMT"</js> <jc>// ja_JP</jc>
* <li><js>"오전 10시 11분 12초"</js> <jc>// ko_KR</jc>
* </ul>
*/
LONG_T,
/**
* Transform to {@link DateFormat#MEDIUM} time strings.
*
* <h5 class='section'>Example Output:</h5>
* <ul>
* <li><js>"10:11:12 AM"</js> <jc>// en_US</jc>
* <li><js>"10:11:12"</js> <jc>// ja_JP</jc>
* <li><js>"오전 10:11:12"</js> <jc>// ko_KR</jc>
* </ul>
*/
MEDIUM_T,
/**
* Transform to {@link DateFormat#SHORT} time strings.
*
* <h5 class='section'>Example Output:</h5>
* <ul>
* <li><js>"10:11 AM"</js> <jc>// en_US</jc>
* <li><js>"10:11 AM"</js> <jc>// ja_JP</jc>
* <li><js>"오전 10:11"</js> <jc>// ko_KR</jc>
* </ul>
*/
SHORT_T,
/**
* Transform to {@link DateFormat#FULL} date-time strings.
*
* <h5 class='section'>Example Output:</h5>
* <ul>
* <li><js>"Saturday, March 3, 2001 10:11:12 AM GMT"</js> <jc>// en_US</jc>
* <li><js>"2001年3月3日 10時11分12秒 GMT"</js> <jc>// ja_JP</jc>
* <li><js>"2001년 3월 3일 토요일 오전 10시 11분 12초 GMT"</js> <jc>// ko_KR</jc>
* </ul>
*/
FULL_DT,
/**
* Transform to {@link DateFormat#LONG} date-time strings.
*
* <h5 class='section'>Example Output:</h5>
* <ul>
* <li><js>"March 3, 2001 10:11:12 AM GMT"</js> <jc>// en_US</jc>
* <li><js>"2001/03/03 10:11:12 GMT"</js> <jc>// ja_JP</jc>
* <li><js>"2001년 3월 3일 (토) 오전 10시 11분 12초"</js> <jc>// ko_KR</jc>
* </ul>
*/
LONG_DT,
/**
* Transform to {@link DateFormat#MEDIUM} date-time strings.
*
* <h5 class='section'>Example Output:</h5>
* <ul>
* <li><js>"Mar 3, 2001 10:11:12 AM"</js> <jc>// en_US</jc>
* <li><js>"2001/03/03 10:11:12"</js> <jc>// ja_JP</jc>
* <li><js>"2001. 3. 3 오전 10:11:12"</js> <jc>// ko_KR</jc>
* </ul>
*/
MEDIUM_DT,
/**
* Transform to {@link DateFormat#SHORT} date-time strings.
*
* <h5 class='section'>Example Output:</h5>
* <ul>
* <li><js>"3/3/01 10:11 AM"</js> <jc>// en_US</jc>
* <li><js>"01/03/03 10:11"</js> <jc>// ja_JP</jc>
* <li><js>"01. 3. 3 오전 10:11"</js> <jc>// ko_KR</jc>
* </ul>
*/
SHORT_DT
}
private static ThreadLocal<Map<DateFormatKey,DateFormat>> patternCache = new ThreadLocal<>();
static class DateFormatKey {
final CalendarUtils.Format format;
final Locale locale;
final TimeZone timeZone;
final int hashCode;
DateFormatKey(CalendarUtils.Format format, Locale locale, TimeZone timeZone) {
this.format = format;
this.locale = locale;
this.timeZone = timeZone;
this.hashCode = format.hashCode() + locale.hashCode() + timeZone.hashCode();
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object o) {
if (o == null)
return false;
DateFormatKey key = (DateFormatKey)o;
return format.equals(key.format) && locale.equals(key.locale) && timeZone.equals(key.timeZone);
}
}
private static DateFormat getFormat(CalendarUtils.Format format, Locale locale, TimeZone timeZone) {
if (locale == null)
locale = Locale.getDefault();
if (timeZone == null)
timeZone = TimeZone.getDefault();
DateFormatKey key = new DateFormatKey(format, locale, timeZone);
Map<DateFormatKey,DateFormat> m1 = patternCache.get();
if (m1 == null) {
m1 = new ConcurrentHashMap<>();
patternCache.set(m1);
}
DateFormat df = m1.get(key);
if (df == null) {
String p = null;
switch (format) {
case ISO8601_DTL: p = "yyyy-MM-dd'T'HH:mm:ss"; break;
case ISO8601_D: p = "yyyy-MM-dd"; break;
case TO_STRING: p = "EEE MMM dd HH:mm:ss zzz yyyy"; break;
case RFC2822_DT: p = "EEE, dd MMM yyyy HH:mm:ss Z"; break;
case RFC2822_DTZ: p = "EEE, dd MMM yyyy HH:mm:ss 'GMT'"; break;
case RFC2822_D: p = "dd MMM yyyy"; break;
case SIMPLE_DT: p = "yyyy/MM/dd HH:mm:ss"; break;
case SIMPLE_D: p = "yyyy/MM/dd"; break;
case SIMPLE_T: p = "HH:mm:ss"; break;
case FULL_D: df = DateFormat.getDateInstance(DateFormat.FULL, locale); break;
case LONG_D: df = DateFormat.getDateInstance(DateFormat.LONG, locale); break;
case MEDIUM_D: df = DateFormat.getDateInstance(DateFormat.MEDIUM, locale); break;
case SHORT_D: df = DateFormat.getDateInstance(DateFormat.SHORT, locale); break;
case FULL_T: df = DateFormat.getTimeInstance(DateFormat.FULL, locale); break;
case LONG_T: df = DateFormat.getTimeInstance(DateFormat.LONG, locale); break;
case MEDIUM_T: df = DateFormat.getTimeInstance(DateFormat.MEDIUM, locale); break;
case SHORT_T: df = DateFormat.getTimeInstance(DateFormat.SHORT, locale); break;
case FULL_DT: df = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL, locale); break;
case LONG_DT: df = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale); break;
case MEDIUM_DT: df = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, locale); break;
case SHORT_DT: df = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale); break;
default: return null;
}
if (p != null) {
df = new SimpleDateFormat(p, locale);
}
if (df != null)
df.setTimeZone(timeZone);
m1.put(key, df);
}
return df;
}
/**
* Converts the specified calendar to a string of the specified format.
*
* @param c The calendar to serialize.
* @param format The date format.
* @param locale The locale to use. If <jk>null</jk>, uses {@link Locale#getDefault()}.
* @param timeZone The time zone to use. If <jk>null</jk>, uses {@link TimeZone#getDefault()}.
* @return The serialized date, or <jk>null</jk> if the calendar was <jk>null</jk>.
*/
public static final String serialize(Calendar c, CalendarUtils.Format format, Locale locale, TimeZone timeZone) {
if (c == null)
return null;
if (timeZone == null)
timeZone = c.getTimeZone();
switch(format) {
case ISO8601_DTL:
case ISO8601_D:
case RFC2822_D:
case RFC2822_DT:
case TO_STRING:
case FULL_D:
case FULL_DT:
case FULL_T:
case LONG_D:
case LONG_DT:
case LONG_T:
case MEDIUM_D:
case MEDIUM_DT:
case MEDIUM_T:
case SHORT_D:
case SHORT_DT:
case SHORT_T:
case SIMPLE_D:
case SIMPLE_DT:
case SIMPLE_T:
return serializeFromDateFormat(c.getTime(), format, locale, timeZone);
case ISO8601_DT:
return DatatypeConverter.printDateTime(setTimeZone(c, timeZone));
case ISO8601_DTP:
String s = DatatypeConverter.printDateTime(setTimeZone(c, timeZone));
return String.format("%s.%03d%s", s.substring(0, 19), c.get(Calendar.MILLISECOND), s.substring(19));
case ISO8601_DTZ:
if (c.getTimeZone().getRawOffset() != 0) {
Calendar c2 = Calendar.getInstance(GMT);
c2.setTime(c.getTime());
c = c2;
}
return DatatypeConverter.printDateTime(c);
case ISO8601_DTPZ:
if (c.getTimeZone().getRawOffset() != 0) {
Calendar c2 = Calendar.getInstance(GMT);
c2.setTime(c.getTime());
c = c2;
}
s = DatatypeConverter.printDateTime(c);
return String.format("%s.%03d%s", s.substring(0, 19), c.get(Calendar.MILLISECOND), s.substring(19));
case RFC2822_DTZ:
return serializeFromDateFormat(c.getTime(), format, locale, GMT);
default:
break;
}
return null;
}
/**
* Converts the specified date to a string of the specified format.
*
* @param format The date format.
* @param d The date to serialize.
* @param locale The locale to use. If <jk>null</jk>, uses {@link Locale#getDefault()}.
* @param timeZone The time zone to use. If <jk>null</jk>, uses {@link TimeZone#getDefault()}.
* @return The serialized date, or <jk>null</jk> if the calendar was <jk>null</jk>.
*/
public static final String serialize(Date d, CalendarUtils.Format format, Locale locale, TimeZone timeZone) {
if (d == null)
return null;
if (timeZone == null)
timeZone = TimeZone.getDefault();
switch(format) {
case ISO8601_DTL:
case ISO8601_D:
case RFC2822_D:
case RFC2822_DT:
case TO_STRING:
case FULL_D:
case FULL_DT:
case FULL_T:
case LONG_D:
case LONG_DT:
case LONG_T:
case MEDIUM_D:
case MEDIUM_DT:
case MEDIUM_T:
case SHORT_D:
case SHORT_DT:
case SHORT_T:
case SIMPLE_D:
case SIMPLE_DT:
case SIMPLE_T:
return serializeFromDateFormat(d, format, locale, timeZone);
case ISO8601_DT:
Calendar c = new GregorianCalendar();
c.setTime(d);
c.setTimeZone(timeZone);
return DatatypeConverter.printDateTime(c);
case ISO8601_DTP:
c = new GregorianCalendar();
c.setTime(d);
c.setTimeZone(timeZone);
String s = DatatypeConverter.printDateTime(setTimeZone(c, timeZone));
return String.format("%s.%03d%s", s.substring(0, 19), c.get(Calendar.MILLISECOND), s.substring(19));
case ISO8601_DTZ:
c = new GregorianCalendar();
c.setTime(d);
c.setTimeZone(GMT);
return DatatypeConverter.printDateTime(c);
case ISO8601_DTPZ:
c = new GregorianCalendar();
c.setTime(d);
c.setTimeZone(GMT);
s = DatatypeConverter.printDateTime(c);
return String.format("%s.%03d%s", s.substring(0, 19), c.get(Calendar.MILLISECOND), s.substring(19));
case RFC2822_DTZ:
return serializeFromDateFormat(d, format, locale, GMT);
}
return null;
}
/**
* Converts the specified serialized date back into a {@link Calendar} object.
*
* @param format The date format.
* @param in The serialized date.
* @param locale
* The locale to use.
* If <jk>null</jk>, uses {@link Locale#getDefault()}.
* @param timeZone
* The timezone to assume if input string doesn't contain timezone info.
* If <jk>null</jk>, uses {@link TimeZone#getDefault()}.
* @return The date as a {@link Calendar}, or <jk>null</jk> if the input was <jk>null</jk> or empty.
* @throws java.text.ParseException Malformed input encountered.
*/
public static final Calendar parseCalendar(String in, CalendarUtils.Format format, Locale locale, TimeZone timeZone) throws java.text.ParseException {
if (isEmpty(in))
return null;
if (timeZone == null)
timeZone = TimeZone.getDefault();
Date d = null;
switch(format) {
// These use DatatypeConverter to parse the date.
case ISO8601_DTL:
case ISO8601_DT:
case ISO8601_DTZ:
case ISO8601_DTP:
case ISO8601_DTPZ:
case ISO8601_D:
return DatatypeConverter.parseDateTime(toValidISO8601DT(in));
// These don't specify timezones, so we have to assume the timezone is whatever is specified.
case RFC2822_D:
case SIMPLE_DT:
case SIMPLE_D:
case SIMPLE_T:
case FULL_D:
case LONG_D:
case MEDIUM_D:
case SHORT_D:
case MEDIUM_T:
case SHORT_T:
case MEDIUM_DT:
case SHORT_DT:
d = getFormat(format, locale, GMT).parse(in);
d.setTime(d.getTime() - timeZone.getRawOffset());
break;
// This is always in GMT.
case RFC2822_DTZ:
DateFormat f = getFormat(format, locale, GMT);
d = f.parse(in);
break;
// These specify timezones in the strings, so we don't use the specified timezone.
case TO_STRING:
case FULL_DT:
case FULL_T:
case LONG_DT:
case LONG_T:
case RFC2822_DT:
d = getFormat(format, locale, timeZone).parse(in);
break;
}
if (d == null)
return null;
Calendar c = new GregorianCalendar();
c.setTime(d);
c.setTimeZone(timeZone);
return c;
}
/**
* Converts the specified serialized date back into a {@link Date} object.
*
* @param format The date format.
* @param in The serialized date.
* @param locale
* The locale to use.
* If <jk>null</jk>, uses {@link Locale#getDefault()}.
* @param timeZone
* The timezone to assume if input string doesn't contain timezone info.
* If <jk>null</jk>, uses {@link TimeZone#getDefault()}.
* @return The date as a {@link Date}, or <jk>null</jk> if the input was <jk>null</jk> or empty.
* @throws java.text.ParseException Malformed input encountered.
*/
public static final Date parseDate(String in, CalendarUtils.Format format, Locale locale, TimeZone timeZone) throws java.text.ParseException {
if (isEmpty(in))
return null;
if (timeZone == null)
timeZone = TimeZone.getDefault();
switch(format) {
// These use DatatypeConverter to parse the date.
case ISO8601_DTL:
case ISO8601_D:
case ISO8601_DT:
case ISO8601_DTZ:
case ISO8601_DTP:
case ISO8601_DTPZ:
return DatatypeConverter.parseDateTime(toValidISO8601DT(in)).getTime();
// These don't specify timezones, so we have to assume the timezone is whatever is specified.
case FULL_D:
case LONG_D:
case MEDIUM_D:
case MEDIUM_DT:
case MEDIUM_T:
case RFC2822_D:
case SHORT_D:
case SHORT_DT:
case SHORT_T:
case SIMPLE_D:
case SIMPLE_DT:
case SIMPLE_T:
return getFormat(format, locale, timeZone).parse(in);
// This is always in GMT.
case RFC2822_DTZ:
Date d = getFormat(format, locale, TimeZone.getDefault()).parse(in);
d.setTime(d.getTime() + TimeZone.getDefault().getRawOffset());
return d;
// These specify timezones in the strings, so we don't use the specified timezone.
case TO_STRING:
case FULL_DT:
case FULL_T:
case LONG_DT:
case LONG_T:
case RFC2822_DT:
return getFormat(format, locale, timeZone).parse(in);
}
return null;
}
private static String serializeFromDateFormat(Date date, CalendarUtils.Format format, Locale locale, TimeZone timeZone) {
DateFormat df = getFormat(format, locale, timeZone);
String s = df.format(date);
return s;
}
private static Calendar setTimeZone(Calendar c, TimeZone tz) {
if (tz != null && ! tz.equals(c.getTimeZone())) {
c = (Calendar)c.clone();
c.setTimeZone(tz);
}
return c;
}
}