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