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) {
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 or {@code null}.
142 */
143 protected abstract F createInstance(String pattern, TimeZone timeZone, Locale locale);
144
145 /**
146 * Gets a date formatter instance using the specified style,
147 * time zone and locale.
148 *
149 * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT.
150 * @param timeZone optional time zone, overrides time zone of formatted date, null means use default Locale.
151 * @param locale optional locale, overrides system locale.
152 * @return a localized standard date/time formatter.
153 * @throws IllegalArgumentException if the Locale has no date/time pattern defined.
154 */
155 // package protected, for access from FastDateFormat; do not make public or protected
156 F getDateInstance(final int dateStyle, final TimeZone timeZone, final Locale locale) {
157 return getDateTimeInstance(Integer.valueOf(dateStyle), null, timeZone, locale);
158 }
159
160 /**
161 * Gets a date/time formatter instance using the specified style,
162 * time zone and locale.
163 *
164 * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT.
165 * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT.
166 * @param timeZone optional time zone, overrides time zone of formatted date, null means use default Locale.
167 * @param locale optional locale, overrides system locale.
168 * @return a localized standard date/time formatter.
169 * @throws IllegalArgumentException if the Locale has no date/time pattern defined.
170 */
171 // package protected, for access from FastDateFormat; do not make public or protected
172 F getDateTimeInstance(final int dateStyle, final int timeStyle, final TimeZone timeZone, final Locale locale) {
173 return getDateTimeInstance(Integer.valueOf(dateStyle), Integer.valueOf(timeStyle), timeZone, locale);
174 }
175
176 /**
177 * Gets a date/time formatter instance using the specified style,
178 * time zone and locale.
179 *
180 * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT, null indicates no date in format.
181 * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT, null indicates no time in format.
182 * @param timeZone optional time zone, overrides time zone of formatted date, null means use default Locale.
183 * @param locale optional locale, overrides system locale.
184 * @return a localized standard date/time formatter.
185 * @throws IllegalArgumentException if the Locale has no date/time pattern defined.
186 */
187 // This must remain private, see LANG-884
188 private F getDateTimeInstance(final Integer dateStyle, final Integer timeStyle, final TimeZone timeZone, Locale locale) {
189 locale = LocaleUtils.toLocale(locale);
190 return getInstance(getPatternForStyle(dateStyle, timeStyle, locale), timeZone, locale);
191 }
192
193 /**
194 * Gets a formatter instance using the default pattern in the
195 * default time zone and locale.
196 *
197 * @return a date/time formatter.
198 */
199 public F getInstance() {
200 return getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, TimeZone.getDefault(), Locale.getDefault());
201 }
202
203 /**
204 * Gets a formatter instance using the specified pattern, time zone
205 * and locale.
206 *
207 * @param pattern {@link java.text.SimpleDateFormat} compatible pattern, non-null.
208 * @param timeZone the time zone, null means use the default TimeZone.
209 * @param locale the locale, null means use the default Locale.
210 * @return a pattern based date/time formatter.
211 * @throws NullPointerException if pattern is {@code null}.
212 * @throws IllegalArgumentException if pattern is invalid.
213 */
214 public F getInstance(final String pattern, final TimeZone timeZone, final Locale locale) {
215 Objects.requireNonNull(pattern, "pattern");
216 final TimeZone actualTimeZone = TimeZones.toTimeZone(timeZone);
217 final Locale actualLocale = LocaleUtils.toLocale(locale);
218 final ArrayKey key = new ArrayKey(pattern, actualTimeZone, actualLocale);
219 return instanceCache.computeIfAbsent(key, k -> createInstance(pattern, actualTimeZone, actualLocale));
220 }
221
222 /**
223 * Gets a time formatter instance using the specified style,
224 * time zone and locale.
225 *
226 * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT.
227 * @param timeZone optional time zone, overrides time zone of formatted date, null means use default Locale.
228 * @param locale optional locale, overrides system locale.
229 * @return a localized standard date/time formatter.
230 * @throws IllegalArgumentException if the Locale has no date/time pattern defined.
231 */
232 // package protected, for access from FastDateFormat; do not make public or protected
233 F getTimeInstance(final int timeStyle, final TimeZone timeZone, final Locale locale) {
234 return getDateTimeInstance(null, Integer.valueOf(timeStyle), timeZone, locale);
235 }
236
237 }