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.logging.log4j.core.util.datetime;
19  
20  import java.util.Calendar;
21  import java.util.Objects;
22  import java.util.TimeZone;
23  
24  /**
25   * Custom time formatter that trades flexibility for performance. This formatter only supports the date patterns defined
26   * in {@link FixedFormat}. For any other date patterns use {@link FastDateFormat}.
27   * <p>
28   * Related benchmarks: /log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/TimeFormatBenchmark.java and
29   * /log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadsafeDateFormatBenchmark.java
30   */
31  public class FixedDateFormat {
32      /**
33       * Enumeration over the supported date/time format patterns.
34       * <p>
35       * Package protected for unit tests.
36       */
37      public enum FixedFormat {
38          /**
39           * ABSOLUTE time format: {@code "HH:mm:ss,SSS"}.
40           */
41          ABSOLUTE("HH:mm:ss,SSS", null, 0, ':', 1, ',', 1),
42  
43          /**
44           * ABSOLUTE time format variation with period separator: {@code "HH:mm:ss.SSS"}.
45           */
46          ABSOLUTE_PERIOD("HH:mm:ss.SSS", null, 0, ':', 1, '.', 1),
47  
48          /**
49           * COMPACT time format: {@code "yyyyMMddHHmmssSSS"}.
50           */
51          COMPACT("yyyyMMddHHmmssSSS", "yyyyMMdd", 0, ' ', 0, ' ', 0),
52  
53          /**
54           * DATE_AND_TIME time format: {@code "dd MMM yyyy HH:mm:ss,SSS"}.
55           */
56          DATE("dd MMM yyyy HH:mm:ss,SSS", "dd MMM yyyy ", 0, ':', 1, ',', 1),
57  
58          /**
59           * DATE_AND_TIME time format variation with period separator: {@code "dd MMM yyyy HH:mm:ss.SSS"}.
60           */
61          DATE_PERIOD("dd MMM yyyy HH:mm:ss.SSS", "dd MMM yyyy ", 0, ':', 1, '.', 1),
62  
63          /**
64           * DEFAULT time format: {@code "yyyy-MM-dd HH:mm:ss,SSS"}.
65           */
66          DEFAULT("yyyy-MM-dd HH:mm:ss,SSS", "yyyy-MM-dd ", 0, ':', 1, ',', 1),
67  
68          /**
69           * DEFAULT time format variation with period separator: {@code "yyyy-MM-dd HH:mm:ss.SSS"}.
70           */
71          DEFAULT_PERIOD("yyyy-MM-dd HH:mm:ss.SSS", "yyyy-MM-dd ", 0, ':', 1, '.', 1),
72  
73          /**
74           * ISO8601_BASIC time format: {@code "yyyyMMdd'T'HHmmss,SSS"}.
75           */
76          ISO8601_BASIC("yyyyMMdd'T'HHmmss,SSS", "yyyyMMdd'T'", 2, ' ', 0, ',', 1),
77  
78          /**
79           * ISO8601_BASIC time format: {@code "yyyyMMdd'T'HHmmss.SSS"}.
80           */
81          ISO8601_BASIC_PERIOD("yyyyMMdd'T'HHmmss.SSS", "yyyyMMdd'T'", 2, ' ', 0, '.', 1),
82  
83          /**
84           * ISO8601 time format: {@code "yyyy-MM-dd'T'HH:mm:ss,SSS"}.
85           */
86          ISO8601("yyyy-MM-dd'T'HH:mm:ss,SSS", "yyyy-MM-dd'T'", 2, ':', 1, ',', 1),
87  
88          /**
89           * ISO8601 time format: {@code "yyyy-MM-dd'T'HH:mm:ss.SSS"}.
90           */
91          ISO8601_PERIOD("yyyy-MM-dd'T'HH:mm:ss.SSS", "yyyy-MM-dd'T'", 2, ':', 1, '.', 1);
92  
93          private final String pattern;
94          private final String datePattern;
95          private final int escapeCount;
96          private final char timeSeparatorChar;
97          private final int timeSeparatorLength;
98          private final char millisSeparatorChar;
99          private final int millisSeparatorLength;
100 
101         FixedFormat(final String pattern, final String datePattern, final int escapeCount, final char timeSeparator,
102                     final int timeSepLength, final char millisSeparator, final int millisSepLength) {
103             this.timeSeparatorChar = timeSeparator;
104             this.timeSeparatorLength = timeSepLength;
105             this.millisSeparatorChar = millisSeparator;
106             this.millisSeparatorLength = millisSepLength;
107             this.pattern = Objects.requireNonNull(pattern);
108             this.datePattern = datePattern; // may be null
109             this.escapeCount = escapeCount;
110         }
111 
112         /**
113          * Returns the full pattern.
114          *
115          * @return the full pattern
116          */
117         public String getPattern() {
118             return pattern;
119         }
120 
121         /**
122          * Returns the date part of the pattern.
123          *
124          * @return the date part of the pattern
125          */
126         public String getDatePattern() {
127             return datePattern;
128         }
129 
130         /**
131          * Returns the FixedFormat with the name or pattern matching the specified string or {@code null} if not found.
132          *
133          * @param nameOrPattern the name or pattern to find a FixedFormat for
134          * @return the FixedFormat with the name or pattern matching the specified string
135          */
136         public static FixedFormat lookup(final String nameOrPattern) {
137             for (final FixedFormat type : FixedFormat.values()) {
138                 if (type.name().equals(nameOrPattern) || type.getPattern().equals(nameOrPattern)) {
139                     return type;
140                 }
141             }
142             return null;
143         }
144 
145         /**
146          * Returns the length of the resulting formatted date and time strings.
147          *
148          * @return the length of the resulting formatted date and time strings
149          */
150         public int getLength() {
151             return pattern.length() - escapeCount;
152         }
153 
154         /**
155          * Returns the length of the date part of the resulting formatted string.
156          *
157          * @return the length of the date part of the resulting formatted string
158          */
159         public int getDatePatternLength() {
160             return getDatePattern() == null ? 0 : getDatePattern().length() - escapeCount;
161         }
162 
163         /**
164          * Returns the {@code FastDateFormat} object for formatting the date part of the pattern or {@code null} if the
165          * pattern does not have a date part.
166          *
167          * @return the {@code FastDateFormat} object for formatting the date part of the pattern or {@code null}
168          */
169         public FastDateFormat getFastDateFormat() {
170             return getFastDateFormat(null);
171         }
172 
173         /**
174          * Returns the {@code FastDateFormat} object for formatting the date part of the pattern or {@code null} if the
175          * pattern does not have a date part.
176          *
177          * @param tz the time zone to use
178          * @return the {@code FastDateFormat} object for formatting the date part of the pattern or {@code null}
179          */
180         public FastDateFormat getFastDateFormat(final TimeZone tz) {
181             return getDatePattern() == null ? null : FastDateFormat.getInstance(getDatePattern(), tz);
182         }
183     }
184 
185     private final FixedFormat fixedFormat;
186     private final TimeZone timeZone;
187     private final int length;
188     private final FastDateFormat fastDateFormat; // may be null
189     private final char timeSeparatorChar;
190     private final char millisSeparatorChar;
191     private final int timeSeparatorLength;
192     private final int millisSeparatorLength;
193 
194     private volatile long midnightToday = 0;
195     private volatile long midnightTomorrow = 0;
196     // cachedDate does not need to be volatile because
197     // there is a write to a volatile field *after* cachedDate is modified,
198     // and there is a read from a volatile field *before* cachedDate is read.
199     // The Java memory model guarantees that because of the above,
200     // changes to cachedDate in one thread are visible to other threads.
201     // See http://g.oswego.edu/dl/jmm/cookbook.html
202     private char[] cachedDate; // may be null
203     private int dateLength;
204 
205     /**
206      * Constructs a FixedDateFormat for the specified fixed format.
207      * <p>
208      * Package protected for unit tests.
209      *
210      * @param fixedFormat the fixed format
211      */
212     FixedDateFormat(final FixedFormat fixedFormat, final TimeZone tz) {
213         this.fixedFormat = Objects.requireNonNull(fixedFormat);
214         this.timeZone = Objects.requireNonNull(tz);
215         this.timeSeparatorChar = fixedFormat.timeSeparatorChar;
216         this.timeSeparatorLength = fixedFormat.timeSeparatorLength;
217         this.millisSeparatorChar = fixedFormat.millisSeparatorChar;
218         this.millisSeparatorLength = fixedFormat.millisSeparatorLength;
219         this.length = fixedFormat.getLength();
220         this.fastDateFormat = fixedFormat.getFastDateFormat(tz);
221     }
222 
223     public static FixedDateFormat createIfSupported(final String... options) {
224         if (options == null || options.length == 0 || options[0] == null) {
225             return new FixedDateFormat(FixedFormat.DEFAULT, TimeZone.getDefault());
226         }
227         final TimeZone tz;
228         if (options.length > 1) {
229             if (options[1] != null){
230                 tz = TimeZone.getTimeZone(options[1]);
231             } else {
232                 tz = TimeZone.getDefault();
233             }
234         } else if (options.length > 2) {
235             return null;
236         } else {
237             tz = TimeZone.getDefault();
238         }
239 
240         final FixedFormat type = FixedFormat.lookup(options[0]);
241         return type == null ? null : new FixedDateFormat(type, tz);
242     }
243 
244     /**
245      * Returns a new {@code FixedDateFormat} object for the specified {@code FixedFormat} and a {@code TimeZone.getDefault()} TimeZone.
246      *
247      * @param format the format to use
248      * @return a new {@code FixedDateFormat} object
249      */
250     public static FixedDateFormat create(final FixedFormat format) {
251         return new FixedDateFormat(format, TimeZone.getDefault());
252     }
253 
254     /**
255      * Returns a new {@code FixedDateFormat} object for the specified {@code FixedFormat} and TimeZone.
256      *
257      * @param format the format to use
258      * @param tz the time zone to use
259      * @return a new {@code FixedDateFormat} object
260      */
261     public static FixedDateFormat create(final FixedFormat format, final TimeZone tz) {
262         return new FixedDateFormat(format, tz != null ? tz : TimeZone.getDefault());
263     }
264 
265     /**
266      * Returns the full pattern of the selected fixed format.
267      *
268      * @return the full date-time pattern
269      */
270     public String getFormat() {
271         return fixedFormat.getPattern();
272     }
273 
274     /**
275      * Returns the time zone.
276      *
277      * @return the time zone
278      */
279 
280     public TimeZone getTimeZone() {
281         return timeZone;
282     }
283 
284     // Profiling showed this method is important to log4j performance. Modify with care!
285     // 30 bytes (allows immediate JVM inlining: <= -XX:MaxInlineSize=35 bytes)
286     private long millisSinceMidnight(final long now) {
287         if (now >= midnightTomorrow || now < midnightToday) {
288             updateMidnightMillis(now);
289         }
290         return now - midnightToday;
291     }
292 
293     private void updateMidnightMillis(final long now) {
294 
295         updateCachedDate(now);
296 
297         midnightToday = calcMidnightMillis(now, 0);
298         midnightTomorrow = calcMidnightMillis(now, 1);
299     }
300 
301     private long calcMidnightMillis(final long time, final int addDays) {
302         final Calendar cal = Calendar.getInstance(timeZone);
303         cal.setTimeInMillis(time);
304         cal.set(Calendar.HOUR_OF_DAY, 0);
305         cal.set(Calendar.MINUTE, 0);
306         cal.set(Calendar.SECOND, 0);
307         cal.set(Calendar.MILLISECOND, 0);
308         cal.add(Calendar.DATE, addDays);
309         return cal.getTimeInMillis();
310     }
311 
312     private void updateCachedDate(final long now) {
313         if (fastDateFormat != null) {
314             final StringBuilder result = fastDateFormat.format(now, new StringBuilder());
315             cachedDate = result.toString().toCharArray();
316             dateLength = result.length();
317         }
318     }
319 
320     // Profiling showed this method is important to log4j performance. Modify with care!
321     // 28 bytes (allows immediate JVM inlining: <= -XX:MaxInlineSize=35 bytes)
322     public String format(final long time) {
323         final char[] result = new char[length << 1]; // double size for locales with lengthy DateFormatSymbols
324         final int written = format(time, result, 0);
325         return new String(result, 0, written);
326     }
327 
328     // Profiling showed this method is important to log4j performance. Modify with care!
329     // 31 bytes (allows immediate JVM inlining: <= -XX:MaxInlineSize=35 bytes)
330     public int format(final long time, final char[] buffer, final int startPos) {
331         // Calculate values by getting the ms values first and do then
332         // calculate the hour minute and second values divisions.
333 
334         // Get daytime in ms: this does fit into an int
335         // int ms = (int) (time % 86400000);
336         final int ms = (int) (millisSinceMidnight(time));
337         writeDate(buffer, startPos);
338         return writeTime(ms, buffer, startPos + dateLength) - startPos;
339     }
340 
341     // Profiling showed this method is important to log4j performance. Modify with care!
342     // 22 bytes (allows immediate JVM inlining: <= -XX:MaxInlineSize=35 bytes)
343     private void writeDate(final char[] buffer, final int startPos) {
344         if (cachedDate != null) {
345             System.arraycopy(cachedDate, 0, buffer, startPos, dateLength);
346         }
347     }
348 
349     // Profiling showed this method is important to log4j performance. Modify with care!
350     // 262 bytes (will be inlined when hot enough: <= -XX:FreqInlineSize=325 bytes on Linux)
351     private int writeTime(int ms, final char[] buffer, int pos) {
352         final int hours = ms / 3600000;
353         ms -= 3600000 * hours;
354 
355         final int minutes = ms / 60000;
356         ms -= 60000 * minutes;
357 
358         final int seconds = ms / 1000;
359         ms -= 1000 * seconds;
360 
361         // Hour
362         int temp = hours / 10;
363         buffer[pos++] = ((char) (temp + '0'));
364 
365         // Do subtract to get remainder instead of doing % 10
366         buffer[pos++] = ((char) (hours - 10 * temp + '0'));
367         buffer[pos] = timeSeparatorChar;
368         pos += timeSeparatorLength;
369 
370         // Minute
371         temp = minutes / 10;
372         buffer[pos++] = ((char) (temp + '0'));
373 
374         // Do subtract to get remainder instead of doing % 10
375         buffer[pos++] = ((char) (minutes - 10 * temp + '0'));
376         buffer[pos] = timeSeparatorChar;
377         pos += timeSeparatorLength;
378 
379         // Second
380         temp = seconds / 10;
381         buffer[pos++] = ((char) (temp + '0'));
382         buffer[pos++] = ((char) (seconds - 10 * temp + '0'));
383         buffer[pos] = millisSeparatorChar;
384         pos += millisSeparatorLength;
385 
386         // Millisecond
387         temp = ms / 100;
388         buffer[pos++] = ((char) (temp + '0'));
389 
390         ms -= 100 * temp;
391         temp = ms / 10;
392         buffer[pos++] = ((char) (temp + '0'));
393 
394         ms -= 10 * temp;
395         buffer[pos++] = ((char) (ms + '0'));
396         return pos;
397     }
398 }