1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.apache.log4j.pattern;
19
20 import java.text.DateFormat;
21 import java.text.FieldPosition;
22 import java.text.NumberFormat;
23 import java.text.ParsePosition;
24 import java.util.Date;
25 import java.util.TimeZone;
26
27
28 /***
29 * CachedDateFormat optimizes the performance of a wrapped
30 * DateFormat. The implementation is not thread-safe.
31 * If the millisecond pattern is not recognized,
32 * the class will only use the cache if the
33 * same value is requested.
34 *
35 */
36 public final class CachedDateFormat extends DateFormat {
37 /***
38 * Constant used to represent that there was no change
39 * observed when changing the millisecond count.
40 */
41 public static final int NO_MILLISECONDS = -2;
42
43 /***
44 * Supported digit set. If the wrapped DateFormat uses
45 * a different unit set, the millisecond pattern
46 * will not be recognized and duplicate requests
47 * will use the cache.
48 */
49 private static final String DIGITS = "0123456789";
50
51 /***
52 * Constant used to represent that there was an
53 * observed change, but was an expected change.
54 */
55 public static final int UNRECOGNIZED_MILLISECONDS = -1;
56
57 /***
58 * First magic number used to detect the millisecond position.
59 */
60 private static final int MAGIC1 = 654;
61
62 /***
63 * Expected representation of first magic number.
64 */
65 private static final String MAGICSTRING1 = "654";
66
67 /***
68 * Second magic number used to detect the millisecond position.
69 */
70 private static final int MAGIC2 = 987;
71
72 /***
73 * Expected representation of second magic number.
74 */
75 private static final String MAGICSTRING2 = "987";
76
77 /***
78 * Expected representation of 0 milliseconds.
79 */
80 private static final String ZERO_STRING = "000";
81
82 /***
83 * Wrapped formatter.
84 */
85 private final DateFormat formatter;
86
87 /***
88 * Index of initial digit of millisecond pattern or
89 * UNRECOGNIZED_MILLISECONDS or NO_MILLISECONDS.
90 */
91 private int millisecondStart;
92
93 /***
94 * Integral second preceding the previous convered Date.
95 */
96 private long slotBegin;
97
98 /***
99 * Cache of previous conversion.
100 */
101 private StringBuffer cache = new StringBuffer(50);
102
103 /***
104 * Maximum validity period for the cache.
105 * Typically 1, use cache for duplicate requests only, or
106 * 1000, use cache for requests within the same integral second.
107 */
108 private final int expiration;
109
110 /***
111 * Date requested in previous conversion.
112 */
113 private long previousTime;
114
115 /***
116 * Scratch date object used to minimize date object creation.
117 */
118 private final Date tmpDate = new Date(0);
119
120 /***
121 * Creates a new CachedDateFormat object.
122 * @param dateFormat Date format, may not be null.
123 * @param expiration maximum cached range in milliseconds.
124 * If the dateFormat is known to be incompatible with the
125 * caching algorithm, use a value of 0 to totally disable
126 * caching or 1 to only use cache for duplicate requests.
127 */
128 public CachedDateFormat(final DateFormat dateFormat, final int expiration) {
129 if (dateFormat == null) {
130 throw new IllegalArgumentException("dateFormat cannot be null");
131 }
132
133 if (expiration < 0) {
134 throw new IllegalArgumentException("expiration must be non-negative");
135 }
136
137 formatter = dateFormat;
138 this.expiration = expiration;
139 millisecondStart = 0;
140
141
142
143
144 previousTime = Long.MIN_VALUE;
145 slotBegin = Long.MIN_VALUE;
146 }
147
148 /***
149 * Finds start of millisecond field in formatted time.
150 * @param time long time, must be integral number of seconds
151 * @param formatted String corresponding formatted string
152 * @param formatter DateFormat date format
153 * @return int position in string of first digit of milliseconds,
154 * -1 indicates no millisecond field, -2 indicates unrecognized
155 * field (likely RelativeTimeDateFormat)
156 */
157 public static int findMillisecondStart(
158 final long time, final String formatted, final DateFormat formatter) {
159 long slotBegin = (time / 1000) * 1000;
160
161 if (slotBegin > time) {
162 slotBegin -= 1000;
163 }
164
165 int millis = (int) (time - slotBegin);
166
167 int magic = MAGIC1;
168 String magicString = MAGICSTRING1;
169
170 if (millis == MAGIC1) {
171 magic = MAGIC2;
172 magicString = MAGICSTRING2;
173 }
174
175 String plusMagic = formatter.format(new Date(slotBegin + magic));
176
177 /***
178 * If the string lengths differ then
179 * we can't use the cache except for duplicate requests.
180 */
181 if (plusMagic.length() != formatted.length()) {
182 return UNRECOGNIZED_MILLISECONDS;
183 } else {
184
185 for (int i = 0; i < formatted.length(); i++) {
186 if (formatted.charAt(i) != plusMagic.charAt(i)) {
187
188
189 StringBuffer formattedMillis = new StringBuffer("ABC");
190 millisecondFormat(millis, formattedMillis, 0);
191
192 String plusZero = formatter.format(new Date(slotBegin));
193
194
195
196 if (
197 (plusZero.length() == formatted.length())
198 && magicString.regionMatches(
199 0, plusMagic, i, magicString.length())
200 && formattedMillis.toString().regionMatches(
201 0, formatted, i, magicString.length())
202 && ZERO_STRING.regionMatches(
203 0, plusZero, i, ZERO_STRING.length())) {
204 return i;
205 } else {
206 return UNRECOGNIZED_MILLISECONDS;
207 }
208 }
209 }
210 }
211
212 return NO_MILLISECONDS;
213 }
214
215 /***
216 * Formats a Date into a date/time string.
217 *
218 * @param date the date to format.
219 * @param sbuf the string buffer to write to.
220 * @param fieldPosition remains untouched.
221 * @return the formatted time string.
222 */
223 public StringBuffer format(
224 Date date, StringBuffer sbuf, FieldPosition fieldPosition) {
225 format(date.getTime(), sbuf);
226
227 return sbuf;
228 }
229
230 /***
231 * Formats a millisecond count into a date/time string.
232 *
233 * @param now Number of milliseconds after midnight 1 Jan 1970 GMT.
234 * @param buf the string buffer to write to.
235 * @return the formatted time string.
236 */
237 public StringBuffer format(long now, StringBuffer buf) {
238
239
240
241
242 if (now == previousTime) {
243 buf.append(cache);
244
245 return buf;
246 }
247
248
249
250
251
252 if (millisecondStart != UNRECOGNIZED_MILLISECONDS &&
253
254
255
256 (now < (slotBegin + expiration)) && (now >= slotBegin)
257 && (now < (slotBegin + 1000L))) {
258
259
260
261 if (millisecondStart >= 0) {
262 millisecondFormat((int) (now - slotBegin), cache, millisecondStart);
263 }
264
265
266
267
268 previousTime = now;
269 buf.append(cache);
270
271 return buf;
272 }
273
274
275
276
277 cache.setLength(0);
278 tmpDate.setTime(now);
279 cache.append(formatter.format(tmpDate));
280 buf.append(cache);
281 previousTime = now;
282 slotBegin = (previousTime / 1000) * 1000;
283
284 if (slotBegin > previousTime) {
285 slotBegin -= 1000;
286 }
287
288
289
290
291
292 if (millisecondStart >= 0) {
293 millisecondStart =
294 findMillisecondStart(now, cache.toString(), formatter);
295 }
296
297 return buf;
298 }
299
300 /***
301 * Formats a count of milliseconds (0-999) into a numeric representation.
302 * @param millis Millisecond coun between 0 and 999.
303 * @param buf String buffer, may not be null.
304 * @param offset Starting position in buffer, the length of the
305 * buffer must be at least offset + 3.
306 */
307 private static void millisecondFormat(
308 final int millis, final StringBuffer buf, final int offset) {
309 buf.setCharAt(offset, DIGITS.charAt(millis / 100));
310 buf.setCharAt(offset + 1, DIGITS.charAt((millis / 10) % 10));
311 buf.setCharAt(offset + 2, DIGITS.charAt(millis % 10));
312 }
313
314 /***
315 * Set timezone.
316 *
317 * Setting the timezone using getCalendar().setTimeZone()
318 * will likely cause caching to misbehave.
319 * @param timeZone TimeZone new timezone
320 */
321 public void setTimeZone(final TimeZone timeZone) {
322 formatter.setTimeZone(timeZone);
323 previousTime = Long.MIN_VALUE;
324 slotBegin = Long.MIN_VALUE;
325 }
326
327 /***
328 * This method is delegated to the formatter which most
329 * likely returns null.
330 * @param s string representation of date.
331 * @param pos field position, unused.
332 * @return parsed date, likely null.
333 */
334 public Date parse(String s, ParsePosition pos) {
335 return formatter.parse(s, pos);
336 }
337
338 /***
339 * Gets number formatter.
340 *
341 * @return NumberFormat number formatter
342 */
343 public NumberFormat getNumberFormat() {
344 return formatter.getNumberFormat();
345 }
346
347 /***
348 * Gets maximum cache validity for the specified SimpleDateTime
349 * conversion pattern.
350 * @param pattern conversion pattern, may not be null.
351 * @return Duration in milliseconds from an integral second
352 * that the cache will return consistent results.
353 */
354 public static int getMaximumCacheValidity(final String pattern) {
355
356
357
358
359
360 int firstS = pattern.indexOf('S');
361
362 if ((firstS >= 0) && (firstS != pattern.lastIndexOf("SSS"))) {
363 return 1;
364 }
365
366 return 1000;
367 }
368 }