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