blob: ecbf31ebd742bfdafe397ea91ebee5263770fc39 [file] [log] [blame]
/*
* 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.juli;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
/**
* <p>Cache structure for SimpleDateFormat formatted timestamps based on
* seconds.</p>
*
* <p>Millisecond formatting using S is not supported. You should add the
* millisecond information after getting back the second formatting.</p>
*
* <p>The cache consists of entries for a consecutive range of
* seconds. The length of the range is configurable. It is
* implemented based on a cyclic buffer. New entries shift the range.</p>
*
* <p>The cache is not threadsafe. It can be used without synchronization
* via thread local instances, or with synchronization as a global cache.</p>
*
* <p>The cache can be created with a parent cache to build a cache hierarchy.
* Access to the parent cache is threadsafe.</p>
*/
public class DateFormatCache {
private static final String msecPattern = "#";
/* Timestamp format */
private final String format;
/* Number of cached entries */
private final int cacheSize;
private final Cache cache;
/**
* Replace the millisecond formatting character 'S' by
* some dummy characters in order to make the resulting
* formatted time stamps cacheable. Our consumer might
* choose to replace the dummy chars with the actual
* milliseconds because that's relatively cheap.
*/
private String tidyFormat(String format) {
boolean escape = false;
StringBuilder result = new StringBuilder();
int len = format.length();
char x;
for (int i = 0; i < len; i++) {
x = format.charAt(i);
if (escape || x != 'S') {
result.append(x);
} else {
result.append(msecPattern);
}
if (x == '\'') {
escape = !escape;
}
}
return result.toString();
}
public DateFormatCache(int size, String format, DateFormatCache parent) {
cacheSize = size;
this.format = tidyFormat(format);
Cache parentCache = null;
if (parent != null) {
synchronized(parent) {
parentCache = parent.cache;
}
}
cache = new Cache(parentCache);
}
public String getFormat(long time) {
return cache.getFormat(time);
}
private class Cache {
/* Second formatted in most recent invocation */
private long previousSeconds = Long.MIN_VALUE;
/* Formatted timestamp generated in most recent invocation */
private String previousFormat = "";
/* First second contained in cache */
private long first = Long.MIN_VALUE;
/* Last second contained in cache */
private long last = Long.MIN_VALUE;
/* Index of "first" in the cyclic cache */
private int offset = 0;
/* Helper object to be able to call SimpleDateFormat.format(). */
private final Date currentDate = new Date();
private String cache[];
private SimpleDateFormat formatter;
private Cache parent = null;
private Cache(Cache parent) {
cache = new String[cacheSize];
for (int i = 0; i < cacheSize; i++) {
cache[i] = null;
}
formatter = new SimpleDateFormat(format, Locale.US);
formatter.setTimeZone(TimeZone.getDefault());
this.parent = parent;
}
private String getFormat(long time) {
long seconds = time / 1000;
/* First step: if we have seen this timestamp
during the previous call, return the previous value. */
if (seconds == previousSeconds) {
return previousFormat;
}
/* Second step: Try to locate in cache */
previousSeconds = seconds;
int index = (offset + (int)(seconds - first)) % cacheSize;
if (index < 0) {
index += cacheSize;
}
if (seconds >= first && seconds <= last) {
if (cache[index] != null) {
/* Found, so remember for next call and return.*/
previousFormat = cache[index];
return previousFormat;
}
/* Third step: not found in cache, adjust cache and add item */
} else if (seconds >= last + cacheSize || seconds <= first - cacheSize) {
first = seconds;
last = first + cacheSize - 1;
index = 0;
offset = 0;
for (int i = 1; i < cacheSize; i++) {
cache[i] = null;
}
} else if (seconds > last) {
for (int i = 1; i < seconds - last; i++) {
cache[(index + cacheSize - i) % cacheSize] = null;
}
first = seconds - (cacheSize - 1);
last = seconds;
offset = (index + 1) % cacheSize;
} else if (seconds < first) {
for (int i = 1; i < first - seconds; i++) {
cache[(index + i) % cacheSize] = null;
}
first = seconds;
last = seconds + (cacheSize - 1);
offset = index;
}
/* Last step: format new timestamp either using
* parent cache or locally. */
if (parent != null) {
synchronized(parent) {
previousFormat = parent.getFormat(time);
}
} else {
currentDate.setTime(time);
previousFormat = formatter.format(currentDate);
}
cache[index] = previousFormat;
return previousFormat;
}
}
}