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 }