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