CachedDateFormat xref

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 }