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 java.text.DateFormat;
20 import java.text.FieldPosition;
21 import java.text.NumberFormat;
22 import java.text.ParsePosition;
23 import java.util.Date;
24 import java.util.TimeZone;
25
26 import org.apache.logging.log4j.core.util.Constants;
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 }
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 }
220 return UNRECOGNIZED_MILLISECONDS;
221 }
222 }
223
224 return NO_MILLISECONDS;
225 }
226
227 /**
228 * Formats a Date into a date/time string.
229 *
230 * @param date the date to format.
231 * @param sbuf the string buffer to write to.
232 * @param fieldPosition remains untouched.
233 * @return the formatted time string.
234 */
235 @Override
236 public StringBuffer format(final Date date, final StringBuffer sbuf, final FieldPosition fieldPosition) {
237 format(date.getTime(), sbuf);
238
239 return sbuf;
240 }
241
242 /**
243 * Formats a millisecond count into a date/time string.
244 *
245 * @param now Number of milliseconds after midnight 1 Jan 1970 GMT.
246 * @param buf the string buffer to write to.
247 * @return the formatted time string.
248 */
249 public StringBuffer format(final long now, final StringBuffer buf) {
250 //
251 // If the current requested time is identical to the previously
252 // requested time, then append the cache contents.
253 //
254 if (now == previousTime) {
255 buf.append(cache);
256
257 return buf;
258 }
259
260 //
261 // If millisecond pattern was not unrecognized
262 // (that is if it was found or milliseconds did not appear)
263 //
264 if (millisecondStart != UNRECOGNIZED_MILLISECONDS &&
265 // Check if the cache is still valid.
266 // If the requested time is within the same integral second
267 // as the last request and a shorter expiration was not requested.
268 (now < (slotBegin + expiration)) && (now >= slotBegin) && (now < (slotBegin + SLOTS))) {
269 //
270 // if there was a millisecond field then update it
271 //
272 if (millisecondStart >= 0) {
273 millisecondFormat((int) (now - slotBegin), cache, millisecondStart);
274 }
275
276 //
277 // update the previously requested time
278 // (the slot begin should be unchanged)
279 previousTime = now;
280 buf.append(cache);
281
282 return buf;
283 }
284
285 //
286 // could not use previous value.
287 // Call underlying formatter to format date.
288 cache.setLength(0);
289 tmpDate.setTime(now);
290 cache.append(formatter.format(tmpDate));
291 buf.append(cache);
292 previousTime = now;
293 slotBegin = (previousTime / Constants.MILLIS_IN_SECONDS) * Constants.MILLIS_IN_SECONDS;
294
295 if (slotBegin > previousTime) {
296 slotBegin -= Constants.MILLIS_IN_SECONDS;
297 }
298
299 //
300 // if the milliseconds field was previous found
301 // then reevaluate in case it moved.
302 //
303 if (millisecondStart >= 0) {
304 millisecondStart =
305 findMillisecondStart(now, cache.toString(), formatter);
306 }
307
308 return buf;
309 }
310
311 /**
312 * Formats a count of milliseconds (0-999) into a numeric representation.
313 *
314 * @param millis Millisecond count between 0 and 999.
315 * @param buf String buffer, may not be null.
316 * @param offset Starting position in buffer, the length of the
317 * buffer must be at least offset + 3.
318 */
319 private static void millisecondFormat(
320 final int millis, final StringBuffer buf, final int offset) {
321 buf.setCharAt(offset, DIGITS.charAt(millis / THREE_DIGITS));
322 buf.setCharAt(offset + 1, DIGITS.charAt((millis / TWO_DIGITS) % TWO_DIGITS));
323 buf.setCharAt(offset + 2, DIGITS.charAt(millis % TWO_DIGITS));
324 }
325
326 /**
327 * Sets the time zone.
328 * <p>
329 * Setting the time zone using getCalendar().setTimeZone() will likely cause caching to misbehave.
330 * </p>
331 *
332 * @param timeZone
333 * TimeZone new time zone
334 */
335 @Override
336 public void setTimeZone(final TimeZone timeZone) {
337 formatter.setTimeZone(timeZone);
338 previousTime = Long.MIN_VALUE;
339 slotBegin = Long.MIN_VALUE;
340 }
341
342 /**
343 * This method is delegated to the formatter which most
344 * likely returns null.
345 *
346 * @param s string representation of date.
347 * @param pos field position, unused.
348 * @return parsed date, likely null.
349 */
350 @Override
351 public Date parse(final String s, final ParsePosition pos) {
352 return formatter.parse(s, pos);
353 }
354
355 /**
356 * Gets number formatter.
357 *
358 * @return NumberFormat number formatter
359 */
360 @Override
361 public NumberFormat getNumberFormat() {
362 return formatter.getNumberFormat();
363 }
364
365 /**
366 * Gets maximum cache validity for the specified SimpleDateTime
367 * conversion pattern.
368 *
369 * @param pattern conversion pattern, may not be null.
370 * @return Duration in milliseconds from an integral second
371 * that the cache will return consistent results.
372 */
373 public static int getMaximumCacheValidity(final String pattern) {
374 //
375 // If there are more "S" in the pattern than just one "SSS" then
376 // (for example, "HH:mm:ss,SSS SSS"), then set the expiration to
377 // one millisecond which should only perform duplicate request caching.
378 //
379 final int firstS = pattern.indexOf('S');
380
381 if ((firstS >= 0) && (firstS != pattern.lastIndexOf("SSS"))) {
382 return 1;
383 }
384
385 return DEFAULT_VALIDITY;
386 }
387 }