+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.log4j.pattern;
+
+import java.text.DateFormat;
+import java.text.FieldPosition;
+import java.text.NumberFormat;
+import java.text.ParsePosition;
+import java.util.Date;
+import java.util.TimeZone;
+
+
+/**
+ * CachedDateFormat optimizes the performance of a wrapped
+ * DateFormat. The implementation is not thread-safe.
+ * If the millisecond pattern is not recognized,
+ * the class will only use the cache if the
+ * same value is requested.
+ *
+ */
+public final class CachedDateFormat extends DateFormat {
+ /**
+ * Serialization version.
+ */
+ private static final long serialVersionUID = 1;
+ /**
+ * Constant used to represent that there was no change
+ * observed when changing the millisecond count.
+ */
+ public static final int NO_MILLISECONDS = -2;
+
+ /**
+ * Supported digit set. If the wrapped DateFormat uses
+ * a different unit set, the millisecond pattern
+ * will not be recognized and duplicate requests
+ * will use the cache.
+ */
+ private static final String DIGITS = "0123456789";
+
+ /**
+ * Constant used to represent that there was an
+ * observed change, but was an expected change.
+ */
+ public static final int UNRECOGNIZED_MILLISECONDS = -1;
+
+ /**
+ * First magic number used to detect the millisecond position.
+ */
+ private static final int MAGIC1 = 654;
+
+ /**
+ * Expected representation of first magic number.
+ */
+ private static final String MAGICSTRING1 = "654";
+
+ /**
+ * Second magic number used to detect the millisecond position.
+ */
+ private static final int MAGIC2 = 987;
+
+ /**
+ * Expected representation of second magic number.
+ */
+ private static final String MAGICSTRING2 = "987";
+
+ /**
+ * Expected representation of 0 milliseconds.
+ */
+ private static final String ZERO_STRING = "000";
+
+ /**
+ * Wrapped formatter.
+ */
+ private final DateFormat formatter;
+
+ /**
+ * Index of initial digit of millisecond pattern or
+ * UNRECOGNIZED_MILLISECONDS or NO_MILLISECONDS.
+ */
+ private int millisecondStart;
+
+ /**
+ * Integral second preceding the previous convered Date.
+ */
+ private long slotBegin;
+
+ /**
+ * Cache of previous conversion.
+ */
+ private StringBuffer cache = new StringBuffer(50);
+
+ /**
+ * Maximum validity period for the cache.
+ * Typically 1, use cache for duplicate requests only, or
+ * 1000, use cache for requests within the same integral second.
+ */
+ private final int expiration;
+
+ /**
+ * Date requested in previous conversion.
+ */
+ private long previousTime;
+
+ /**
+ * Scratch date object used to minimize date object creation.
+ */
+ private final Date tmpDate = new Date(0);
+
+ /**
+ * Creates a new CachedDateFormat object.
+ * @param dateFormat Date format, may not be null.
+ * @param expiration maximum cached range in milliseconds.
+ * If the dateFormat is known to be incompatible with the
+ * caching algorithm, use a value of 0 to totally disable
+ * caching or 1 to only use cache for duplicate requests.
+ */
+ public CachedDateFormat(final DateFormat dateFormat, final int expiration) {
+ if (dateFormat == null) {
+ throw new IllegalArgumentException("dateFormat cannot be null");
+ }
+
+ if (expiration < 0) {
+ throw new IllegalArgumentException("expiration must be non-negative");
+ }
+
+ formatter = dateFormat;
+ this.expiration = expiration;
+ millisecondStart = 0;
+
+ //
+ // set the previousTime so the cache will be invalid
+ // for the next request.
+ previousTime = Long.MIN_VALUE;
+ slotBegin = Long.MIN_VALUE;
+ }
+
+ /**
+ * Finds start of millisecond field in formatted time.
+ * @param time long time, must be integral number of seconds
+ * @param formatted String corresponding formatted string
+ * @param formatter DateFormat date format
+ * @return int position in string of first digit of milliseconds,
+ * -1 indicates no millisecond field, -2 indicates unrecognized
+ * field (likely RelativeTimeDateFormat)
+ */
+ public static int findMillisecondStart(
+ final long time, final String formatted, final DateFormat formatter) {
+ long slotBegin = (time / 1000) * 1000;
+
+ if (slotBegin > time) {
+ slotBegin -= 1000;
+ }
+
+ int millis = (int) (time - slotBegin);
+
+ int magic = MAGIC1;
+ String magicString = MAGICSTRING1;
+
+ if (millis == MAGIC1) {
+ magic = MAGIC2;
+ magicString = MAGICSTRING2;
+ }
+
+ String plusMagic = formatter.format(new Date(slotBegin + magic));
+
+ /**
+ * If the string lengths differ then
+ * we can't use the cache except for duplicate requests.
+ */
+ if (plusMagic.length() != formatted.length()) {
+ return UNRECOGNIZED_MILLISECONDS;
+ } else {
+ // find first difference between values
+ for (int i = 0; i < formatted.length(); i++) {
+ if (formatted.charAt(i) != plusMagic.charAt(i)) {
+ //
+ // determine the expected digits for the base time
+ StringBuffer formattedMillis = new StringBuffer("ABC");
+ millisecondFormat(millis, formattedMillis, 0);
+
+ String plusZero = formatter.format(new Date(slotBegin));
+
+ // If the next 3 characters match the magic
+ // string and the expected string
+ if (
+ (plusZero.length() == formatted.length())
+ && magicString.regionMatches(
+ 0, plusMagic, i, magicString.length())
+ && formattedMillis.toString().regionMatches(
+ 0, formatted, i, magicString.length())
+ && ZERO_STRING.regionMatches(
+ 0, plusZero, i, ZERO_STRING.length())) {
+ return i;
+ } else {
+ return UNRECOGNIZED_MILLISECONDS;
+ }
+ }
+ }
+ }
+
+ return NO_MILLISECONDS;
+ }
+
+ /**
+ * Formats a Date into a date/time string.
+ *
+ * @param date the date to format.
+ * @param sbuf the string buffer to write to.
+ * @param fieldPosition remains untouched.
+ * @return the formatted time string.
+ */
+ public StringBuffer format(
+ Date date, StringBuffer sbuf, FieldPosition fieldPosition) {
+ format(date.getTime(), sbuf);
+
+ return sbuf;
+ }
+
+ /**
+ * Formats a millisecond count into a date/time string.
+ *
+ * @param now Number of milliseconds after midnight 1 Jan 1970 GMT.
+ * @param buf the string buffer to write to.
+ * @return the formatted time string.
+ */
+ public StringBuffer format(long now, StringBuffer buf) {
+ //
+ // If the current requested time is identical to the previously
+ // requested time, then append the cache contents.
+ //
+ if (now == previousTime) {
+ buf.append(cache);
+
+ return buf;
+ }
+
+ //
+ // If millisecond pattern was not unrecognized
+ // (that is if it was found or milliseconds did not appear)
+ //
+ if (millisecondStart != UNRECOGNIZED_MILLISECONDS &&
+ // Check if the cache is still valid.
+ // If the requested time is within the same integral second
+ // as the last request and a shorter expiration was not requested.
+ (now < (slotBegin + expiration)) && (now >= slotBegin)
+ && (now < (slotBegin + 1000L))) {
+ //
+ // if there was a millisecond field then update it
+ //
+ if (millisecondStart >= 0) {
+ millisecondFormat((int) (now - slotBegin), cache, millisecondStart);
+ }
+
+ //
+ // update the previously requested time
+ // (the slot begin should be unchanged)
+ previousTime = now;
+ buf.append(cache);
+
+ return buf;
+ }
+
+ //
+ // could not use previous value.
+ // Call underlying formatter to format date.
+ cache.setLength(0);
+ tmpDate.setTime(now);
+ cache.append(formatter.format(tmpDate));
+ buf.append(cache);
+ previousTime = now;
+ slotBegin = (previousTime / 1000) * 1000;
+
+ if (slotBegin > previousTime) {
+ slotBegin -= 1000;
+ }
+
+ //
+ // if the milliseconds field was previous found
+ // then reevaluate in case it moved.
+ //
+ if (millisecondStart >= 0) {
+ millisecondStart =
+ findMillisecondStart(now, cache.toString(), formatter);
+ }
+
+ return buf;
+ }
+
+ /**
+ * Formats a count of milliseconds (0-999) into a numeric representation.
+ * @param millis Millisecond coun between 0 and 999.
+ * @param buf String buffer, may not be null.
+ * @param offset Starting position in buffer, the length of the
+ * buffer must be at least offset + 3.
+ */
+ private static void millisecondFormat(
+ final int millis, final StringBuffer buf, final int offset) {
+ buf.setCharAt(offset, DIGITS.charAt(millis / 100));
+ buf.setCharAt(offset + 1, DIGITS.charAt((millis / 10) % 10));
+ buf.setCharAt(offset + 2, DIGITS.charAt(millis % 10));
+ }
+
+ /**
+ * Set timezone.
+ *
+ * Setting the timezone using getCalendar().setTimeZone()
+ * will likely cause caching to misbehave.
+ * @param timeZone TimeZone new timezone
+ */
+ public void setTimeZone(final TimeZone timeZone) {
+ formatter.setTimeZone(timeZone);
+ previousTime = Long.MIN_VALUE;
+ slotBegin = Long.MIN_VALUE;
+ }
+
+ /**
+ * This method is delegated to the formatter which most
+ * likely returns null.
+ * @param s string representation of date.
+ * @param pos field position, unused.
+ * @return parsed date, likely null.
+ */
+ public Date parse(String s, ParsePosition pos) {
+ return formatter.parse(s, pos);
+ }
+
+ /**
+ * Gets number formatter.
+ *
+ * @return NumberFormat number formatter
+ */
+ public NumberFormat getNumberFormat() {
+ return formatter.getNumberFormat();
+ }
+
+ /**
+ * Gets maximum cache validity for the specified SimpleDateTime
+ * conversion pattern.
+ * @param pattern conversion pattern, may not be null.
+ * @return Duration in milliseconds from an integral second
+ * that the cache will return consistent results.
+ */
+ public static int getMaximumCacheValidity(final String pattern) {
+ //
+ // If there are more "S" in the pattern than just one "SSS" then
+ // (for example, "HH:mm:ss,SSS SSS"), then set the expiration to
+ // one millisecond which should only perform duplicate request caching.
+ //
+ int firstS = pattern.indexOf('S');
+
+ if ((firstS >= 0) && (firstS != pattern.lastIndexOf("SSS"))) {
+ return 1;
+ }
+
+ return 1000;
+ }
+}