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