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      private static final long serialVersionUID = -1253877934598423628L;
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      /**
51       * Supported digit set.  If the wrapped DateFormat uses
52       * a different unit set, the millisecond pattern
53       * will not be recognized and duplicate requests
54       * will use the cache.
55       */
56      private static final String DIGITS = "0123456789";
57  
58      /**
59       * First magic number used to detect the millisecond position.
60       */
61      private static final int MAGIC1 = 654;
62  
63      /**
64       * Expected representation of first magic number.
65       */
66      private static final String MAGICSTRING1 = "654";
67  
68      /**
69       * Second magic number used to detect the millisecond position.
70       */
71      private static final int MAGIC2 = 987;
72  
73      /**
74       * Expected representation of second magic number.
75       */
76      private static final String MAGICSTRING2 = "987";
77  
78      /**
79       * Expected representation of 0 milliseconds.
80       */
81      private static final String ZERO_STRING = "000";
82  
83      private static final int BUF_SIZE = 50;
84  
85      private static final int MILLIS_IN_SECONDS = 1000;
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 convered Date.
108      */
109     private long slotBegin;
110 
111     /**
112      * Cache of previous conversion.
113      */
114     private 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 / MILLIS_IN_SECONDS) * MILLIS_IN_SECONDS;
174 
175         if (slotBegin > time) {
176             slotBegin -= MILLIS_IN_SECONDS;
177         }
178 
179         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         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                     StringBuffer formattedMillis = new StringBuffer("ABC");
204                     millisecondFormat(millis, formattedMillis, 0);
205 
206                     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(Date date, StringBuffer sbuf, 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(long now, 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 / MILLIS_IN_SECONDS) * MILLIS_IN_SECONDS;
296 
297         if (slotBegin > previousTime) {
298             slotBegin -= 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 coun 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(String s, 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         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 }