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}