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