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 }