/*
* 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.googlecode.perftrace.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;
}
}