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   * FormatCache is a cache and factory for {@link Format}s.
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) {
64                  return false;
65              }
66              if (getClass() != obj.getClass()) {
67                  return false;
68              }
69              final ArrayKey other = (ArrayKey) obj;
70              return Arrays.deepEquals(keys, other.keys);
71          }
72  
73          @Override
74          public int hashCode() {
75              return hashCode;
76          }
77  
78      }
79  
80      /**
81       * No date or no time.  Used in same parameters as DateFormat.SHORT or DateFormat.LONG
82       */
83      static final int NONE = -1;
84  
85      private static final ConcurrentMap<ArrayKey, String> dateTimeInstanceCache = new ConcurrentHashMap<>(7);
86  
87      /**
88       * Clears the cache.
89       */
90      static void clear() {
91          dateTimeInstanceCache.clear();
92      }
93  
94      /**
95       * Gets a date/time format for the specified styles and locale.
96       *
97       * @param dateStyle  date style: FULL, LONG, MEDIUM, or SHORT, null indicates no date in format
98       * @param timeStyle  time style: FULL, LONG, MEDIUM, or SHORT, null indicates no time in format
99       * @param locale  The non-null locale of the desired format
100      * @return a localized standard date/time format
101      * @throws IllegalArgumentException if the Locale has no date/time pattern defined
102      */
103     // package protected, for access from test code; do not make public or protected
104     static String getPatternForStyle(final Integer dateStyle, final Integer timeStyle, final Locale locale) {
105         final Locale safeLocale = LocaleUtils.toLocale(locale);
106         final ArrayKey key = new ArrayKey(dateStyle, timeStyle, safeLocale);
107         return dateTimeInstanceCache.computeIfAbsent(key, k -> {
108             try {
109                 final DateFormat formatter;
110                 if (dateStyle == null) {
111                     formatter = DateFormat.getTimeInstance(timeStyle.intValue(), safeLocale);
112                 } else if (timeStyle == null) {
113                     formatter = DateFormat.getDateInstance(dateStyle.intValue(), safeLocale);
114                 } else {
115                     formatter = DateFormat.getDateTimeInstance(dateStyle.intValue(), timeStyle.intValue(), safeLocale);
116                 }
117                 return ((SimpleDateFormat) formatter).toPattern();
118             } catch (final ClassCastException ex) {
119                 throw new IllegalArgumentException("No date time pattern for locale: " + safeLocale);
120             }
121         });
122     }
123 
124     private final ConcurrentMap<ArrayKey, F> instanceCache = new ConcurrentHashMap<>(7);
125 
126     /**
127      * Clears the cache.
128      */
129     void clearInstance() {
130         instanceCache.clear();
131     }
132 
133     /**
134      * Create a format instance using the specified pattern, time zone
135      * and locale.
136      *
137      * @param pattern  {@link java.text.SimpleDateFormat} compatible pattern, this will not be null.
138      * @param timeZone  time zone, this will not be null.
139      * @param locale  locale, this will not be null.
140      * @return a pattern based date/time formatter
141      * @throws IllegalArgumentException if pattern is invalid
142      *  or {@code null}
143      */
144     protected abstract F createInstance(String pattern, TimeZone timeZone, Locale locale);
145 
146     /**
147      * Gets a date formatter instance using the specified style,
148      * time zone and locale.
149      *
150      * @param dateStyle  date style: FULL, LONG, MEDIUM, or SHORT
151      * @param timeZone  optional time zone, overrides time zone of
152      *  formatted date, null means use default Locale
153      * @param locale  optional locale, overrides system locale
154      * @return a localized standard date/time formatter
155      * @throws IllegalArgumentException if the Locale has no date/time
156      *  pattern defined
157      */
158     // package protected, for access from FastDateFormat; do not make public or protected
159     F getDateInstance(final int dateStyle, final TimeZone timeZone, final Locale locale) {
160         return getDateTimeInstance(Integer.valueOf(dateStyle), null, timeZone, locale);
161     }
162 
163     /**
164      * Gets a date/time formatter instance using the specified style,
165      * time zone and locale.
166      *
167      * @param dateStyle  date style: FULL, LONG, MEDIUM, or SHORT
168      * @param timeStyle  time style: FULL, LONG, MEDIUM, or SHORT
169      * @param timeZone  optional time zone, overrides time zone of
170      *  formatted date, null means use default Locale
171      * @param locale  optional locale, overrides system locale
172      * @return a localized standard date/time formatter
173      * @throws IllegalArgumentException if the Locale has no date/time
174      *  pattern defined
175      */
176     // package protected, for access from FastDateFormat; do not make public or protected
177     F getDateTimeInstance(final int dateStyle, final int timeStyle, final TimeZone timeZone, final Locale locale) {
178         return getDateTimeInstance(Integer.valueOf(dateStyle), Integer.valueOf(timeStyle), timeZone, locale);
179     }
180 
181     /**
182      * Gets a date/time formatter instance using the specified style,
183      * time zone and locale.
184      *
185      * @param dateStyle  date style: FULL, LONG, MEDIUM, or SHORT, null indicates no date in format
186      * @param timeStyle  time style: FULL, LONG, MEDIUM, or SHORT, null indicates no time in format
187      * @param timeZone  optional time zone, overrides time zone of
188      *  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 if the Locale has no date/time
192      *  pattern defined
193      */
194     // This must remain private, see LANG-884
195     private F getDateTimeInstance(final Integer dateStyle, final Integer timeStyle, final TimeZone timeZone, Locale locale) {
196         locale = LocaleUtils.toLocale(locale);
197         final String pattern = getPatternForStyle(dateStyle, timeStyle, locale);
198         return getInstance(pattern, timeZone, locale);
199     }
200 
201     /**
202      * Gets a formatter instance using the default pattern in the
203      * default time zone and locale.
204      *
205      * @return a date/time formatter
206      */
207     public F getInstance() {
208         return getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, TimeZone.getDefault(), Locale.getDefault());
209     }
210 
211     /**
212      * Gets a formatter instance using the specified pattern, time zone
213      * and locale.
214      *
215      * @param pattern  {@link java.text.SimpleDateFormat} compatible
216      *  pattern, non-null
217      * @param timeZone  the time zone, null means use the default TimeZone
218      * @param locale  the locale, null means use the default Locale
219      * @return a pattern based date/time formatter
220      * @throws NullPointerException if pattern is {@code null}
221      * @throws IllegalArgumentException if pattern is invalid
222      */
223     public F getInstance(final String pattern, final TimeZone timeZone, final Locale locale) {
224         Objects.requireNonNull(pattern, "pattern");
225         final TimeZone actualTimeZone = TimeZones.toTimeZone(timeZone);
226         final Locale actualLocale = LocaleUtils.toLocale(locale);
227         final ArrayKey key = new ArrayKey(pattern, actualTimeZone, actualLocale);
228         return instanceCache.computeIfAbsent(key, k -> createInstance(pattern, actualTimeZone, actualLocale));
229     }
230 
231     /**
232      * Gets a time formatter instance using the specified style,
233      * time zone and locale.
234      *
235      * @param timeStyle  time style: FULL, LONG, MEDIUM, or SHORT
236      * @param timeZone  optional time zone, overrides time zone of
237      *  formatted date, null means use default Locale
238      * @param locale  optional locale, overrides system locale
239      * @return a localized standard date/time formatter
240      * @throws IllegalArgumentException if the Locale has no date/time
241      *  pattern defined
242      */
243     // package protected, for access from FastDateFormat; do not make public or protected
244     F getTimeInstance(final int timeStyle, final TimeZone timeZone, final Locale locale) {
245         return getDateTimeInstance(null, Integer.valueOf(timeStyle), timeZone, locale);
246     }
247 
248 }