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