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 18 package org.apache.log4j.pattern; 19 20 import java.text.DateFormat; 21 import java.text.FieldPosition; 22 import java.text.NumberFormat; 23 import java.text.ParsePosition; 24 import java.util.Date; 25 import java.util.TimeZone; 26 27 28 /** 29 * CachedDateFormat optimizes the performance of a wrapped 30 * DateFormat. The implementation is not thread-safe. 31 * If the millisecond pattern is not recognized, 32 * the class will only use the cache if the 33 * same value is requested. 34 * 35 */ 36 public final class CachedDateFormat extends DateFormat { 37 /** 38 * Serialization version. 39 */ 40 private static final long serialVersionUID = 1; 41 /** 42 * Constant used to represent that there was no change 43 * observed when changing the millisecond count. 44 */ 45 public static final int NO_MILLISECONDS = -2; 46 47 /** 48 * Supported digit set. If the wrapped DateFormat uses 49 * a different unit set, the millisecond pattern 50 * will not be recognized and duplicate requests 51 * will use the cache. 52 */ 53 private static final String DIGITS = "0123456789"; 54 55 /** 56 * Constant used to represent that there was an 57 * observed change, but was an expected change. 58 */ 59 public static final int UNRECOGNIZED_MILLISECONDS = -1; 60 61 /** 62 * First magic number used to detect the millisecond position. 63 */ 64 private static final int MAGIC1 = 654; 65 66 /** 67 * Expected representation of first magic number. 68 */ 69 private static final String MAGICSTRING1 = "654"; 70 71 /** 72 * Second magic number used to detect the millisecond position. 73 */ 74 private static final int MAGIC2 = 987; 75 76 /** 77 * Expected representation of second magic number. 78 */ 79 private static final String MAGICSTRING2 = "987"; 80 81 /** 82 * Expected representation of 0 milliseconds. 83 */ 84 private static final String ZERO_STRING = "000"; 85 86 /** 87 * Wrapped formatter. 88 */ 89 private final DateFormat formatter; 90 91 /** 92 * Index of initial digit of millisecond pattern or 93 * UNRECOGNIZED_MILLISECONDS or NO_MILLISECONDS. 94 */ 95 private int millisecondStart; 96 97 /** 98 * Integral second preceding the previous convered Date. 99 */ 100 private long slotBegin; 101 102 /** 103 * Cache of previous conversion. 104 */ 105 private StringBuffer cache = new StringBuffer(50); 106 107 /** 108 * Maximum validity period for the cache. 109 * Typically 1, use cache for duplicate requests only, or 110 * 1000, use cache for requests within the same integral second. 111 */ 112 private final int expiration; 113 114 /** 115 * Date requested in previous conversion. 116 */ 117 private long previousTime; 118 119 /** 120 * Scratch date object used to minimize date object creation. 121 */ 122 private final Date tmpDate = new Date(0); 123 124 /** 125 * Creates a new CachedDateFormat object. 126 * @param dateFormat Date format, may not be null. 127 * @param expiration maximum cached range in milliseconds. 128 * If the dateFormat is known to be incompatible with the 129 * caching algorithm, use a value of 0 to totally disable 130 * caching or 1 to only use cache for duplicate requests. 131 */ 132 public CachedDateFormat(final DateFormat dateFormat, final int expiration) { 133 if (dateFormat == null) { 134 throw new IllegalArgumentException("dateFormat cannot be null"); 135 } 136 137 if (expiration < 0) { 138 throw new IllegalArgumentException("expiration must be non-negative"); 139 } 140 141 formatter = dateFormat; 142 this.expiration = expiration; 143 millisecondStart = 0; 144 145 // 146 // set the previousTime so the cache will be invalid 147 // for the next request. 148 previousTime = Long.MIN_VALUE; 149 slotBegin = Long.MIN_VALUE; 150 } 151 152 /** 153 * Finds start of millisecond field in formatted time. 154 * @param time long time, must be integral number of seconds 155 * @param formatted String corresponding formatted string 156 * @param formatter DateFormat date format 157 * @return int position in string of first digit of milliseconds, 158 * -1 indicates no millisecond field, -2 indicates unrecognized 159 * field (likely RelativeTimeDateFormat) 160 */ 161 public static int findMillisecondStart( 162 final long time, final String formatted, final DateFormat formatter) { 163 long slotBegin = (time / 1000) * 1000; 164 165 if (slotBegin > time) { 166 slotBegin -= 1000; 167 } 168 169 int millis = (int) (time - slotBegin); 170 171 int magic = MAGIC1; 172 String magicString = MAGICSTRING1; 173 174 if (millis == MAGIC1) { 175 magic = MAGIC2; 176 magicString = MAGICSTRING2; 177 } 178 179 String plusMagic = formatter.format(new Date(slotBegin + magic)); 180 181 /** 182 * If the string lengths differ then 183 * we can't use the cache except for duplicate requests. 184 */ 185 if (plusMagic.length() != formatted.length()) { 186 return UNRECOGNIZED_MILLISECONDS; 187 } else { 188 // find first difference between values 189 for (int i = 0; i < formatted.length(); i++) { 190 if (formatted.charAt(i) != plusMagic.charAt(i)) { 191 // 192 // determine the expected digits for the base time 193 StringBuffer formattedMillis = new StringBuffer("ABC"); 194 millisecondFormat(millis, formattedMillis, 0); 195 196 String plusZero = formatter.format(new Date(slotBegin)); 197 198 // If the next 3 characters match the magic 199 // string and the expected string 200 if ( 201 (plusZero.length() == formatted.length()) 202 && magicString.regionMatches( 203 0, plusMagic, i, magicString.length()) 204 && formattedMillis.toString().regionMatches( 205 0, formatted, i, magicString.length()) 206 && ZERO_STRING.regionMatches( 207 0, plusZero, i, ZERO_STRING.length())) { 208 return i; 209 } else { 210 return UNRECOGNIZED_MILLISECONDS; 211 } 212 } 213 } 214 } 215 216 return NO_MILLISECONDS; 217 } 218 219 /** 220 * Formats a Date into a date/time string. 221 * 222 * @param date the date to format. 223 * @param sbuf the string buffer to write to. 224 * @param fieldPosition remains untouched. 225 * @return the formatted time string. 226 */ 227 public StringBuffer format( 228 Date date, StringBuffer sbuf, FieldPosition fieldPosition) { 229 format(date.getTime(), sbuf); 230 231 return sbuf; 232 } 233 234 /** 235 * Formats a millisecond count into a date/time string. 236 * 237 * @param now Number of milliseconds after midnight 1 Jan 1970 GMT. 238 * @param buf the string buffer to write to. 239 * @return the formatted time string. 240 */ 241 public StringBuffer format(long now, StringBuffer buf) { 242 // 243 // If the current requested time is identical to the previously 244 // requested time, then append the cache contents. 245 // 246 if (now == previousTime) { 247 buf.append(cache); 248 249 return buf; 250 } 251 252 // 253 // If millisecond pattern was not unrecognized 254 // (that is if it was found or milliseconds did not appear) 255 // 256 if (millisecondStart != UNRECOGNIZED_MILLISECONDS && 257 // Check if the cache is still valid. 258 // If the requested time is within the same integral second 259 // as the last request and a shorter expiration was not requested. 260 (now < (slotBegin + expiration)) && (now >= slotBegin) 261 && (now < (slotBegin + 1000L))) { 262 // 263 // if there was a millisecond field then update it 264 // 265 if (millisecondStart >= 0) { 266 millisecondFormat((int) (now - slotBegin), cache, millisecondStart); 267 } 268 269 // 270 // update the previously requested time 271 // (the slot begin should be unchanged) 272 previousTime = now; 273 buf.append(cache); 274 275 return buf; 276 } 277 278 // 279 // could not use previous value. 280 // Call underlying formatter to format date. 281 cache.setLength(0); 282 tmpDate.setTime(now); 283 cache.append(formatter.format(tmpDate)); 284 buf.append(cache); 285 previousTime = now; 286 slotBegin = (previousTime / 1000) * 1000; 287 288 if (slotBegin > previousTime) { 289 slotBegin -= 1000; 290 } 291 292 // 293 // if the milliseconds field was previous found 294 // then reevaluate in case it moved. 295 // 296 if (millisecondStart >= 0) { 297 millisecondStart = 298 findMillisecondStart(now, cache.toString(), formatter); 299 } 300 301 return buf; 302 } 303 304 /** 305 * Formats a count of milliseconds (0-999) into a numeric representation. 306 * @param millis Millisecond coun between 0 and 999. 307 * @param buf String buffer, may not be null. 308 * @param offset Starting position in buffer, the length of the 309 * buffer must be at least offset + 3. 310 */ 311 private static void millisecondFormat( 312 final int millis, final StringBuffer buf, final int offset) { 313 buf.setCharAt(offset, DIGITS.charAt(millis / 100)); 314 buf.setCharAt(offset + 1, DIGITS.charAt((millis / 10) % 10)); 315 buf.setCharAt(offset + 2, DIGITS.charAt(millis % 10)); 316 } 317 318 /** 319 * Set timezone. 320 * 321 * Setting the timezone using getCalendar().setTimeZone() 322 * will likely cause caching to misbehave. 323 * @param timeZone TimeZone new timezone 324 */ 325 public void setTimeZone(final TimeZone timeZone) { 326 formatter.setTimeZone(timeZone); 327 previousTime = Long.MIN_VALUE; 328 slotBegin = Long.MIN_VALUE; 329 } 330 331 /** 332 * This method is delegated to the formatter which most 333 * likely returns null. 334 * @param s string representation of date. 335 * @param pos field position, unused. 336 * @return parsed date, likely null. 337 */ 338 public Date parse(String s, ParsePosition pos) { 339 return formatter.parse(s, pos); 340 } 341 342 /** 343 * Gets number formatter. 344 * 345 * @return NumberFormat number formatter 346 */ 347 public NumberFormat getNumberFormat() { 348 return formatter.getNumberFormat(); 349 } 350 351 /** 352 * Gets maximum cache validity for the specified SimpleDateTime 353 * conversion pattern. 354 * @param pattern conversion pattern, may not be null. 355 * @return Duration in milliseconds from an integral second 356 * that the cache will return consistent results. 357 */ 358 public static int getMaximumCacheValidity(final String pattern) { 359 // 360 // If there are more "S" in the pattern than just one "SSS" then 361 // (for example, "HH:mm:ss,SSS SSS"), then set the expiration to 362 // one millisecond which should only perform duplicate request caching. 363 // 364 int firstS = pattern.indexOf('S'); 365 366 if ((firstS >= 0) && (firstS != pattern.lastIndexOf("SSS"))) { 367 return 1; 368 } 369 370 return 1000; 371 } 372 }