001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.log4j.pattern;
019
020import java.text.DateFormat;
021import java.text.FieldPosition;
022import java.text.NumberFormat;
023import java.text.ParsePosition;
024import java.util.Date;
025import java.util.TimeZone;
026
027
028/**
029 * CachedDateFormat optimizes the performance of a wrapped
030 * DateFormat.  The implementation is not thread-safe.
031 * If the millisecond pattern is not recognized,
032 * the class will only use the cache if the
033 * same value is requested.
034 *
035 */
036public final class CachedDateFormat extends DateFormat {
037  /**
038   *  Serialization version.
039  */
040  private static final long serialVersionUID = 1;
041  /**
042   *  Constant used to represent that there was no change
043   *  observed when changing the millisecond count.
044   */
045  public static final int NO_MILLISECONDS = -2;
046
047  /**
048   *  Supported digit set.  If the wrapped DateFormat uses
049   *  a different unit set, the millisecond pattern
050   *  will not be recognized and duplicate requests
051   *  will use the cache.
052   */
053  private static final String DIGITS = "0123456789";
054
055  /**
056   *  Constant used to represent that there was an
057   *  observed change, but was an expected change.
058   */
059  public static final int UNRECOGNIZED_MILLISECONDS = -1;
060
061  /**
062   *  First magic number used to detect the millisecond position.
063   */
064  private static final int MAGIC1 = 654;
065
066  /**
067   *  Expected representation of first magic number.
068   */
069  private static final String MAGICSTRING1 = "654";
070
071  /**
072   *  Second magic number used to detect the millisecond position.
073   */
074  private static final int MAGIC2 = 987;
075
076  /**
077   *  Expected representation of second magic number.
078   */
079  private static final String MAGICSTRING2 = "987";
080
081  /**
082   *  Expected representation of 0 milliseconds.
083   */
084  private static final String ZERO_STRING = "000";
085
086  /**
087   *   Wrapped formatter.
088   */
089  private final DateFormat formatter;
090
091  /**
092   *  Index of initial digit of millisecond pattern or
093   *   UNRECOGNIZED_MILLISECONDS or NO_MILLISECONDS.
094   */
095  private int millisecondStart;
096
097  /**
098   *  Integral second preceding the previous convered Date.
099   */
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}