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