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
19 package org.apache.juli;
20
21
22 import java.text.SimpleDateFormat;
23 import java.util.Date;
24 import java.util.Locale;
25 import java.util.TimeZone;
26
27 /**
28 * <p>Cache structure for SimpleDateFormat formatted timestamps based on
29 * seconds.</p>
30 *
31 * <p>Millisecond formatting using S is not supported. You should add the
32 * millisecond information after getting back the second formatting.</p>
33 *
34 * <p>The cache consists of entries for a consecutive range of
35 * seconds. The length of the range is configurable. It is
36 * implemented based on a cyclic buffer. New entries shift the range.</p>
37 *
38 * <p>The cache is not threadsafe. It can be used without synchronization
39 * via thread local instances, or with synchronization as a global cache.</p>
40 *
41 * <p>The cache can be created with a parent cache to build a cache hierarchy.
42 * Access to the parent cache is threadsafe.</p>
43 */
44 public class DateFormatCache {
45
46 public static final char MSEC_PATTERN = '#';
47
48 /* Timestamp format */
49 private final String format;
50
51 /* Number of cached entries */
52 private final int cacheSize;
53
54 private final Cache cache;
55
56 /**
57 * Replace the millisecond formatting character 'S' by
58 * some dummy characters in order to make the resulting
59 * formatted time stamps cacheable. Our consumer might
60 * choose to replace the dummy chars with the actual
61 * milliseconds because that's relatively cheap.
62 */
63 private String tidyFormat(String format) {
64 boolean escape = false;
65 StringBuilder result = new StringBuilder();
66 int len = format.length();
67 char x;
68 for (int i = 0; i < len; i++) {
69 x = format.charAt(i);
70 if (escape || x != 'S') {
71 result.append(x);
72 } else {
73 result.append(MSEC_PATTERN);
74 }
75 if (x == '\'') {
76 escape = !escape;
77 }
78 }
79 return result.toString();
80 }
81
82 public DateFormatCache(int size, String format, DateFormatCache parent) {
83 cacheSize = size;
84 this.format = tidyFormat(format);
85 Cache parentCache = null;
86 if (parent != null) {
87 synchronized(parent) {
88 parentCache = parent.cache;
89 }
90 }
91 cache = new Cache(parentCache);
92 }
93
94 public String getFormat(long time) {
95 return cache.getFormat(time);
96 }
97
98 public String getTimeFormat() {
99 return format;
100 }
101
102 private class Cache {
103
104 /* Second formatted in most recent invocation */
105 private long previousSeconds = Long.MIN_VALUE;
106 /* Formatted timestamp generated in most recent invocation */
107 private String previousFormat = "";
108
109 /* First second contained in cache */
110 private long first = Long.MIN_VALUE;
111 /* Last second contained in cache */
112 private long last = Long.MIN_VALUE;
113 /* Index of "first" in the cyclic cache */
114 private int offset = 0;
115 /* Helper object to be able to call SimpleDateFormat.format(). */
116 private final Date currentDate = new Date();
117
118 private String cache[];
119 private SimpleDateFormat formatter;
120
121 private Cache parent = null;
122
123 private Cache(Cache parent) {
124 cache = new String[cacheSize];
125 formatter = new SimpleDateFormat(format, Locale.US);
126 formatter.setTimeZone(TimeZone.getDefault());
127 this.parent = parent;
128 }
129
130 private String getFormat(long time) {
131
132 long seconds = time / 1000;
133
134 /* First step: if we have seen this timestamp
135 during the previous call, return the previous value. */
136 if (seconds == previousSeconds) {
137 return previousFormat;
138 }
139
140 /* Second step: Try to locate in cache */
141 previousSeconds = seconds;
142 int index = (offset + (int)(seconds - first)) % cacheSize;
143 if (index < 0) {
144 index += cacheSize;
145 }
146 if (seconds >= first && seconds <= last) {
147 if (cache[index] != null) {
148 /* Found, so remember for next call and return.*/
149 previousFormat = cache[index];
150 return previousFormat;
151 }
152
153 /* Third step: not found in cache, adjust cache and add item */
154 } else if (seconds >= last + cacheSize || seconds <= first - cacheSize) {
155 first = seconds;
156 last = first + cacheSize - 1;
157 index = 0;
158 offset = 0;
159 for (int i = 1; i < cacheSize; i++) {
160 cache[i] = null;
161 }
162 } else if (seconds > last) {
163 for (int i = 1; i < seconds - last; i++) {
164 cache[(index + cacheSize - i) % cacheSize] = null;
165 }
166 first = seconds - (cacheSize - 1);
167 last = seconds;
168 offset = (index + 1) % cacheSize;
169 } else if (seconds < first) {
170 for (int i = 1; i < first - seconds; i++) {
171 cache[(index + i) % cacheSize] = null;
172 }
173 first = seconds;
174 last = seconds + (cacheSize - 1);
175 offset = index;
176 }
177
178 /* Last step: format new timestamp either using
179 * parent cache or locally. */
180 if (parent != null) {
181 synchronized(parent) {
182 previousFormat = parent.getFormat(time);
183 }
184 } else {
185 currentDate.setTime(time);
186 previousFormat = formatter.format(currentDate);
187 }
188 cache[index] = previousFormat;
189 return previousFormat;
190 }
191 }
192 }
193