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}