1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.forgerock.util.time;
18
19 import static java.lang.String.format;
20 import static java.util.Arrays.asList;
21 import static java.util.concurrent.TimeUnit.DAYS;
22 import static java.util.concurrent.TimeUnit.HOURS;
23 import static java.util.concurrent.TimeUnit.MICROSECONDS;
24 import static java.util.concurrent.TimeUnit.MILLISECONDS;
25 import static java.util.concurrent.TimeUnit.MINUTES;
26 import static java.util.concurrent.TimeUnit.NANOSECONDS;
27 import static java.util.concurrent.TimeUnit.SECONDS;
28 import static org.forgerock.util.Reject.checkNotNull;
29
30 import java.util.ArrayList;
31 import java.util.HashMap;
32 import java.util.List;
33 import java.util.Locale;
34 import java.util.Map;
35 import java.util.Objects;
36 import java.util.Set;
37 import java.util.TreeSet;
38 import java.util.concurrent.TimeUnit;
39
40 import org.forgerock.util.Reject;
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56 public class Duration implements Comparable<Duration> {
57
58
59
60
61 public static final Duration UNLIMITED = new Duration();
62
63
64
65
66 public static final Duration ZERO = new Duration(0L, SECONDS);
67
68
69
70
71 private static final Set<String> UNLIMITED_TOKENS = new TreeSet<>(
72 String.CASE_INSENSITIVE_ORDER);
73 static {
74 UNLIMITED_TOKENS.addAll(asList("unlimited", "indefinite", "infinity", "undefined", "none"));
75 }
76
77
78
79
80 private static final Set<String> ZERO_TOKENS = new TreeSet<>(
81 String.CASE_INSENSITIVE_ORDER);
82 static {
83 ZERO_TOKENS.addAll(asList("zero", "disabled"));
84 }
85
86 private long number;
87 private TimeUnit unit;
88
89
90
91
92
93 private Duration() {
94 this.number = Long.MAX_VALUE;
95 this.unit = null;
96 }
97
98
99
100
101
102
103
104
105 @Deprecated
106 public Duration(final Long number, final TimeUnit unit) {
107 Reject.ifTrue(number < 0, "Negative durations are not supported");
108 this.number = number;
109 this.unit = checkNotNull(unit);
110 }
111
112
113
114
115
116
117
118
119 public static Duration duration(final long number, final TimeUnit unit) {
120 if (number == 0) {
121 return ZERO;
122 }
123 return new Duration(number, unit);
124 }
125
126
127
128
129
130
131
132
133
134
135 public static Duration duration(final String value) {
136 List<Duration> composite = new ArrayList<>();
137
138
139 String[] fragments = value.split(",| and ");
140
141
142 if (fragments.length == 1) {
143 String trimmed = fragments[0].trim();
144 if (UNLIMITED_TOKENS.contains(trimmed)) {
145
146 return UNLIMITED;
147 } else if (ZERO_TOKENS.contains(trimmed)) {
148
149 return ZERO;
150 }
151 }
152
153
154 for (String fragment : fragments) {
155 fragment = fragment.trim();
156
157 if ("".equals(fragment)) {
158 throw new IllegalArgumentException("Cannot parse empty duration, expecting '<value> <unit>' pattern");
159 }
160
161
162 int i = 0;
163 StringBuilder numberSB = new StringBuilder();
164 while (Character.isDigit(fragment.charAt(i))) {
165 numberSB.append(fragment.charAt(i));
166 i++;
167 }
168
169
170 while (Character.isWhitespace(fragment.charAt(i))) {
171 i++;
172 }
173
174
175 StringBuilder unitSB = new StringBuilder();
176 while ((i < fragment.length()) && Character.isLetter(fragment.charAt(i))) {
177 unitSB.append(fragment.charAt(i));
178 i++;
179 }
180 Long number = Long.valueOf(numberSB.toString());
181 TimeUnit unit = parseTimeUnit(unitSB.toString());
182
183 composite.add(new Duration(number, unit));
184 }
185
186
187 Duration duration = new Duration(0L, DAYS);
188 for (Duration elements : composite) {
189 duration.merge(elements);
190 }
191
192
193 if (duration.number == 0L) {
194 return ZERO;
195 }
196
197 return duration;
198 }
199
200
201
202
203
204
205
206 private void merge(final Duration duration) {
207 if (!isUnlimited() && !duration.isUnlimited()) {
208
209
210 if (unit.ordinal() > duration.unit.ordinal()) {
211
212 number = duration.unit.convert(number, unit) + duration.number;
213 unit = duration.unit;
214 } else {
215
216 number = unit.convert(duration.number, duration.unit) + number;
217 }
218 }
219 }
220
221 private static final Map<String, TimeUnit> TIME_UNITS = new HashMap<>();
222 static {
223 for (String days : asList("days", "day", "d")) {
224 TIME_UNITS.put(days, DAYS);
225 }
226 for (String hours : asList("hours", "hour", "h")) {
227 TIME_UNITS.put(hours, HOURS);
228 }
229 for (String minutes : asList("minutes", "minute", "min", "m")) {
230 TIME_UNITS.put(minutes, MINUTES);
231 }
232 for (String seconds : asList("seconds", "second", "sec", "s")) {
233 TIME_UNITS.put(seconds, SECONDS);
234 }
235 for (String ms : asList("milliseconds", "millisecond", "millisec", "millis", "milli", "ms")) {
236 TIME_UNITS.put(ms, MILLISECONDS);
237 }
238 for (String us : asList("microseconds", "microsecond", "microsec", "micros", "micro", "us", "\u03BCs",
239 "\u00B5s")) {
240 TIME_UNITS.put(us, MICROSECONDS);
241 }
242 for (String ns : asList("nanoseconds", "nanosecond", "nanosec", "nanos", "nano", "ns")) {
243 TIME_UNITS.put(ns, NANOSECONDS);
244 }
245 }
246
247
248
249
250 private static TimeUnit parseTimeUnit(final String unit) {
251 final String lowercase = unit.toLowerCase(Locale.ENGLISH);
252 final TimeUnit timeUnit = TIME_UNITS.get(lowercase);
253 if (timeUnit != null) {
254 return timeUnit;
255 }
256 throw new IllegalArgumentException(format("TimeUnit %s is not recognized", unit));
257 }
258
259
260
261
262
263
264 public long getValue() {
265 return number;
266 }
267
268
269
270
271
272
273 public TimeUnit getUnit() {
274 if (isUnlimited()) {
275
276 return TimeUnit.DAYS;
277 }
278 return unit;
279 }
280
281
282
283
284
285
286
287
288
289
290 public Duration convertTo(TimeUnit targetUnit) {
291 if (isUnlimited() || isZero()) {
292 return this;
293 }
294 return new Duration(to(targetUnit), targetUnit);
295 }
296
297
298
299
300
301
302
303
304
305
306 public long to(TimeUnit targetUnit) {
307 if (isUnlimited()) {
308 return number;
309 }
310 return targetUnit.convert(number, unit);
311 }
312
313
314
315
316
317
318 public boolean isUnlimited() {
319 return this == UNLIMITED;
320 }
321
322
323
324
325
326
327 public boolean isZero() {
328 return number == 0;
329 }
330
331 @Override
332 public String toString() {
333 if (isUnlimited()) {
334 return "UNLIMITED";
335 }
336 if (isZero()) {
337 return "ZERO";
338 }
339 return number + " " + unit;
340 }
341
342 @Override
343 public int compareTo(Duration that) {
344 if (this.isUnlimited()) {
345 if (that.isUnlimited()) {
346
347 return 0;
348 } else {
349
350 return 1;
351 }
352 }
353 if (that.isUnlimited()) {
354
355 return -1;
356 }
357 if (this.isZero()) {
358 if (that.isZero()) {
359
360 return 0;
361 } else {
362
363 return -1;
364 }
365 }
366 if (that.isZero()) {
367
368 return 1;
369 }
370
371
372
373 final int unitCompare = this.getUnit().compareTo(that.getUnit());
374 final boolean biggestOverflowed;
375 final long thisConverted, thatConverted;
376 if (unitCompare > 0) {
377 thisConverted = this.convertTo(that.getUnit()).getValue();
378 thatConverted = that.getValue();
379 biggestOverflowed = thisConverted == Long.MAX_VALUE;
380 } else if (unitCompare < 0) {
381 thisConverted = this.getValue();
382 thatConverted = that.convertTo(this.getUnit()).getValue();
383 biggestOverflowed = thatConverted == Long.MAX_VALUE;
384 } else {
385
386
387 biggestOverflowed = false;
388 thisConverted = this.getValue();
389 thatConverted = that.getValue();
390 }
391
392
393 return !biggestOverflowed ? Long.compare(thisConverted, thatConverted) : unitCompare;
394 }
395
396 @Override
397 public boolean equals(Object other) {
398 if (this == other) {
399 return true;
400 }
401
402 if (!(other instanceof Duration)) {
403 return false;
404 }
405
406 Duration duration = (Duration) other;
407 return number == duration.number && unit == duration.unit;
408 }
409
410 @Override
411 public int hashCode() {
412 return Objects.hash(number, unit);
413 }
414
415 }