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 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 * 36 * @since 3.0 37 */ 38 // TODO: Before making public move from getDateTimeInstance(Integer, ...) to int; or some other approach. 39 abstract class AbstractFormatCache<F extends Format> { 40 41 /** 42 * Helper class to hold multipart Map keys as arrays. 43 */ 44 private static final class ArrayKey { 45 46 private static int computeHashCode(final Object[] keys) { 47 final int prime = 31; 48 int result = 1; 49 result = prime * result + Arrays.hashCode(keys); 50 return result; 51 } 52 53 private final Object[] keys; 54 private final int hashCode; 55 56 /** 57 * Constructs an instance of {@link MultipartKey} to hold the specified objects. 58 * 59 * @param keys the set of objects that make up the key. Each key may be null. 60 */ 61 ArrayKey(final Object... keys) { 62 this.keys = keys; 63 this.hashCode = computeHashCode(keys); 64 } 65 66 @Override 67 public boolean equals(final Object obj) { 68 if (this == obj) { 69 return true; 70 } 71 if (obj == null) { 72 return false; 73 } 74 if (getClass() != obj.getClass()) { 75 return false; 76 } 77 final ArrayKey other = (ArrayKey) obj; 78 return Arrays.deepEquals(keys, other.keys); 79 } 80 81 @Override 82 public int hashCode() { 83 return hashCode; 84 } 85 86 } 87 88 /** 89 * No date or no time. Used in same parameters as DateFormat.SHORT or DateFormat.LONG 90 */ 91 static final int NONE = -1; 92 93 private static final ConcurrentMap<ArrayKey, String> cDateTimeInstanceCache = new ConcurrentHashMap<>(7); 94 95 /** 96 * Gets a date/time format for the specified styles and locale. 97 * 98 * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT, null indicates no date in format 99 * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT, null indicates no time in format 100 * @param locale The non-null locale of the desired format 101 * @return a localized standard date/time format 102 * @throws IllegalArgumentException if the Locale has no date/time pattern defined 103 */ 104 // package protected, for access from test code; do not make public or protected 105 static String getPatternForStyle(final Integer dateStyle, final Integer timeStyle, final Locale locale) { 106 final Locale safeLocale = LocaleUtils.toLocale(locale); 107 final ArrayKey key = new ArrayKey(dateStyle, timeStyle, safeLocale); 108 return cDateTimeInstanceCache.computeIfAbsent(key, k -> { 109 try { 110 final DateFormat formatter; 111 if (dateStyle == null) { 112 formatter = DateFormat.getTimeInstance(timeStyle.intValue(), safeLocale); 113 } else if (timeStyle == null) { 114 formatter = DateFormat.getDateInstance(dateStyle.intValue(), safeLocale); 115 } else { 116 formatter = DateFormat.getDateTimeInstance(dateStyle.intValue(), timeStyle.intValue(), safeLocale); 117 } 118 return ((SimpleDateFormat) formatter).toPattern(); 119 } catch (final ClassCastException ex) { 120 throw new IllegalArgumentException("No date time pattern for locale: " + safeLocale); 121 } 122 }); 123 } 124 125 private final ConcurrentMap<ArrayKey, F> cInstanceCache = new ConcurrentHashMap<>(7); 126 127 /** 128 * Create a format instance using the specified pattern, time zone 129 * and locale. 130 * 131 * @param pattern {@link java.text.SimpleDateFormat} compatible pattern, this will not be null. 132 * @param timeZone time zone, this will not be null. 133 * @param locale locale, this will not be null. 134 * @return a pattern based date/time formatter 135 * @throws IllegalArgumentException if pattern is invalid 136 * or {@code null} 137 */ 138 protected abstract F createInstance(String pattern, TimeZone timeZone, Locale locale); 139 140 /** 141 * Gets a date formatter instance using the specified style, 142 * time zone and locale. 143 * 144 * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT 145 * @param timeZone optional time zone, overrides time zone of 146 * formatted date, null means use default Locale 147 * @param locale optional locale, overrides system locale 148 * @return a localized standard date/time formatter 149 * @throws IllegalArgumentException if the Locale has no date/time 150 * pattern defined 151 */ 152 // package protected, for access from FastDateFormat; do not make public or protected 153 F getDateInstance(final int dateStyle, final TimeZone timeZone, final Locale locale) { 154 return getDateTimeInstance(Integer.valueOf(dateStyle), null, timeZone, locale); 155 } 156 157 /** 158 * Gets a date/time formatter instance using the specified style, 159 * time zone and locale. 160 * 161 * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT 162 * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT 163 * @param timeZone optional time zone, overrides time zone of 164 * formatted date, null means use default Locale 165 * @param locale optional locale, overrides system locale 166 * @return a localized standard date/time formatter 167 * @throws IllegalArgumentException if the Locale has no date/time 168 * pattern defined 169 */ 170 // package protected, for access from FastDateFormat; do not make public or protected 171 F getDateTimeInstance(final int dateStyle, final int timeStyle, final TimeZone timeZone, final Locale locale) { 172 return getDateTimeInstance(Integer.valueOf(dateStyle), Integer.valueOf(timeStyle), timeZone, locale); 173 } 174 175 /** 176 * Gets a date/time formatter instance using the specified style, 177 * time zone and locale. 178 * 179 * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT, null indicates no date in format 180 * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT, null indicates no time in format 181 * @param timeZone optional time zone, overrides time zone of 182 * 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 186 * pattern defined 187 */ 188 // This must remain private, see LANG-884 189 private F getDateTimeInstance(final Integer dateStyle, final Integer timeStyle, final TimeZone timeZone, Locale locale) { 190 locale = LocaleUtils.toLocale(locale); 191 final String pattern = getPatternForStyle(dateStyle, timeStyle, locale); 192 return getInstance(pattern, timeZone, locale); 193 } 194 195 /** 196 * Gets a formatter instance using the default pattern in the 197 * default time zone and locale. 198 * 199 * @return a date/time formatter 200 */ 201 public F getInstance() { 202 return getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, TimeZone.getDefault(), Locale.getDefault()); 203 } 204 205 /** 206 * Gets a formatter instance using the specified pattern, time zone 207 * and locale. 208 * 209 * @param pattern {@link java.text.SimpleDateFormat} compatible 210 * pattern, non-null 211 * @param timeZone the time zone, null means use the default TimeZone 212 * @param locale the locale, null means use the default Locale 213 * @return a pattern based date/time formatter 214 * @throws NullPointerException if pattern is {@code null} 215 * @throws IllegalArgumentException if pattern is invalid 216 */ 217 public F getInstance(final String pattern, final TimeZone timeZone, final Locale locale) { 218 Objects.requireNonNull(pattern, "pattern"); 219 final TimeZone actualTimeZone = TimeZones.toTimeZone(timeZone); 220 final Locale actualLocale = LocaleUtils.toLocale(locale); 221 final ArrayKey key = new ArrayKey(pattern, actualTimeZone, actualLocale); 222 return cInstanceCache.computeIfAbsent(key, k -> createInstance(pattern, actualTimeZone, actualLocale)); 223 } 224 225 /** 226 * Gets a time formatter instance using the specified style, 227 * time zone and locale. 228 * 229 * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT 230 * @param timeZone optional time zone, overrides time zone of 231 * formatted date, null means use default Locale 232 * @param locale optional locale, overrides system locale 233 * @return a localized standard date/time formatter 234 * @throws IllegalArgumentException if the Locale has no date/time 235 * pattern defined 236 */ 237 // package protected, for access from FastDateFormat; do not make public or protected 238 F getTimeInstance(final int timeStyle, final TimeZone timeZone, final Locale locale) { 239 return getDateTimeInstance(null, Integer.valueOf(timeStyle), timeZone, locale); 240 } 241 242 }