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    *      http://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.TimeZone;
25  import java.util.concurrent.ConcurrentHashMap;
26  import java.util.concurrent.ConcurrentMap;
27  
28  import org.apache.commons.lang3.LocaleUtils;
29  import org.apache.commons.lang3.Validate;
30  
31  /**
32   * FormatCache is a cache and factory for {@link Format}s.
33   *
34   * @since 3.0
35   */
36  // TODO: Before making public move from getDateTimeInstance(Integer, ...) to int; or some other approach.
37  abstract class FormatCache<F extends Format> {
38  
39      /**
40       * No date or no time.  Used in same parameters as DateFormat.SHORT or DateFormat.LONG
41       */
42      static final int NONE = -1;
43  
44      private final ConcurrentMap<ArrayKey, F> cInstanceCache = new ConcurrentHashMap<>(7);
45  
46      private static final ConcurrentMap<ArrayKey, String> cDateTimeInstanceCache = new ConcurrentHashMap<>(7);
47  
48      /**
49       * Gets a formatter instance using the default pattern in the
50       * default time zone and locale.
51       *
52       * @return a date/time formatter
53       */
54      public F getInstance() {
55          return getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, TimeZone.getDefault(), Locale.getDefault());
56      }
57  
58      /**
59       * Gets a formatter instance using the specified pattern, time zone
60       * and locale.
61       *
62       * @param pattern  {@link java.text.SimpleDateFormat} compatible
63       *  pattern, non-null
64       * @param timeZone  the time zone, null means use the default TimeZone
65       * @param locale  the locale, null means use the default Locale
66       * @return a pattern based date/time formatter
67       * @throws NullPointerException if pattern is {@code null}
68       * @throws IllegalArgumentException if pattern is invalid
69       */
70      public F getInstance(final String pattern, TimeZone timeZone, Locale locale) {
71          Validate.notNull(pattern, "pattern");
72          if (timeZone == null) {
73              timeZone = TimeZone.getDefault();
74          }
75          locale = LocaleUtils.toLocale(locale);
76          final ArrayKey key = new ArrayKey(pattern, timeZone, locale);
77          F format = cInstanceCache.get(key);
78          if (format == null) {
79              format = createInstance(pattern, timeZone, locale);
80              final F previousValue = cInstanceCache.putIfAbsent(key, format);
81              if (previousValue != null) {
82                  // another thread snuck in and did the same work
83                  // we should return the instance that is in ConcurrentMap
84                  format = previousValue;
85              }
86          }
87          return format;
88      }
89  
90      /**
91       * Create a format instance using the specified pattern, time zone
92       * and locale.
93       *
94       * @param pattern  {@link java.text.SimpleDateFormat} compatible pattern, this will not be null.
95       * @param timeZone  time zone, this will not be null.
96       * @param locale  locale, this will not be null.
97       * @return a pattern based date/time formatter
98       * @throws IllegalArgumentException if pattern is invalid
99       *  or {@code null}
100      */
101     protected abstract F createInstance(String pattern, TimeZone timeZone, Locale locale);
102 
103     /**
104      * Gets a date/time formatter instance using the specified style,
105      * time zone and locale.
106      *
107      * @param dateStyle  date style: FULL, LONG, MEDIUM, or SHORT, null indicates no date in format
108      * @param timeStyle  time style: FULL, LONG, MEDIUM, or SHORT, null indicates no time in format
109      * @param timeZone  optional time zone, overrides time zone of
110      *  formatted date, null means use default Locale
111      * @param locale  optional locale, overrides system locale
112      * @return a localized standard date/time formatter
113      * @throws IllegalArgumentException if the Locale has no date/time
114      *  pattern defined
115      */
116     // This must remain private, see LANG-884
117     private F getDateTimeInstance(final Integer dateStyle, final Integer timeStyle, final TimeZone timeZone, Locale locale) {
118         locale = LocaleUtils.toLocale(locale);
119         final String pattern = getPatternForStyle(dateStyle, timeStyle, locale);
120         return getInstance(pattern, timeZone, locale);
121     }
122 
123     /**
124      * Gets a date/time formatter instance using the specified style,
125      * time zone and locale.
126      *
127      * @param dateStyle  date style: FULL, LONG, MEDIUM, or SHORT
128      * @param timeStyle  time style: FULL, LONG, MEDIUM, or SHORT
129      * @param timeZone  optional time zone, overrides time zone of
130      *  formatted date, null means use default Locale
131      * @param locale  optional locale, overrides system locale
132      * @return a localized standard date/time formatter
133      * @throws IllegalArgumentException if the Locale has no date/time
134      *  pattern defined
135      */
136     // package protected, for access from FastDateFormat; do not make public or protected
137     F getDateTimeInstance(final int dateStyle, final int timeStyle, final TimeZone timeZone, final Locale locale) {
138         return getDateTimeInstance(Integer.valueOf(dateStyle), Integer.valueOf(timeStyle), timeZone, locale);
139     }
140 
141     /**
142      * Gets a date formatter instance using the specified style,
143      * time zone and locale.
144      *
145      * @param dateStyle  date style: FULL, LONG, MEDIUM, or SHORT
146      * @param timeZone  optional time zone, overrides time zone of
147      *  formatted date, null means use default Locale
148      * @param locale  optional locale, overrides system locale
149      * @return a localized standard date/time formatter
150      * @throws IllegalArgumentException if the Locale has no date/time
151      *  pattern defined
152      */
153     // package protected, for access from FastDateFormat; do not make public or protected
154     F getDateInstance(final int dateStyle, final TimeZone timeZone, final Locale locale) {
155         return getDateTimeInstance(Integer.valueOf(dateStyle), null, timeZone, locale);
156     }
157 
158     /**
159      * Gets a time formatter instance using the specified style,
160      * time zone and locale.
161      *
162      * @param timeStyle  time style: FULL, LONG, MEDIUM, or SHORT
163      * @param timeZone  optional time zone, overrides time zone of
164      *  formatted date, null means use default Locale
165      * @param locale  optional locale, overrides system locale
166      * @return a localized standard date/time formatter
167      * @throws IllegalArgumentException if the Locale has no date/time
168      *  pattern defined
169      */
170     // package protected, for access from FastDateFormat; do not make public or protected
171     F getTimeInstance(final int timeStyle, final TimeZone timeZone, final Locale locale) {
172         return getDateTimeInstance(null, Integer.valueOf(timeStyle), timeZone, locale);
173     }
174 
175     /**
176      * Gets a date/time format for the specified styles and locale.
177      *
178      * @param dateStyle  date style: FULL, LONG, MEDIUM, or SHORT, null indicates no date in format
179      * @param timeStyle  time style: FULL, LONG, MEDIUM, or SHORT, null indicates no time in format
180      * @param locale  The non-null locale of the desired format
181      * @return a localized standard date/time format
182      * @throws IllegalArgumentException if the Locale has no date/time pattern defined
183      */
184     // package protected, for access from test code; do not make public or protected
185     static String getPatternForStyle(final Integer dateStyle, final Integer timeStyle, final Locale locale) {
186         final Locale safeLocale = LocaleUtils.toLocale(locale);
187         final ArrayKey key = new ArrayKey(dateStyle, timeStyle, safeLocale);
188 
189         String pattern = cDateTimeInstanceCache.get(key);
190         if (pattern == null) {
191             try {
192                 final DateFormat formatter;
193                 if (dateStyle == null) {
194                     formatter = DateFormat.getTimeInstance(timeStyle.intValue(), safeLocale);
195                 } else if (timeStyle == null) {
196                     formatter = DateFormat.getDateInstance(dateStyle.intValue(), safeLocale);
197                 } else {
198                     formatter = DateFormat.getDateTimeInstance(dateStyle.intValue(), timeStyle.intValue(), safeLocale);
199                 }
200                 pattern = ((SimpleDateFormat) formatter).toPattern();
201                 final String previous = cDateTimeInstanceCache.putIfAbsent(key, pattern);
202                 if (previous != null) {
203                     // even though it doesn't matter if another thread put the pattern
204                     // it's still good practice to return the String instance that is
205                     // actually in the ConcurrentMap
206                     pattern = previous;
207                 }
208             } catch (final ClassCastException ex) {
209                 throw new IllegalArgumentException("No date time pattern for locale: " + safeLocale);
210             }
211         }
212         return pattern;
213     }
214 
215     /**
216      * Helper class to hold multi-part Map keys as arrays.
217      */
218     private static final class ArrayKey {
219 
220         private static int computeHashCode(final Object[] keys) {
221             final int prime = 31;
222             int result = 1;
223             result = prime * result + Arrays.hashCode(keys);
224             return result;
225         }
226 
227         private final Object[] keys;
228         private final int hashCode;
229 
230         /**
231          * Constructs an instance of {@code MultipartKey} to hold the specified objects.
232          *
233          * @param keys the set of objects that make up the key.  Each key may be null.
234          */
235         ArrayKey(final Object... keys) {
236             this.keys = keys;
237             this.hashCode = computeHashCode(keys);
238         }
239 
240         @Override
241         public int hashCode() {
242             return hashCode;
243         }
244 
245         @Override
246         public boolean equals(final Object obj) {
247             if (this == obj) {
248                 return true;
249             }
250             if (obj == null) {
251                 return false;
252             }
253             if (getClass() != obj.getClass()) {
254                 return false;
255             }
256             final ArrayKey other = (ArrayKey) obj;
257             return Arrays.deepEquals(keys, other.keys);
258         }
259 
260 
261     }
262 
263 }