/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.log4j.pattern; import java.text.DateFormat; import java.text.FieldPosition; import java.text.NumberFormat; import java.text.ParsePosition; import java.util.Date; import java.util.TimeZone; /** * CachedDateFormat optimizes the performance of a wrapped * DateFormat. The implementation is not thread-safe. * If the millisecond pattern is not recognized, * the class will only use the cache if the * same value is requested. * */ public final class CachedDateFormat extends DateFormat { /** * Serialization version. */ private static final long serialVersionUID = 1; /** * Constant used to represent that there was no change * observed when changing the millisecond count. */ public static final int NO_MILLISECONDS = -2; /** * Supported digit set. If the wrapped DateFormat uses * a different unit set, the millisecond pattern * will not be recognized and duplicate requests * will use the cache. */ private static final String DIGITS = "0123456789"; /** * Constant used to represent that there was an * observed change, but was an expected change. */ public static final int UNRECOGNIZED_MILLISECONDS = -1; /** * First magic number used to detect the millisecond position. */ private static final int MAGIC1 = 654; /** * Expected representation of first magic number. */ private static final String MAGICSTRING1 = "654"; /** * Second magic number used to detect the millisecond position. */ private static final int MAGIC2 = 987; /** * Expected representation of second magic number. */ private static final String MAGICSTRING2 = "987"; /** * Expected representation of 0 milliseconds. */ private static final String ZERO_STRING = "000"; /** * Wrapped formatter. */ private final DateFormat formatter; /** * Index of initial digit of millisecond pattern or * UNRECOGNIZED_MILLISECONDS or NO_MILLISECONDS. */ private int millisecondStart; /** * Integral second preceding the previous convered Date. */ private long slotBegin; /** * Cache of previous conversion. */ private StringBuffer cache = new StringBuffer(50); /** * Maximum validity period for the cache. * Typically 1, use cache for duplicate requests only, or * 1000, use cache for requests within the same integral second. */ private final int expiration; /** * Date requested in previous conversion. */ private long previousTime; /** * Scratch date object used to minimize date object creation. */ private final Date tmpDate = new Date(0); /** * Creates a new CachedDateFormat object. * @param dateFormat Date format, may not be null. * @param expiration maximum cached range in milliseconds. * If the dateFormat is known to be incompatible with the * caching algorithm, use a value of 0 to totally disable * caching or 1 to only use cache for duplicate requests. */ public CachedDateFormat(final DateFormat dateFormat, final int expiration) { if (dateFormat == null) { throw new IllegalArgumentException("dateFormat cannot be null"); } if (expiration < 0) { throw new IllegalArgumentException("expiration must be non-negative"); } formatter = dateFormat; this.expiration = expiration; millisecondStart = 0; // // set the previousTime so the cache will be invalid // for the next request. previousTime = Long.MIN_VALUE; slotBegin = Long.MIN_VALUE; } /** * Finds start of millisecond field in formatted time. * @param time long time, must be integral number of seconds * @param formatted String corresponding formatted string * @param formatter DateFormat date format * @return int position in string of first digit of milliseconds, * -1 indicates no millisecond field, -2 indicates unrecognized * field (likely RelativeTimeDateFormat) */ public static int findMillisecondStart( final long time, final String formatted, final DateFormat formatter) { long slotBegin = (time / 1000) * 1000; if (slotBegin > time) { slotBegin -= 1000; } int millis = (int) (time - slotBegin); int magic = MAGIC1; String magicString = MAGICSTRING1; if (millis == MAGIC1) { magic = MAGIC2; magicString = MAGICSTRING2; } String plusMagic = formatter.format(new Date(slotBegin + magic)); /** * If the string lengths differ then * we can't use the cache except for duplicate requests. */ if (plusMagic.length() != formatted.length()) { return UNRECOGNIZED_MILLISECONDS; } else { // find first difference between values for (int i = 0; i < formatted.length(); i++) { if (formatted.charAt(i) != plusMagic.charAt(i)) { // // determine the expected digits for the base time StringBuffer formattedMillis = new StringBuffer("ABC"); millisecondFormat(millis, formattedMillis, 0); String plusZero = formatter.format(new Date(slotBegin)); // If the next 3 characters match the magic // string and the expected string if ( (plusZero.length() == formatted.length()) && magicString.regionMatches( 0, plusMagic, i, magicString.length()) && formattedMillis.toString().regionMatches( 0, formatted, i, magicString.length()) && ZERO_STRING.regionMatches( 0, plusZero, i, ZERO_STRING.length())) { return i; } else { return UNRECOGNIZED_MILLISECONDS; } } } } return NO_MILLISECONDS; } /** * Formats a Date into a date/time string. * * @param date the date to format. * @param sbuf the string buffer to write to. * @param fieldPosition remains untouched. * @return the formatted time string. */ public StringBuffer format( Date date, StringBuffer sbuf, FieldPosition fieldPosition) { format(date.getTime(), sbuf); return sbuf; } /** * Formats a millisecond count into a date/time string. * * @param now Number of milliseconds after midnight 1 Jan 1970 GMT. * @param buf the string buffer to write to. * @return the formatted time string. */ public StringBuffer format(long now, StringBuffer buf) { // // If the current requested time is identical to the previously // requested time, then append the cache contents. // if (now == previousTime) { buf.append(cache); return buf; } // // If millisecond pattern was not unrecognized // (that is if it was found or milliseconds did not appear) // if (millisecondStart != UNRECOGNIZED_MILLISECONDS && // Check if the cache is still valid. // If the requested time is within the same integral second // as the last request and a shorter expiration was not requested. (now < (slotBegin + expiration)) && (now >= slotBegin) && (now < (slotBegin + 1000L))) { // // if there was a millisecond field then update it // if (millisecondStart >= 0) { millisecondFormat((int) (now - slotBegin), cache, millisecondStart); } // // update the previously requested time // (the slot begin should be unchanged) previousTime = now; buf.append(cache); return buf; } // // could not use previous value. // Call underlying formatter to format date. cache.setLength(0); tmpDate.setTime(now); cache.append(formatter.format(tmpDate)); buf.append(cache); previousTime = now; slotBegin = (previousTime / 1000) * 1000; if (slotBegin > previousTime) { slotBegin -= 1000; } // // if the milliseconds field was previous found // then reevaluate in case it moved. // if (millisecondStart >= 0) { millisecondStart = findMillisecondStart(now, cache.toString(), formatter); } return buf; } /** * Formats a count of milliseconds (0-999) into a numeric representation. * @param millis Millisecond coun between 0 and 999. * @param buf String buffer, may not be null. * @param offset Starting position in buffer, the length of the * buffer must be at least offset + 3. */ private static void millisecondFormat( final int millis, final StringBuffer buf, final int offset) { buf.setCharAt(offset, DIGITS.charAt(millis / 100)); buf.setCharAt(offset + 1, DIGITS.charAt((millis / 10) % 10)); buf.setCharAt(offset + 2, DIGITS.charAt(millis % 10)); } /** * Set timezone. * * Setting the timezone using getCalendar().setTimeZone() * will likely cause caching to misbehave. * @param timeZone TimeZone new timezone */ public void setTimeZone(final TimeZone timeZone) { formatter.setTimeZone(timeZone); previousTime = Long.MIN_VALUE; slotBegin = Long.MIN_VALUE; } /** * This method is delegated to the formatter which most * likely returns null. * @param s string representation of date. * @param pos field position, unused. * @return parsed date, likely null. */ public Date parse(String s, ParsePosition pos) { return formatter.parse(s, pos); } /** * Gets number formatter. * * @return NumberFormat number formatter */ public NumberFormat getNumberFormat() { return formatter.getNumberFormat(); } /** * Gets maximum cache validity for the specified SimpleDateTime * conversion pattern. * @param pattern conversion pattern, may not be null. * @return Duration in milliseconds from an integral second * that the cache will return consistent results. */ public static int getMaximumCacheValidity(final String pattern) { // // If there are more "S" in the pattern than just one "SSS" then // (for example, "HH:mm:ss,SSS SSS"), then set the expiration to // one millisecond which should only perform duplicate request caching. // int firstS = pattern.indexOf('S'); if ((firstS >= 0) && (firstS != pattern.lastIndexOf("SSS"))) { return 1; } return 1000; } }