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 }