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.appender.rolling.action;
19  
20  import java.io.Serializable;
21  import java.util.Objects;
22  import java.util.regex.Matcher;
23  import java.util.regex.Pattern;
24  
25  /**
26   * Simplified implementation of the <a href="https://en.wikipedia.org/wiki/ISO_8601#Durations">ISO-8601 Durations</a>
27   * standard. The supported format is {@code PnDTnHnMnS}, with 'P' and 'T' optional. Days are considered to be exactly 24
28   * hours.
29   * <p>
30   * Similarly to the {@code java.time.Duration} class, this class does not support year or month sections in the format.
31   * This implementation does not support fractions or negative values.
32   *
33   * @see #parse(CharSequence)
34   */
35  public class Duration implements Serializable, Comparable<Duration> {
36      private static final long serialVersionUID = -3756810052716342061L;
37  
38      /**
39       * Constant for a duration of zero.
40       */
41      public static final Duration ZERO = new Duration(0);
42  
43      /**
44       * Hours per day.
45       */
46      private static final int HOURS_PER_DAY = 24;
47      /**
48       * Minutes per hour.
49       */
50      private static final int MINUTES_PER_HOUR = 60;
51      /**
52       * Seconds per minute.
53       */
54      private static final int SECONDS_PER_MINUTE = 60;
55      /**
56       * Seconds per hour.
57       */
58      private static final int SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
59      /**
60       * Seconds per day.
61       */
62      private static final int SECONDS_PER_DAY = SECONDS_PER_HOUR * HOURS_PER_DAY;
63  
64      /**
65       * The pattern for parsing.
66       */
67      private static final Pattern PATTERN = Pattern.compile("P?(?:([0-9]+)D)?"
68              + "(T?(?:([0-9]+)H)?(?:([0-9]+)M)?(?:([0-9]+)?S)?)?", Pattern.CASE_INSENSITIVE);
69  
70      /**
71       * The number of seconds in the duration.
72       */
73      private final long seconds;
74  
75      /**
76       * Constructs an instance of {@code Duration} using seconds.
77       *
78       * @param seconds the length of the duration in seconds, positive or negative
79       */
80      private Duration(final long seconds) {
81          super();
82          this.seconds = seconds;
83      }
84  
85      /**
86       * Obtains a {@code Duration} from a text string such as {@code PnDTnHnMnS}.
87       * <p>
88       * This will parse a textual representation of a duration, including the string produced by {@code toString()}. The
89       * formats accepted are based on the ISO-8601 duration format {@code PnDTnHnMnS} with days considered to be exactly
90       * 24 hours.
91       * <p>
92       * This implementation does not support negative numbers or fractions (so the smallest non-zero value a Duration can
93       * have is one second).
94       * <p>
95       * The string optionally starts with the ASCII letter "P" in upper or lower case. There are then four sections, each
96       * consisting of a number and a suffix. The sections have suffixes in ASCII of "D", "H", "M" and "S" for days,
97       * hours, minutes and seconds, accepted in upper or lower case. The suffixes must occur in order. The ASCII letter
98       * "T" may occur before the first occurrence, if any, of an hour, minute or second section. At least one of the four
99       * sections must be present, and if "T" is present there must be at least one section after the "T". The number part
100      * of each section must consist of one or more ASCII digits. The number may not be prefixed by the ASCII negative or
101      * positive symbol. The number of days, hours, minutes and seconds must parse to a {@code long}.
102      * <p>
103      * Examples:
104      *
105      * <pre>
106      *    "PT20S" -- parses as "20 seconds"
107      *    "PT15M"     -- parses as "15 minutes" (where a minute is 60 seconds)
108      *    "PT10H"     -- parses as "10 hours" (where an hour is 3600 seconds)
109      *    "P2D"       -- parses as "2 days" (where a day is 24 hours or 86400 seconds)
110      *    "P2DT3H4M"  -- parses as "2 days, 3 hours and 4 minutes"
111      * </pre>
112      *
113      * @param text the text to parse, not null
114      * @return the parsed duration, not null
115      * @throws IllegalArgumentException if the text cannot be parsed to a duration
116      */
117     public static Duration parse(final CharSequence text) {
118         Objects.requireNonNull(text, "text");
119         final Matcher matcher = PATTERN.matcher(text);
120         if (matcher.matches()) {
121             // check for letter T but no time sections
122             if ("T".equals(matcher.group(2)) == false) {
123                 final String dayMatch = matcher.group(1);
124                 final String hourMatch = matcher.group(3);
125                 final String minuteMatch = matcher.group(4);
126                 final String secondMatch = matcher.group(5);
127                 if (dayMatch != null || hourMatch != null || minuteMatch != null || secondMatch != null) {
128                     final long daysAsSecs = parseNumber(text, dayMatch, SECONDS_PER_DAY, "days");
129                     final long hoursAsSecs = parseNumber(text, hourMatch, SECONDS_PER_HOUR, "hours");
130                     final long minsAsSecs = parseNumber(text, minuteMatch, SECONDS_PER_MINUTE, "minutes");
131                     final long seconds = parseNumber(text, secondMatch, 1, "seconds");
132                     try {
133                         return create(daysAsSecs, hoursAsSecs, minsAsSecs, seconds);
134                     } catch (final ArithmeticException ex) {
135                         throw new IllegalArgumentException("Text cannot be parsed to a Duration (overflow) " + text, ex);
136                     }
137                 }
138             }
139         }
140         throw new IllegalArgumentException("Text cannot be parsed to a Duration: " + text);
141     }
142 
143     private static long parseNumber(final CharSequence text, final String parsed, final int multiplier,
144             final String errorText) {
145         // regex limits to [0-9]+
146         if (parsed == null) {
147             return 0;
148         }
149         try {
150             final long val = Long.parseLong(parsed);
151             return val * multiplier;
152         } catch (final Exception ex) {
153             throw new IllegalArgumentException("Text cannot be parsed to a Duration: " + errorText + " (in " + text
154                     + ")", ex);
155         }
156     }
157 
158     private static Duration create(final long daysAsSecs, final long hoursAsSecs, final long minsAsSecs, final long secs) {
159         return create(daysAsSecs + hoursAsSecs + minsAsSecs + secs);
160     }
161 
162     /**
163      * Obtains an instance of {@code Duration} using seconds.
164      *
165      * @param seconds the length of the duration in seconds, positive only
166      */
167     private static Duration create(final long seconds) {
168         if ((seconds) == 0) {
169             return ZERO;
170         }
171         return new Duration(seconds);
172     }
173 
174     /**
175      * Converts this duration to the total length in milliseconds.
176      *
177      * @return the total length of the duration in milliseconds
178      */
179     public long toMillis() {
180         return seconds * 1000L;
181     }
182 
183     @Override
184     public boolean equals(final Object obj) {
185         if (obj == this) {
186             return true;
187         }
188         if (!(obj instanceof Duration)) {
189             return false;
190         }
191         final Duration other = (Duration) obj;
192         return other.seconds == this.seconds;
193     }
194 
195     @Override
196     public int hashCode() {
197         return (int) (seconds ^ (seconds >>> 32));
198     }
199 
200     /**
201      * A string representation of this duration using ISO-8601 seconds based representation, such as {@code PT8H6M12S}.
202      * <p>
203      * The format of the returned string will be {@code PnDTnHnMnS}, where n is the relevant days, hours, minutes or
204      * seconds part of the duration. If a section has a zero value, it is omitted. The hours, minutes and seconds are
205      * all positive.
206      * <p>
207      * Examples:
208      *
209      * <pre>
210      *    "20 seconds"                     -- "PT20S
211      *    "15 minutes" (15 * 60 seconds)   -- "PT15M"
212      *    "10 hours" (10 * 3600 seconds)   -- "PT10H"
213      *    "2 days" (2 * 86400 seconds)     -- "P2D"
214      * </pre>
215      *
216      * @return an ISO-8601 representation of this duration, not null
217      */
218     @Override
219     public String toString() {
220         if (this == ZERO) {
221             return "PT0S";
222         }
223         final long days = seconds / SECONDS_PER_DAY;
224         final long hours = (seconds % SECONDS_PER_DAY) / SECONDS_PER_HOUR;
225         final int minutes = (int) ((seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE);
226         final int secs = (int) (seconds % SECONDS_PER_MINUTE);
227         final StringBuilder buf = new StringBuilder(24);
228         buf.append("P");
229         if (days != 0) {
230             buf.append(days).append('D');
231         }
232         if ((hours | minutes | secs) != 0) {
233             buf.append('T');
234         }
235         if (hours != 0) {
236             buf.append(hours).append('H');
237         }
238         if (minutes != 0) {
239             buf.append(minutes).append('M');
240         }
241         if (secs == 0 && buf.length() > 0) {
242             return buf.toString();
243         }
244         buf.append(secs).append('S');
245         return buf.toString();
246     }
247 
248     /*
249      * (non-Javadoc)
250      *
251      * @see java.lang.Comparable#compareTo(java.lang.Object)
252      */
253     @Override
254     public int compareTo(final Duration other) {
255         return Long.signum(toMillis() - other.toMillis());
256     }
257 }