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  import org.apache.logging.log4j.core.util.Constants;
    27
    28
    29  /**
    30   * CachedDateFormat optimizes the performance of a wrapped
    31   * DateFormat.  The implementation is not thread-safe.
    32   * If the millisecond pattern is not recognized,
    33   * the class will only use the cache if the
    34   * same value is requested.
    35   */
    36  final class CachedDateFormat extends DateFormat {
    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      private static final long serialVersionUID = -1253877934598423628L;
    51
    52      /**
    53       * Supported digit set.  If the wrapped DateFormat uses
    54       * a different unit set, the millisecond pattern
    55       * will not be recognized and duplicate requests
    56       * will use the cache.
    57       */
    58      private static final String DIGITS = "0123456789";
    59
    60      /**
    61       * First magic number used to detect the millisecond position.
    62       */
    63      private static final int MAGIC1 = 654;
    64
    65      /**
    66       * Expected representation of first magic number.
    67       */
    68      private static final String MAGICSTRING1 = "654";
    69
    70      /**
    71       * Second magic number used to detect the millisecond position.
    72       */
    73      private static final int MAGIC2 = 987;
    74
    75      /**
    76       * Expected representation of second magic number.
    77       */
    78      private static final String MAGICSTRING2 = "987";
    79
    80      /**
    81       * Expected representation of 0 milliseconds.
    82       */
    83      private static final String ZERO_STRING = "000";
    84
    85      private static final int BUF_SIZE = 50;
    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 converted Date.
    108      */
    109     private long slotBegin;
    110
    111     /**
    112      * Cache of previous conversion.
    113      */
    114     private final 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 / Constants.MILLIS_IN_SECONDS) * Constants.MILLIS_IN_SECONDS;
    174
    175         if (slotBegin > time) {
    176             slotBegin -= Constants.MILLIS_IN_SECONDS;
    177         }
    178
    179         final 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         final 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         }
    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                 final StringBuffer formattedMillis = new StringBuffer("ABC");
    204                 millisecondFormat(millis, formattedMillis, 0);
    205
    206                 final 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                 }
    220                 return UNRECOGNIZED_MILLISECONDS;
    221             }
    222         }
    223
    224         return NO_MILLISECONDS;
    225     }
    226
    227     /**
    228      * Formats a Date into a date/time string.
    229      *
    230      * @param date          the date to format.
    231      * @param sbuf          the string buffer to write to.
    232      * @param fieldPosition remains untouched.
    233      * @return the formatted time string.
    234      */
    235     @Override
    236     public StringBuffer format(final Date date, final StringBuffer sbuf, final FieldPosition fieldPosition) {
    237         format(date.getTime(), sbuf);
    238
    239         return sbuf;
    240     }
    241
    242     /**
    243      * Formats a millisecond count into a date/time string.
    244      *
    245      * @param now Number of milliseconds after midnight 1 Jan 1970 GMT.
    246      * @param buf the string buffer to write to.
    247      * @return the formatted time string.
    248      */
    249     public StringBuffer format(final long now, final StringBuffer buf) {
    250         //
    251         // If the current requested time is identical to the previously
    252         //     requested time, then append the cache contents.
    253         //
    254         if (now == previousTime) {
    255             buf.append(cache);
    256
    257             return buf;
    258         }
    259
    260         //
    261         //   If millisecond pattern was not unrecognized
    262         //     (that is if it was found or milliseconds did not appear)
    263         //
    264         if (millisecondStart != UNRECOGNIZED_MILLISECONDS &&
    265             //    Check if the cache is still valid.
    266             //    If the requested time is within the same integral second
    267             //       as the last request and a shorter expiration was not requested.
    268             (now < (slotBegin + expiration)) && (now >= slotBegin) && (now < (slotBegin + SLOTS))) {
    269             //
    270             //    if there was a millisecond field then update it
    271             //
    272             if (millisecondStart >= 0) {
    273                 millisecondFormat((int) (now - slotBegin), cache, millisecondStart);
    274             }
    275
    276             //
    277             //   update the previously requested time
    278             //      (the slot begin should be unchanged)
    279             previousTime = now;
    280             buf.append(cache);
    281
    282             return buf;
    283         }
    284
    285         //
    286         //  could not use previous value.
    287         //    Call underlying formatter to format date.
    288         cache.setLength(0);
    289         tmpDate.setTime(now);
    290         cache.append(formatter.format(tmpDate));
    291         buf.append(cache);
    292         previousTime = now;
    293         slotBegin = (previousTime / Constants.MILLIS_IN_SECONDS) * Constants.MILLIS_IN_SECONDS;
    294
    295         if (slotBegin > previousTime) {
    296             slotBegin -= Constants.MILLIS_IN_SECONDS;
    297         }
    298
    299         //
    300         //    if the milliseconds field was previous found
    301         //       then reevaluate in case it moved.
    302         //
    303         if (millisecondStart >= 0) {
    304             millisecondStart =
    305                 findMillisecondStart(now, cache.toString(), formatter);
    306         }
    307
    308         return buf;
    309     }
    310
    311     /**
    312      * Formats a count of milliseconds (0-999) into a numeric representation.
    313      *
    314      * @param millis Millisecond count between 0 and 999.
    315      * @param buf    String buffer, may not be null.
    316      * @param offset Starting position in buffer, the length of the
    317      *               buffer must be at least offset + 3.
    318      */
    319     private static void millisecondFormat(
    320         final int millis, final StringBuffer buf, final int offset) {
    321         buf.setCharAt(offset, DIGITS.charAt(millis / THREE_DIGITS));
    322         buf.setCharAt(offset + 1, DIGITS.charAt((millis / TWO_DIGITS) % TWO_DIGITS));
    323         buf.setCharAt(offset + 2, DIGITS.charAt(millis % TWO_DIGITS));
    324     }
    325
    326     /**
    327      * Sets the time zone.
    328      * <p>
    329      * Setting the time zone using getCalendar().setTimeZone() will likely cause caching to misbehave.
    330      * </p>
    331      *
    332      * @param timeZone
    333      *        TimeZone new time zone
    334      */
    335     @Override
    336     public void setTimeZone(final TimeZone timeZone) {
    337         formatter.setTimeZone(timeZone);
    338         previousTime = Long.MIN_VALUE;
    339         slotBegin = Long.MIN_VALUE;
    340     }
    341
    342     /**
    343      * This method is delegated to the formatter which most
    344      * likely returns null.
    345      *
    346      * @param s   string representation of date.
    347      * @param pos field position, unused.
    348      * @return parsed date, likely null.
    349      */
    350     @Override
    351     public Date parse(final String s, final ParsePosition pos) {
    352         return formatter.parse(s, pos);
    353     }
    354
    355     /**
    356      * Gets number formatter.
    357      *
    358      * @return NumberFormat number formatter
    359      */
    360     @Override
    361     public NumberFormat getNumberFormat() {
    362         return formatter.getNumberFormat();
    363     }
    364
    365     /**
    366      * Gets maximum cache validity for the specified SimpleDateTime
    367      * conversion pattern.
    368      *
    369      * @param pattern conversion pattern, may not be null.
    370      * @return Duration in milliseconds from an integral second
    371      *         that the cache will return consistent results.
    372      */
    373     public static int getMaximumCacheValidity(final String pattern) {
    374         //
    375         //   If there are more "S" in the pattern than just one "SSS" then
    376         //      (for example, "HH:mm:ss,SSS SSS"), then set the expiration to
    377         //      one millisecond which should only perform duplicate request caching.
    378         //
    379         final int firstS = pattern.indexOf('S');
    380
    381         if ((firstS >= 0) && (firstS != pattern.lastIndexOf("SSS"))) {
    382             return 1;
    383         }
    384
    385         return DEFAULT_VALIDITY;
    386     }
    387 }