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 }