View Javadoc
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 }