001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2014 ForgeRock AS. 015 */ 016 017package org.forgerock.openig.util; 018 019import java.util.ArrayList; 020import java.util.List; 021import java.util.Set; 022import java.util.concurrent.TimeUnit; 023 024import static java.lang.String.format; 025import static java.util.Arrays.asList; 026import static java.util.concurrent.TimeUnit.*; 027import static org.forgerock.util.Reject.checkNotNull; 028 029/** 030 * Represents a duration in english. Cases is not important, plurals units are accepted. 031 * Notice that negative durations are not supported. 032 * 033 * <code> 034 * 6 days 035 * 59 minutes and 1 millisecond 036 * 1 minute and 10 seconds 037 * 42 millis 038 * unlimited 039 * none 040 * zero 041 * </code> 042 */ 043public class Duration { 044 045 /** 046 * Special duration that represents an unlimited duration (or indefinite). 047 */ 048 private static final Duration UNLIMITED = new Duration(Long.MAX_VALUE, DAYS); 049 050 /** 051 * Special duration that represents a zero-length duration. 052 */ 053 private static final Duration ZERO = new Duration(0L, SECONDS); 054 055 /** 056 * Tokens that represents the unlimited duration. 057 */ 058 private static final Set<String> UNLIMITED_TOKENS = new CaseInsensitiveSet(asList("unlimited", 059 "indefinite", 060 "infinity", 061 "undefined")); 062 063 /** 064 * Tokens that represents the zero duration. 065 */ 066 private static final Set<String> ZERO_TOKENS = new CaseInsensitiveSet(asList("zero", 067 "disabled")); 068 069 private Long number; 070 private TimeUnit unit; 071 072 /** 073 * Builds a new Duration. 074 * @param number number of time unit (cannot be {@literal null}). 075 * @param unit TimeUnit to express the duration in (cannot be {@literal null}). 076 */ 077 public Duration(final Long number, final TimeUnit unit) { 078 this.number = checkNotNull(number); 079 this.unit = checkNotNull(unit); 080 } 081 082 /** 083 * Builds a new {@link Duration} that will represents the given duration expressed in english. 084 * 085 * @param value 086 * natural speech duration 087 * @return a new {@link Duration} 088 * @throws IllegalArgumentException 089 * if the input string is incorrectly formatted. 090 */ 091 public static Duration duration(final String value) { 092 List<Duration> composite = new ArrayList<Duration>(); 093 094 // Split around ',' and ' and ' patterns 095 String[] fragments = value.split(",| and "); 096 097 // If there is only 1 fragment and that it matches the recognized "unlimited" tokens 098 if (fragments.length == 1) { 099 String trimmed = fragments[0].trim(); 100 if (UNLIMITED_TOKENS.contains(trimmed)) { 101 // Unlimited Duration 102 return UNLIMITED; 103 } else if (ZERO_TOKENS.contains(trimmed)) { 104 // Zero-length Duration 105 return ZERO; 106 } 107 } 108 109 // Build the normal duration 110 for (String fragment : fragments) { 111 112 fragment = fragment.trim(); 113 114 if ("".equals(fragment)) { 115 throw new IllegalArgumentException("Cannot parse empty duration, expecting '<value> <unit>' pattern"); 116 } 117 118 // Parse the number part 119 int i = 0; 120 StringBuilder numberSB = new StringBuilder(); 121 while (Character.isDigit(fragment.charAt(i))) { 122 numberSB.append(fragment.charAt(i)); 123 i++; 124 } 125 126 // Ignore whitespace 127 while (Character.isWhitespace(fragment.charAt(i))) { 128 i++; 129 } 130 131 // Parse the time unit part 132 StringBuilder unitSB = new StringBuilder(); 133 while ((i < fragment.length()) && Character.isLetter(fragment.charAt(i))) { 134 unitSB.append(fragment.charAt(i)); 135 i++; 136 } 137 Long number = Long.valueOf(numberSB.toString()); 138 TimeUnit unit = parseTimeUnit(unitSB.toString()); 139 140 composite.add(new Duration(number, unit)); 141 } 142 143 // Merge components of the composite together 144 Duration duration = new Duration(0L, DAYS); 145 for (Duration elements : composite) { 146 duration.merge(elements); 147 } 148 149 // If someone used '0 ms' for example 150 if (duration.number == 0L) { 151 return ZERO; 152 } 153 154 return duration; 155 } 156 157 /** 158 * Aggregates this Duration with the given Duration. Littlest {@link TimeUnit} will be used as a common ground. 159 * 160 * @param duration 161 * other Duration 162 */ 163 private void merge(final Duration duration) { 164 // find littlest unit 165 // conversion will happen on the littlest unit otherwise we loose details 166 if (unit.ordinal() > duration.unit.ordinal()) { 167 // Other duration is smaller than me 168 number = duration.unit.convert(number, unit) + duration.number; 169 unit = duration.unit; 170 } else { 171 // Other duration is greater than me 172 number = unit.convert(duration.number, duration.unit) + number; 173 } 174 } 175 176 /** 177 * Parse the given input string as a {@link TimeUnit}. 178 */ 179 private static TimeUnit parseTimeUnit(final String unit) { 180 String lowercase = unit.toLowerCase(); 181 182 // @Checkstyle:off 183 184 if ("days".equals(lowercase)) return DAYS; 185 if ("day".equals(lowercase)) return DAYS; 186 if ("d".equals(lowercase)) return DAYS; 187 188 if ("hours".equals(lowercase)) return TimeUnit.HOURS; 189 if ("hour".equals(lowercase)) return TimeUnit.HOURS; 190 if ("h".equals(lowercase)) return TimeUnit.HOURS; 191 192 if ("minutes".equals(lowercase)) return TimeUnit.MINUTES; 193 if ("minute".equals(lowercase)) return TimeUnit.MINUTES; 194 if ("min".equals(lowercase)) return TimeUnit.MINUTES; 195 if ("m".equals(lowercase)) return TimeUnit.MINUTES; 196 197 if ("seconds".equals(lowercase)) return TimeUnit.SECONDS; 198 if ("second".equals(lowercase)) return TimeUnit.SECONDS; 199 if ("sec".equals(lowercase)) return TimeUnit.SECONDS; 200 if ("s".equals(lowercase)) return TimeUnit.SECONDS; 201 202 if ("milliseconds".equals(lowercase)) return TimeUnit.MILLISECONDS; 203 if ("millisecond".equals(lowercase)) return TimeUnit.MILLISECONDS; 204 if ("millisec".equals(lowercase)) return TimeUnit.MILLISECONDS; 205 if ("millis".equals(lowercase)) return TimeUnit.MILLISECONDS; 206 if ("milli".equals(lowercase)) return TimeUnit.MILLISECONDS; 207 if ("ms".equals(lowercase)) return TimeUnit.MILLISECONDS; 208 209 if ("microseconds".equals(lowercase)) return TimeUnit.MICROSECONDS; 210 if ("microsecond".equals(lowercase)) return TimeUnit.MICROSECONDS; 211 if ("microsec".equals(lowercase)) return TimeUnit.MICROSECONDS; 212 if ("micros".equals(lowercase)) return TimeUnit.MICROSECONDS; 213 if ("micro".equals(lowercase)) return TimeUnit.MICROSECONDS; 214 if ("us".equals(lowercase)) return TimeUnit.MICROSECONDS; 215 216 if ("nanoseconds".equals(lowercase)) return TimeUnit.NANOSECONDS; 217 if ("nanosecond".equals(lowercase)) return TimeUnit.NANOSECONDS; 218 if ("nanosec".equals(lowercase)) return TimeUnit.NANOSECONDS; 219 if ("nanos".equals(lowercase)) return TimeUnit.NANOSECONDS; 220 if ("nano".equals(lowercase)) return TimeUnit.NANOSECONDS; 221 if ("ns".equals(lowercase)) return TimeUnit.NANOSECONDS; 222 223 // @Checkstyle:on 224 225 throw new IllegalArgumentException(format("TimeUnit %s is not recognized", unit)); 226 } 227 228 /** 229 * Returns the number of {@link TimeUnit} this duration represents. 230 * 231 * @return the number of {@link TimeUnit} this duration represents. 232 */ 233 public long getValue() { 234 return number; 235 } 236 237 /** 238 * Returns the {@link TimeUnit} this duration is expressed in. 239 * 240 * @return the {@link TimeUnit} this duration is expressed in. 241 */ 242 public TimeUnit getUnit() { 243 return unit; 244 } 245 246 /** 247 * Convert the current duration to a given {@link TimeUnit}. 248 * Conversions from finer to coarser granularities truncate, so loose precision. 249 * 250 * @param targetUnit 251 * target unit of the conversion. 252 * @return converted duration 253 * @see TimeUnit#convert(long, TimeUnit) 254 */ 255 public Duration convertTo(TimeUnit targetUnit) { 256 return new Duration(targetUnit.convert(number, unit), targetUnit); 257 } 258 259 /** 260 * Convert the current duration to a number of given {@link TimeUnit}. 261 * Conversions from finer to coarser granularities truncate, so loose precision. 262 * 263 * @param targetUnit 264 * target unit of the conversion. 265 * @return converted duration value 266 * @see TimeUnit#convert(long, TimeUnit) 267 */ 268 public long to(TimeUnit targetUnit) { 269 return convertTo(targetUnit).getValue(); 270 } 271 272 /** 273 * Returns {@literal true} if this Duration represents an unlimited duration. 274 * 275 * @return {@literal true} if this Duration represents an unlimited duration. 276 */ 277 public boolean isUnlimited() { 278 return this == UNLIMITED; 279 } 280 281 /** 282 * Returns {@literal true} if this Duration represents a zero-length duration. 283 * 284 * @return {@literal true} if this Duration represents a zero-length duration. 285 */ 286 public boolean isZero() { 287 return this == ZERO; 288 } 289 290}