View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.lang3.time;
18  
19  import java.text.DateFormat;
20  import java.text.Format;
21  import java.text.SimpleDateFormat;
22  import java.util.Arrays;
23  import java.util.Locale;
24  import java.util.Objects;
25  import java.util.TimeZone;
26  import java.util.concurrent.ConcurrentHashMap;
27  import java.util.concurrent.ConcurrentMap;
28  
29  import org.apache.commons.lang3.LocaleUtils;
30  
31  /**
32   * Caches for {@link Format} instances.
33   *
34   * @param <F> The Format type.
35   * @since 3.0
36   */
37  // TODO: Before making public move from getDateTimeInstance(Integer, ...) to int; or some other approach.
38  abstract class AbstractFormatCache<F extends Format> {
39  
40      /**
41       * Helper class to hold multipart Map keys as arrays.
42       */
43      private static final class ArrayKey {
44  
45          private final Object[] keys;
46          private final int hashCode;
47  
48          /**
49           * Constructs an instance of {@link MultipartKey} to hold the specified objects.
50           *
51           * @param keys The set of objects that make up the key.  Each key may be null.
52           */
53          ArrayKey(final Object... keys) {
54              this.keys = keys;
55              this.hashCode = Objects.hash(keys);
56          }
57  
58          @Override
59          public boolean equals(final Object obj) {
60              if (this == obj) {
61                  return true;
62              }
63              if (obj == null || getClass() != obj.getClass()) {
64                  return false;
65              }
66              final ArrayKey other = (ArrayKey) obj;
67              return Arrays.deepEquals(keys, other.keys);
68          }
69  
70          @Override
71          public int hashCode() {
72              return hashCode;
73          }
74  
75      }
76  
77      /**
78       * No date or no time. Used in same parameters as DateFormat.SHORT or DateFormat.LONG.
79       */
80      static final int NONE = -1;
81  
82      /**
83       * Maximum number of entries a static cache in this package may hold before it is flushed.
84       * These caches live for the lifetime of the JVM and are keyed by caller-supplied values
85       * (pattern, time zone, locale), so without a bound, unbounded-cardinality inputs would pin
86       * memory forever. The bound is approximate under concurrency; flushing only costs
87       * re-creation of evicted entries.
88       */
89      static final int MAX_CACHE_SIZE = 1024;
90  
91      private static final ConcurrentMap<ArrayKey, String> dateTimeInstanceCache = new ConcurrentHashMap<>(7);
92  
93      /**
94       * Clears the cache.
95       */
96      static void clear() {
97          dateTimeInstanceCache.clear();
98      }
99  
100     /**
101      * Gets a date/time format for the specified styles and locale.
102      *
103      * @param dateStyle  date style: FULL, LONG, MEDIUM, or SHORT, null indicates no date in format.
104      * @param timeStyle  time style: FULL, LONG, MEDIUM, or SHORT, null indicates no time in format.
105      * @param locale  The non-null locale of the desired format.
106      * @return A localized standard date/time format.
107      * @throws IllegalArgumentException Thrown if the Locale has no date/time pattern defined.
108      */
109     // package protected, for access from test code; do not make public or protected
110     static String getPatternForStyle(final Integer dateStyle, final Integer timeStyle, final Locale locale) {
111         final Locale safeLocale = LocaleUtils.toLocale(locale);
112         final ArrayKey key = new ArrayKey(dateStyle, timeStyle, safeLocale);
113         return dateTimeInstanceCache.computeIfAbsent(key, k -> {
114             try {
115                 final DateFormat formatter;
116                 if (dateStyle == null) {
117                     formatter = DateFormat.getTimeInstance(timeStyle.intValue(), safeLocale);
118                 } else if (timeStyle == null) {
119                     formatter = DateFormat.getDateInstance(dateStyle.intValue(), safeLocale);
120                 } else {
121                     formatter = DateFormat.getDateTimeInstance(dateStyle.intValue(), timeStyle.intValue(), safeLocale);
122                 }
123                 return ((SimpleDateFormat) formatter).toPattern();
124             } catch (final ClassCastException ex) {
125                 throw new IllegalArgumentException("No date time pattern for locale: " + safeLocale);
126             }
127         });
128     }
129 
130     private final ConcurrentMap<ArrayKey, F> instanceCache = new ConcurrentHashMap<>(7);
131 
132     /**
133      * Clears the cache.
134      */
135     void clearInstance() {
136         instanceCache.clear();
137     }
138 
139     /**
140      * Create a format instance using the specified pattern, time zone
141      * and locale.
142      *
143      * @param pattern  {@link java.text.SimpleDateFormat} compatible pattern, this will not be null.
144      * @param timeZone  time zone, this will not be null.
145      * @param locale  locale, this will not be null.
146      * @return A pattern based date/time formatter.
147      * @throws IllegalArgumentException Thrown if pattern is invalid or {@code null}.
148      */
149     protected abstract F createInstance(String pattern, TimeZone timeZone, Locale locale);
150 
151     /**
152      * Gets a date formatter instance using the specified style,
153      * time zone and locale.
154      *
155      * @param dateStyle  date style: FULL, LONG, MEDIUM, or SHORT.
156      * @param timeZone  optional time zone, overrides time zone of formatted date, null means use default Locale.
157      * @param locale  optional locale, overrides system locale.
158      * @return A localized standard date/time formatter.
159      * @throws IllegalArgumentException Thrown if the Locale has no date/time pattern defined.
160      */
161     // package protected, for access from FastDateFormat; do not make public or protected
162     F getDateInstance(final int dateStyle, final TimeZone timeZone, final Locale locale) {
163         return getDateTimeInstance(Integer.valueOf(dateStyle), null, timeZone, locale);
164     }
165 
166     /**
167      * Gets a date/time formatter instance using the specified style,
168      * time zone and locale.
169      *
170      * @param dateStyle  date style: FULL, LONG, MEDIUM, or SHORT.
171      * @param timeStyle  time style: FULL, LONG, MEDIUM, or SHORT.
172      * @param timeZone  optional time zone, overrides time zone of formatted date, null means use default Locale.
173      * @param locale  optional locale, overrides system locale.
174      * @return A localized standard date/time formatter.
175      * @throws IllegalArgumentException Thrown if the Locale has no date/time pattern defined.
176      */
177     // package protected, for access from FastDateFormat; do not make public or protected
178     F getDateTimeInstance(final int dateStyle, final int timeStyle, final TimeZone timeZone, final Locale locale) {
179         return getDateTimeInstance(Integer.valueOf(dateStyle), Integer.valueOf(timeStyle), timeZone, locale);
180     }
181 
182     /**
183      * Gets a date/time formatter instance using the specified style,
184      * time zone and locale.
185      *
186      * @param dateStyle  date style: FULL, LONG, MEDIUM, or SHORT, null indicates no date in format.
187      * @param timeStyle  time style: FULL, LONG, MEDIUM, or SHORT, null indicates no time in format.
188      * @param timeZone  optional time zone, overrides time zone of formatted date, null means use default Locale.
189      * @param locale  optional locale, overrides system locale.
190      * @return A localized standard date/time formatter.
191      * @throws IllegalArgumentException Thrown if the Locale has no date/time pattern defined.
192      */
193     // This must remain private, see LANG-884
194     private F getDateTimeInstance(final Integer dateStyle, final Integer timeStyle, final TimeZone timeZone, Locale locale) {
195         locale = LocaleUtils.toLocale(locale);
196         return getInstance(getPatternForStyle(dateStyle, timeStyle, locale), timeZone, locale);
197     }
198 
199     /**
200      * Gets a formatter instance using the default pattern in the
201      * default time zone and locale.
202      *
203      * @return A date/time formatter.
204      */
205     public F getInstance() {
206         return getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, TimeZone.getDefault(), Locale.getDefault());
207     }
208 
209     /**
210      * Gets a formatter instance using the specified pattern, time zone
211      * and locale.
212      *
213      * @param pattern  {@link java.text.SimpleDateFormat} compatible pattern, non-null.
214      * @param timeZone  The time zone, null means use the default TimeZone.
215      * @param locale  The locale, null means use the default Locale.
216      * @return A pattern based date/time formatter.
217      * @throws NullPointerException Thrown if pattern is {@code null}.
218      * @throws IllegalArgumentException Thrown if pattern is invalid.
219      */
220     public F getInstance(final String pattern, final TimeZone timeZone, final Locale locale) {
221         Objects.requireNonNull(pattern, "pattern");
222         // Snapshot the mutable zone so the cache key and formatter retain the same rules.
223         final TimeZone actualTimeZone = (TimeZone) TimeZones.toTimeZone(timeZone).clone();
224         final Locale actualLocale = LocaleUtils.toLocale(locale);
225         final ArrayKey key = new ArrayKey(pattern, actualTimeZone, actualLocale);
226         // Bound the cache: it is static and process-lifetime, so unbounded-cardinality keys
227         // (for example patterns derived from caller input) would otherwise pin memory forever.
228         if (instanceCache.size() >= MAX_CACHE_SIZE && !instanceCache.containsKey(key)) {
229             instanceCache.clear();
230         }
231         return instanceCache.computeIfAbsent(key, k -> createInstance(pattern, actualTimeZone, actualLocale));
232     }
233 
234     /**
235      * Gets a time formatter instance using the specified style,
236      * time zone and locale.
237      *
238      * @param timeStyle  time style: FULL, LONG, MEDIUM, or SHORT.
239      * @param timeZone  optional time zone, overrides time zone of formatted date, null means use default Locale.
240      * @param locale  optional locale, overrides system locale.
241      * @return A localized standard date/time formatter.
242      * @throws IllegalArgumentException Thrown if the Locale has no date/time pattern defined.
243      */
244     // package protected, for access from FastDateFormat; do not make public or protected
245     F getTimeInstance(final int timeStyle, final TimeZone timeZone, final Locale locale) {
246         return getDateTimeInstance(null, Integer.valueOf(timeStyle), timeZone, locale);
247     }
248 
249 }