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