001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache license, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the license for the specific language governing permissions and
015 * limitations under the license.
016 */
017
018package org.apache.logging.log4j.core.appender.rolling.action;
019
020import java.io.Serializable;
021import java.util.Objects;
022import java.util.regex.Matcher;
023import java.util.regex.Pattern;
024
025/**
026 * Simplified implementation of the <a href="https://en.wikipedia.org/wiki/ISO_8601#Durations">ISO-8601 Durations</a>
027 * standard. The supported format is {@code PnDTnHnMnS}, with 'P' and 'T' optional. Days are considered to be exactly 24
028 * hours.
029 * <p>
030 * Similarly to the {@code java.time.Duration} class, this class does not support year or month sections in the format.
031 * This implementation does not support fractions or negative values.
032 *
033 * @see #parse(CharSequence)
034 */
035public class Duration implements Serializable, Comparable<Duration> {
036    private static final long serialVersionUID = -3756810052716342061L;
037
038    /**
039     * Constant for a duration of zero.
040     */
041    public static final Duration ZERO = new Duration(0);
042
043    /**
044     * Hours per day.
045     */
046    private static final int HOURS_PER_DAY = 24;
047    /**
048     * Minutes per hour.
049     */
050    private static final int MINUTES_PER_HOUR = 60;
051    /**
052     * Seconds per minute.
053     */
054    private static final int SECONDS_PER_MINUTE = 60;
055    /**
056     * Seconds per hour.
057     */
058    private static final int SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
059    /**
060     * Seconds per day.
061     */
062    private static final int SECONDS_PER_DAY = SECONDS_PER_HOUR * HOURS_PER_DAY;
063
064    /**
065     * The pattern for parsing.
066     */
067    private static final Pattern PATTERN = Pattern.compile("P?(?:([0-9]+)D)?"
068            + "(T?(?:([0-9]+)H)?(?:([0-9]+)M)?(?:([0-9]+)?S)?)?", Pattern.CASE_INSENSITIVE);
069
070    /**
071     * The number of seconds in the duration.
072     */
073    private final long seconds;
074
075    /**
076     * Constructs an instance of {@code Duration} using seconds.
077     *
078     * @param seconds the length of the duration in seconds, positive or negative
079     */
080    private Duration(final long seconds) {
081        super();
082        this.seconds = seconds;
083    }
084
085    /**
086     * Obtains a {@code Duration} from a text string such as {@code PnDTnHnMnS}.
087     * <p>
088     * This will parse a textual representation of a duration, including the string produced by {@code toString()}. The
089     * formats accepted are based on the ISO-8601 duration format {@code PnDTnHnMnS} with days considered to be exactly
090     * 24 hours.
091     * <p>
092     * This implementation does not support negative numbers or fractions (so the smallest non-zero value a Duration can
093     * have is one second).
094     * <p>
095     * The string optionally starts with the ASCII letter "P" in upper or lower case. There are then four sections, each
096     * consisting of a number and a suffix. The sections have suffixes in ASCII of "D", "H", "M" and "S" for days,
097     * hours, minutes and seconds, accepted in upper or lower case. The suffixes must occur in order. The ASCII letter
098     * "T" may occur before the first occurrence, if any, of an hour, minute or second section. At least one of the four
099     * 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}