blob: 8684a29c6ad49afa1e7d341cf402e6a2b150929a [file] [log] [blame]
/*
* 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.logging.log4j.core.appender.rolling.action;
import java.io.Serializable;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Simplified implementation of the <a href="https://en.wikipedia.org/wiki/ISO_8601#Durations">ISO-8601 Durations</a>
* standard. The supported format is {@code PnDTnHnMnS}, with 'P' and 'T' optional. Days are considered to be exactly 24
* hours.
* <p>
* Similarly to the {@code java.time.Duration} class, this class does not support year or month sections in the format.
* This implementation does not support fractions or negative values.
*
* @see #parse(CharSequence)
*/
public class Duration implements Serializable, Comparable<Duration> {
private static final long serialVersionUID = -3756810052716342061L;
/**
* Constant for a duration of zero.
*/
public static final Duration ZERO = new Duration(0);
/**
* Hours per day.
*/
private static final int HOURS_PER_DAY = 24;
/**
* Minutes per hour.
*/
private static final int MINUTES_PER_HOUR = 60;
/**
* Seconds per minute.
*/
private static final int SECONDS_PER_MINUTE = 60;
/**
* Seconds per hour.
*/
private static final int SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
/**
* Seconds per day.
*/
private static final int SECONDS_PER_DAY = SECONDS_PER_HOUR * HOURS_PER_DAY;
/**
* The pattern for parsing.
*/
private static final Pattern PATTERN = Pattern.compile("P?(?:([0-9]+)D)?"
+ "(T?(?:([0-9]+)H)?(?:([0-9]+)M)?(?:([0-9]+)?S)?)?", Pattern.CASE_INSENSITIVE);
/**
* The number of seconds in the duration.
*/
private final long seconds;
/**
* Constructs an instance of {@code Duration} using seconds.
*
* @param seconds the length of the duration in seconds, positive or negative
*/
private Duration(final long seconds) {
super();
this.seconds = seconds;
}
/**
* Obtains a {@code Duration} from a text string such as {@code PnDTnHnMnS}.
* <p>
* This will parse a textual representation of a duration, including the string produced by {@code toString()}. The
* formats accepted are based on the ISO-8601 duration format {@code PnDTnHnMnS} with days considered to be exactly
* 24 hours.
* <p>
* This implementation does not support negative numbers or fractions (so the smallest non-zero value a Duration can
* have is one second).
* <p>
* The string optionally starts with the ASCII letter "P" in upper or lower case. There are then four sections, each
* consisting of a number and a suffix. The sections have suffixes in ASCII of "D", "H", "M" and "S" for days,
* hours, minutes and seconds, accepted in upper or lower case. The suffixes must occur in order. The ASCII letter
* "T" may occur before the first occurrence, if any, of an hour, minute or second section. At least one of the four
* sections must be present, and if "T" is present there must be at least one section after the "T". The number part
* of each section must consist of one or more ASCII digits. The number may not be prefixed by the ASCII negative or
* positive symbol. The number of days, hours, minutes and seconds must parse to a {@code long}.
* <p>
* Examples:
*
* <pre>
* "PT20S" -- parses as "20 seconds"
* "PT15M" -- parses as "15 minutes" (where a minute is 60 seconds)
* "PT10H" -- parses as "10 hours" (where an hour is 3600 seconds)
* "P2D" -- parses as "2 days" (where a day is 24 hours or 86400 seconds)
* "P2DT3H4M" -- parses as "2 days, 3 hours and 4 minutes"
* </pre>
*
* @param text the text to parse, not null
* @return the parsed duration, not null
* @throws IllegalArgumentException if the text cannot be parsed to a duration
*/
public static Duration parse(final CharSequence text) {
Objects.requireNonNull(text, "text");
final Matcher matcher = PATTERN.matcher(text);
if (matcher.matches()) {
// check for letter T but no time sections
if ("T".equals(matcher.group(2)) == false) {
final String dayMatch = matcher.group(1);
final String hourMatch = matcher.group(3);
final String minuteMatch = matcher.group(4);
final String secondMatch = matcher.group(5);
if (dayMatch != null || hourMatch != null || minuteMatch != null || secondMatch != null) {
final long daysAsSecs = parseNumber(text, dayMatch, SECONDS_PER_DAY, "days");
final long hoursAsSecs = parseNumber(text, hourMatch, SECONDS_PER_HOUR, "hours");
final long minsAsSecs = parseNumber(text, minuteMatch, SECONDS_PER_MINUTE, "minutes");
final long seconds = parseNumber(text, secondMatch, 1, "seconds");
try {
return create(daysAsSecs, hoursAsSecs, minsAsSecs, seconds);
} catch (final ArithmeticException ex) {
throw new IllegalArgumentException("Text cannot be parsed to a Duration (overflow) " + text, ex);
}
}
}
}
throw new IllegalArgumentException("Text cannot be parsed to a Duration: " + text);
}
private static long parseNumber(final CharSequence text, final String parsed, final int multiplier,
final String errorText) {
// regex limits to [0-9]+
if (parsed == null) {
return 0;
}
try {
final long val = Long.parseLong(parsed);
return val * multiplier;
} catch (final Exception ex) {
throw new IllegalArgumentException("Text cannot be parsed to a Duration: " + errorText + " (in " + text
+ ")", ex);
}
}
private static Duration create(final long daysAsSecs, final long hoursAsSecs, final long minsAsSecs, final long secs) {
return create(daysAsSecs + hoursAsSecs + minsAsSecs + secs);
}
/**
* Obtains an instance of {@code Duration} using seconds.
*
* @param seconds the length of the duration in seconds, positive only
*/
private static Duration create(final long seconds) {
if ((seconds) == 0) {
return ZERO;
}
return new Duration(seconds);
}
/**
* Converts this duration to the total length in milliseconds.
*
* @return the total length of the duration in milliseconds
*/
public long toMillis() {
return seconds * 1000L;
}
@Override
public boolean equals(final Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof Duration)) {
return false;
}
final Duration other = (Duration) obj;
return other.seconds == this.seconds;
}
@Override
public int hashCode() {
return (int) (seconds ^ (seconds >>> 32));
}
/**
* A string representation of this duration using ISO-8601 seconds based representation, such as {@code PT8H6M12S}.
* <p>
* The format of the returned string will be {@code PnDTnHnMnS}, where n is the relevant days, hours, minutes or
* seconds part of the duration. If a section has a zero value, it is omitted. The hours, minutes and seconds are
* all positive.
* <p>
* Examples:
*
* <pre>
* "20 seconds" -- "PT20S
* "15 minutes" (15 * 60 seconds) -- "PT15M"
* "10 hours" (10 * 3600 seconds) -- "PT10H"
* "2 days" (2 * 86400 seconds) -- "P2D"
* </pre>
*
* @return an ISO-8601 representation of this duration, not null
*/
@Override
public String toString() {
if (this == ZERO) {
return "PT0S";
}
final long days = seconds / SECONDS_PER_DAY;
final long hours = (seconds % SECONDS_PER_DAY) / SECONDS_PER_HOUR;
final int minutes = (int) ((seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE);
final int secs = (int) (seconds % SECONDS_PER_MINUTE);
final StringBuilder buf = new StringBuilder(24);
buf.append("P");
if (days != 0) {
buf.append(days).append('D');
}
if ((hours | minutes | secs) != 0) {
buf.append('T');
}
if (hours != 0) {
buf.append(hours).append('H');
}
if (minutes != 0) {
buf.append(minutes).append('M');
}
if (secs == 0 && buf.length() > 0) {
return buf.toString();
}
buf.append(secs).append('S');
return buf.toString();
}
/*
* (non-Javadoc)
*
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
@Override
public int compareTo(final Duration other) {
return Long.signum(toMillis() - other.toMillis());
}
}