diff --git a/src/main/java/org/alfresco/util/CachingDateFormat.java b/src/main/java/org/alfresco/util/CachingDateFormat.java index 6232c4913f..446e9567ee 100644 --- a/src/main/java/org/alfresco/util/CachingDateFormat.java +++ b/src/main/java/org/alfresco/util/CachingDateFormat.java @@ -22,17 +22,13 @@ import java.text.NumberFormat; import java.text.ParseException; import java.text.ParsePosition; import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; -import java.util.Locale; -import java.util.Map; -import java.util.TimeZone; -import java.util.WeakHashMap; +import java.util.*; import org.joda.time.format.DateTimeFormatter; import org.joda.time.format.ISODateTimeFormat; +import static java.util.Arrays.stream; + /** * Provides thread safe means of obtaining a cached date formatter. *

@@ -41,88 +37,101 @@ import org.joda.time.format.ISODateTimeFormat; * @see java.text.DateFormat#setLenient(boolean) * * @author Derek Hulley + * @author Andrea Gazzarini */ public class CachingDateFormat extends SimpleDateFormat { - public static final String UTC = "UTC"; private static final long serialVersionUID = 3258415049197565235L; + public static final String UTC = "UTC"; - /**

 yyyy-MM-dd'T'HH:mm:ss 
*/ public static final String FORMAT_FULL_GENERIC = "yyyy-MM-dd'T'HH:mm:ss"; - - /**
 yyyy-MM-dd'T'HH:mm:ss 
*/ public static final String FORMAT_CMIS_SQL = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; - public static final String FORMAT_SOLR = "yyyy-MM-dd'T'HH:mm:ss.SSSX"; - - public static final StringAndResolution[] LENIENT_FORMATS; - - - static - { - ArrayList list = new ArrayList (); - list.add( new StringAndResolution("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Calendar.MILLISECOND)); - list.add( new StringAndResolution("yyyy-MM-dd'T'HH:mm:ss.SSS", Calendar.MILLISECOND)); - list.add( new StringAndResolution("yyyy-MM-dd'T'HH:mm:ssZ", Calendar.SECOND)); - list.add( new StringAndResolution("yyyy-MM-dd'T'HH:mm:ss", Calendar.SECOND)); - list.add( new StringAndResolution("yyyy-MM-dd'T'HH:mmZ", Calendar.MINUTE)); - list.add( new StringAndResolution("yyyy-MM-dd'T'HH:mm", Calendar.MINUTE)); - list.add( new StringAndResolution("yyyy-MM-dd'T'HHZ", Calendar.HOUR_OF_DAY)); - list.add( new StringAndResolution("yyyy-MM-dd'T'HH", Calendar.HOUR_OF_DAY)); - list.add( new StringAndResolution("yyyy-MM-dd'T'Z", Calendar.DAY_OF_MONTH)); - list.add( new StringAndResolution("yyyy-MM-dd'T'", Calendar.DAY_OF_MONTH)); - list.add( new StringAndResolution("yyyy-MM-ddZ", Calendar.DAY_OF_MONTH)); - list.add( new StringAndResolution("yyyy-MM-dd", Calendar.DAY_OF_MONTH)); - list.add( new StringAndResolution("yyyy-MMZ", Calendar.MONTH)); - list.add( new StringAndResolution("yyyy-MM", Calendar.MONTH)); - // year would duplicate :-) and eat stuff - list.add( new StringAndResolution( "yyyy-MMM-dd'T'HH:mm:ss.SSSZ", Calendar.MILLISECOND)); - list.add( new StringAndResolution( "yyyy-MMM-dd'T'HH:mm:ss.SSS", Calendar.MILLISECOND)); - list.add( new StringAndResolution( "yyyy-MMM-dd'T'HH:mm:ssZ", Calendar.SECOND)); - list.add( new StringAndResolution( "yyyy-MMM-dd'T'HH:mm:ss", Calendar.SECOND)); - list.add( new StringAndResolution( "yyyy-MMM-dd'T'HH:mmZ", Calendar.MINUTE)); - list.add( new StringAndResolution( "yyyy-MMM-dd'T'HH:mm", Calendar.MINUTE)); - list.add( new StringAndResolution( "yyyy-MMM-dd'T'HHZ", Calendar.HOUR_OF_DAY)); - list.add( new StringAndResolution( "yyyy-MMM-dd'T'HH", Calendar.HOUR_OF_DAY)); - list.add( new StringAndResolution( "yyyy-MMM-dd'T'Z",Calendar.DAY_OF_MONTH)); - list.add( new StringAndResolution( "yyyy-MMM-dd'T'",Calendar.DAY_OF_MONTH)); - list.add( new StringAndResolution( "yyyy-MMM-ddZ", Calendar.DAY_OF_MONTH)); - list.add( new StringAndResolution( "yyyy-MMM-dd", Calendar.DAY_OF_MONTH)); - list.add( new StringAndResolution( "yyyy-MMMZ", Calendar.MONTH)); - list.add( new StringAndResolution( "yyyy-MMM", Calendar.MONTH)); - list.add( new StringAndResolution("yyyyZ", Calendar.YEAR)); - list.add( new StringAndResolution("yyyy", Calendar.YEAR)); - - - - LENIENT_FORMATS = list.toArray(new StringAndResolution[]{}); - } - - /**
 yyyy-MM-dd 
*/ + public static final String UTC_WITHOUT_MSECS = "yyyy-MM-dd'T'HH:mm:ss'Z'"; public static final String FORMAT_DATE_GENERIC = "yyyy-MM-dd"; - - /**
 HH:mm:ss 
*/ public static final String FORMAT_TIME_GENERIC = "HH:mm:ss"; - private static ThreadLocal s_localDateFormat = new ThreadLocal(); - - private static ThreadLocal s_localDateOnlyFormat = new ThreadLocal(); - - private static ThreadLocal s_localTimeOnlyFormat = new ThreadLocal(); - - private static ThreadLocal s_localCmisSqlDatetime = new ThreadLocal(); - - private static ThreadLocal s_localSolrDatetime = new ThreadLocal(); - - private static ThreadLocal s_lenientParsers = new ThreadLocal(); - - transient private Map cacheDates = new WeakHashMap(89); - - private CachingDateFormat(String format) + public static final StringAndResolution[] LENIENT_FORMATS = { - super(format); + new StringAndResolution("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Calendar.MILLISECOND), + new StringAndResolution("yyyy-MM-dd'T'HH:mm:ss.SSS", Calendar.MILLISECOND), + new StringAndResolution("yyyy-MM-dd'T'HH:mm:ssZ", Calendar.SECOND), + new StringAndResolution("yyyy-MM-dd'T'HH:mm:ss", Calendar.SECOND), + new StringAndResolution("yyyy-MM-dd'T'HH:mmZ", Calendar.MINUTE), + new StringAndResolution("yyyy-MM-dd'T'HH:mm", Calendar.MINUTE), + new StringAndResolution("yyyy-MM-dd'T'HHZ", Calendar.HOUR_OF_DAY), + new StringAndResolution("yyyy-MM-dd'T'HH", Calendar.HOUR_OF_DAY), + new StringAndResolution("yyyy-MM-dd'T'Z", Calendar.DAY_OF_MONTH), + new StringAndResolution("yyyy-MM-dd'T'", Calendar.DAY_OF_MONTH), + new StringAndResolution("yyyy-MM-ddZ", Calendar.DAY_OF_MONTH), + new StringAndResolution("yyyy-MM-dd", Calendar.DAY_OF_MONTH), + new StringAndResolution("yyyy-MMZ", Calendar.MONTH), + new StringAndResolution("yyyy-MM", Calendar.MONTH), + new StringAndResolution( "yyyy-MMM-dd'T'HH:mm:ss.SSSZ", Calendar.MILLISECOND), + new StringAndResolution( "yyyy-MMM-dd'T'HH:mm:ss.SSS", Calendar.MILLISECOND), + new StringAndResolution( "yyyy-MMM-dd'T'HH:mm:ssZ", Calendar.SECOND), + new StringAndResolution( "yyyy-MMM-dd'T'HH:mm:ss", Calendar.SECOND), + new StringAndResolution( "yyyy-MMM-dd'T'HH:mmZ", Calendar.MINUTE), + new StringAndResolution( "yyyy-MMM-dd'T'HH:mm", Calendar.MINUTE), + new StringAndResolution( "yyyy-MMM-dd'T'HHZ", Calendar.HOUR_OF_DAY), + new StringAndResolution( "yyyy-MMM-dd'T'HH", Calendar.HOUR_OF_DAY), + new StringAndResolution( "yyyy-MMM-dd'T'Z",Calendar.DAY_OF_MONTH), + new StringAndResolution( "yyyy-MMM-dd'T'",Calendar.DAY_OF_MONTH), + new StringAndResolution( "yyyy-MMM-ddZ", Calendar.DAY_OF_MONTH), + new StringAndResolution( "yyyy-MMM-dd", Calendar.DAY_OF_MONTH), + new StringAndResolution( "yyyy-MMMZ", Calendar.MONTH), + new StringAndResolution( "yyyy-MMM", Calendar.MONTH), + new StringAndResolution("yyyyZ", Calendar.YEAR), + new StringAndResolution("yyyy", Calendar.YEAR) + }; + + private static ThreadLocal S_LOCAL_DATE_FORMAT = ThreadLocal.withInitial(() -> newDateFormat(FORMAT_FULL_GENERIC)); + + private static ThreadLocal S_LOCAL_DATEONLY_FORMAT = ThreadLocal.withInitial(() -> newDateFormat(FORMAT_DATE_GENERIC)); + + private static ThreadLocal S_LOCAL_TIMEONLY_FORMAT = ThreadLocal.withInitial(() -> newDateFormat(FORMAT_TIME_GENERIC)); + + private static ThreadLocal S_LOCAL_CMIS_SQL_DATETIME = ThreadLocal.withInitial(() -> newDateFormat(FORMAT_CMIS_SQL)); + + private static ThreadLocal S_LOCAL_SOLR_DATETIME = ThreadLocal.withInitial(()-> + { + CachingDateFormat formatter = newDateFormat(FORMAT_SOLR); + /* + SEARCH-1263 + Apache Solr only supports the ISO 8601 date format: + UTC and western locale are mandatory (only Arabic numerals (0123456789) are supported) + */ + formatter.setTimeZone(TimeZone.getTimeZone(UTC)); + formatter.setNumberFormat(NumberFormat.getNumberInstance(Locale.ENGLISH)); + return formatter; + }); + + private static ThreadLocal S_UTC_DATETIME_WITHOUT_MSECS = ThreadLocal.withInitial(() -> + { + CachingDateFormat formatter = newDateFormat(UTC_WITHOUT_MSECS); + formatter.setTimeZone(TimeZone.getTimeZone(UTC)); + formatter.setNumberFormat(NumberFormat.getNumberInstance(Locale.ENGLISH)); + + return formatter; + }); + + private static ThreadLocal S_LENIENT_PARSERS = + ThreadLocal.withInitial(() -> + stream(LENIENT_FORMATS) + .map(format -> { + CachingDateFormat formatter = new CachingDateFormat(format.string); + formatter.setLenient(false); + return new SimpleDateFormatAndResolution(formatter, format.resolution); }) + .toArray(SimpleDateFormatAndResolution[]::new)); + + private Map cacheDates = new WeakHashMap<>(89); + + private CachingDateFormat(String pattern) + { + super(pattern); } + @Override public String toString() { return this.toPattern(); @@ -194,64 +203,47 @@ public class CachingDateFormat extends SimpleDateFormat } /** - * @return Returns a thread-safe formatter for the generic date/time format - * + * Returns a thread-safe formatter for the generic date/time format. + * * @see #FORMAT_FULL_GENERIC + * @return a thread-safe formatter for the generic date/time format. */ public static SimpleDateFormat getDateFormat() { - if (s_localDateFormat.get() != null) - { - return s_localDateFormat.get(); - } - - CachingDateFormat formatter = new CachingDateFormat(FORMAT_FULL_GENERIC); - // it must be strict - formatter.setLenient(false); - // put this into the threadlocal object - s_localDateFormat.set(formatter); - // done - return s_localDateFormat.get(); + return S_LOCAL_DATE_FORMAT.get(); } /** - * @return Returns a thread-safe formatter for the cmis sql datetime format + * Returns a thread-safe formatter for the cmis sql datetime format. + * + * @see #FORMAT_CMIS_SQL + * @return a thread-safe formatter for the cmis sql datetime format. */ public static SimpleDateFormat getCmisSqlDatetimeFormat() { - if (s_localCmisSqlDatetime.get() != null) - { - return s_localCmisSqlDatetime.get(); - } - - CachingDateFormat formatter = new CachingDateFormat(FORMAT_CMIS_SQL); - // it must be strict - formatter.setLenient(false); - // put this into the threadlocal object - s_localCmisSqlDatetime.set(formatter); - // done - return s_localCmisSqlDatetime.get(); + return S_LOCAL_CMIS_SQL_DATETIME.get(); } /** - * @return Returns a thread-safe formatter for the Solr ISO 8601 datetime format + * Returns a thread-safe formatter for the Solr ISO 8601 datetime format (without the msecs part). + * + * @see #UTC_WITHOUT_MSECS + * @return Returns a thread-safe formatter for the Solr ISO 8601 datetime format (without the msecs part). + */ + public static SimpleDateFormat getSolrDatetimeFormatWithoutMsecs() + { + return S_UTC_DATETIME_WITHOUT_MSECS.get(); + } + + /** + * Returns a thread-safe formatter for the Solr ISO 8601 datetime format. + * + * @see #FORMAT_SOLR + * @return a thread-safe formatter for the Solr ISO 8601 datetime format */ public static SimpleDateFormat getSolrDatetimeFormat() { - if (s_localSolrDatetime.get() != null) - { - return s_localSolrDatetime.get(); - } - - CachingDateFormat formatter = new CachingDateFormat(FORMAT_SOLR); - formatter.setLenient(false); - /* Apache Solr only supports the ISO 8601 date format: - * UTC and western locale are mandatory (only Arabic numerals (0123456789) are supported) */ - formatter.setTimeZone(TimeZone.getTimeZone(UTC)); - formatter.setNumberFormat(NumberFormat.getNumberInstance(Locale.ENGLISH)); - - s_localSolrDatetime.set(formatter); - return s_localSolrDatetime.get(); + return S_LOCAL_SOLR_DATETIME.get(); } /** @@ -261,39 +253,18 @@ public class CachingDateFormat extends SimpleDateFormat */ public static SimpleDateFormat getDateOnlyFormat() { - if (s_localDateOnlyFormat.get() != null) - { - return s_localDateOnlyFormat.get(); - } - - CachingDateFormat formatter = new CachingDateFormat(FORMAT_DATE_GENERIC); - // it must be strict - formatter.setLenient(false); - // put this into the threadlocal object - s_localDateOnlyFormat.set(formatter); - // done - return s_localDateOnlyFormat.get(); + return S_LOCAL_DATEONLY_FORMAT.get(); } /** - * @return Returns a thread-safe formatter for the generic time format - * + * Returns a thread-safe formatter for the generic time format. + * * @see #FORMAT_TIME_GENERIC + * @return a thread-safe formatter for the generic time format. */ public static SimpleDateFormat getTimeOnlyFormat() { - if (s_localTimeOnlyFormat.get() != null) - { - return s_localTimeOnlyFormat.get(); - } - - CachingDateFormat formatter = new CachingDateFormat(FORMAT_TIME_GENERIC); - // it must be strict - formatter.setLenient(false); - // put this into the threadlocal object - s_localTimeOnlyFormat.set(formatter); - // done - return s_localTimeOnlyFormat.get(); + return S_LOCAL_TIMEONLY_FORMAT.get(); } /** @@ -311,8 +282,7 @@ public class CachingDateFormat extends SimpleDateFormat if ((date != null) && (pos.getIndex() == text.length())) { cacheDates.put(text, date); - Date clonedDate = (Date) date.clone(); - return clonedDate; + return (Date) date.clone(); } else { @@ -322,8 +292,7 @@ public class CachingDateFormat extends SimpleDateFormat else { pos.setIndex(text.length()); - Date clonedDate = (Date) cached.clone(); - return clonedDate; + return (Date) cached.clone(); } } @@ -337,7 +306,7 @@ public class CachingDateFormat extends SimpleDateFormat } catch(IllegalArgumentException e) { - + // Nothing to be done here } SimpleDateFormatAndResolution[] formatters = getLenientFormatters(); @@ -362,25 +331,7 @@ public class CachingDateFormat extends SimpleDateFormat public static SimpleDateFormatAndResolution[] getLenientFormatters() { - if (s_lenientParsers.get() != null) - { - return s_lenientParsers.get(); - } - - int i = 0; - SimpleDateFormatAndResolution[] formatters = new SimpleDateFormatAndResolution[LENIENT_FORMATS.length]; - for(StringAndResolution format : LENIENT_FORMATS) - { - CachingDateFormat formatter = new CachingDateFormat(format.string); - // it must be strict - formatter.setLenient(false); - formatters[i++] = new SimpleDateFormatAndResolution(formatter, format.resolution); - } - - // put this into the threadlocal object - s_lenientParsers.set(formatters); - // done - return s_lenientParsers.get(); + return S_LENIENT_PARSERS.get(); } public static class StringAndResolution @@ -439,4 +390,17 @@ public class CachingDateFormat extends SimpleDateFormat } } + + /** + * Creates a new non-lenient {@link CachingDateFormat} instance. + * + * @param pattern the date / datetime pattern. + * @return new non-lenient {@link CachingDateFormat} instance. + */ + private static CachingDateFormat newDateFormat(String pattern) + { + CachingDateFormat formatter = new CachingDateFormat(pattern); + formatter.setLenient(false); + return formatter; + } } diff --git a/src/test/java/org/alfresco/util/CachingDateFormatTest.java b/src/test/java/org/alfresco/util/CachingDateFormatTest.java index ce203935e5..ecf964d8d0 100644 --- a/src/test/java/org/alfresco/util/CachingDateFormatTest.java +++ b/src/test/java/org/alfresco/util/CachingDateFormatTest.java @@ -27,9 +27,11 @@ import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Date; import java.util.Locale; +import java.util.TimeZone; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; public class CachingDateFormatTest { @@ -39,8 +41,7 @@ public class CachingDateFormatTest @Test public void solrDatetimeFormat_DateNotUTC_shouldReturnISO8601DateString() { - Instant shanghaiInstant = REFERENCE_DATE_TIME.atZone(ZoneId.of("Asia/Shanghai")).toInstant(); - Date shanghaiDate = Date.from(shanghaiInstant); + Date shanghaiDate = testDate("Asia/Shanghai"); SimpleDateFormat solrDatetimeFormat = CachingDateFormat.getSolrDatetimeFormat(); String formattedDate = solrDatetimeFormat.format(shanghaiDate); @@ -54,8 +55,8 @@ public class CachingDateFormatTest for(Locale currentLocale:Locale.getAvailableLocales()) { Locale.setDefault(currentLocale); - Instant utcInstant = REFERENCE_DATE_TIME.atZone(ZoneId.of("UTC")).toInstant(); - Date utcDate = Date.from(utcInstant); + + Date utcDate = testDate("UTC"); SimpleDateFormat solrDatetimeFormat = CachingDateFormat.getSolrDatetimeFormat(); String formattedDate = solrDatetimeFormat.format(utcDate); @@ -64,9 +65,64 @@ public class CachingDateFormatTest } } + @Test + public void onlyDateFormatReturnsOnlyTheDatePart() + { + Date utcDate = testDate("UTC"); + + SimpleDateFormat formatter = CachingDateFormat.getDateOnlyFormat(); + formatter.setTimeZone(TimeZone.getTimeZone("UTC")); + + assertEquals("2018-04-01", formatter.format(utcDate)); + } + + @Test + public void onlyTimeFormatShouldReturnOnlyTheTimePart() + { + Date utcDate = testDate("UTC"); + + SimpleDateFormat formatter = CachingDateFormat.getTimeOnlyFormat(); + formatter.setTimeZone(TimeZone.getTimeZone("UTC")); + + assertEquals("10:00:00", formatter.format(utcDate)); + } + + @Test + public void dateTimeFormatShouldReturnDateAndTime() + { + Date utcDate = testDate("UTC"); + + SimpleDateFormat formatter = CachingDateFormat.getDateFormat(); + formatter.setTimeZone(TimeZone.getTimeZone("UTC")); + + assertEquals("2018-04-01T10:00:00", formatter.format(utcDate)); + } + + @Test + public void utcWithoutMsecsDatetimeFormat_shouldReturnStringsWithoutMsecs() + { + Date utcDate = testDate("UTC"); + + SimpleDateFormat formatter = CachingDateFormat.getSolrDatetimeFormatWithoutMsecs(); + + assertEquals("2018-04-01T10:00:00Z", formatter.format(utcDate)); + } + @After - public void tearDown() throws Exception + public void tearDown() { Locale.setDefault(defaultLocale); } + + /** + * Creates a test date using the given timezone id. + * + * @param zoneId the timezone id. + * @return a test date using the given timezone id. + */ + private Date testDate(String zoneId) + { + Instant utcInstant = REFERENCE_DATE_TIME.atZone(ZoneId.of(zoneId)).toInstant(); + return Date.from(utcInstant); + } }