    001// Licensed under the Apache License, Version 2.0 (the "License");
    002// you may not use this file except in compliance with the License.
    003// You may obtain a copy of the License at
    004//
    005// http://www.apache.org/licenses/LICENSE-2.0
    006//
    007// Unless required by applicable law or agreed to in writing, software
    008// distributed under the License is distributed on an "AS IS" BASIS,
    009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    010// See the License for the specific language governing permissions and
    011// limitations under the License.
    012
    013package org.apache.tapestry5.commons.util;
    014
    015import java.util.Map;
    016import java.util.regex.Matcher;
    017import java.util.regex.Pattern;
    018
    019import org.apache.tapestry5.commons.internal.util.InternalCommonsUtils;
    020
    021/**
    022 * Used to represent a period of time, specifically as a configuration value. This is often used to specify timeouts.
    023 *
    024 * TimePeriods are parsed from strings.
    025 *
    026 * The string specifys a number of terms. The values of all the terms are summed together to form the total time period.
    027 * Each term consists of a number followed by a unit. Units (from largest to smallest) are: <dl> <dt>y <dd>year <dt>d
    028 * <dd>day <dt>h <dd>hour <dt>m <dd>minute <dt>s <dd>second <dt>ms <dd>millisecond </dl>   Example: "2 h 30 m". By
    029 * convention, terms are specified largest to smallest.  A term without a unit is assumed to be milliseconds.  Units are
    030 * case insensitive ("h" or "H" are treated the same).
    031 */
    032public class TimeInterval
    033{
    034    private static final Map<String, Long> UNITS = CollectionFactory.newCaseInsensitiveMap();
    035
    036    private static final long MILLISECOND = 1000l;
    037
    038    static
    039    {
    040        UNITS.put("ms", 1l);
    041        UNITS.put("s", MILLISECOND);
    042        UNITS.put("m", 60 * MILLISECOND);
    043        UNITS.put("h", 60 * UNITS.get("m"));
    044        UNITS.put("d", 24 * UNITS.get("h"));
    045        UNITS.put("y", 365 * UNITS.get("d"));
    046    }
    047
    048    /**
    049     * The unit keys, sorted in descending order.
    050     */
    051    private static final String[] UNIT_KEYS =
    052    { "y", "d", "h", "m", "s", "ms" };
    053
    054    private static final Pattern PATTERN = Pattern.compile("\\s*(\\d+)\\s*([a-z]*)", Pattern.CASE_INSENSITIVE);
    055
    056    private final long milliseconds;
    057
    058    /**
    059     * Creates a TimeInterval for a string.
    060     *
    061     * @param input
    062     *            the string specifying the amount of time in the period
    063     */
    064    public TimeInterval(String input)
    065    {
    066        this(parseMilliseconds(input));
    067    }
    068
    069    public TimeInterval(long milliseconds)
    070    {
    071        this.milliseconds = milliseconds;
    072    }
    073
    074    public long milliseconds()
    075    {
    076        return milliseconds;
    077    }
    078
    079    public long seconds()
    080    {
    081        return milliseconds / MILLISECOND;
    082    }
    083
    084    /**
    085     * Converts the milliseconds back into a string (compatible with {@link #TimeInterval(String)}).
    086     *
    087     * @since 5.2.0
    088     */
    089    public String toDescription()
    090    {
    091        StringBuilder builder = new StringBuilder();
    092
    093        String sep = "";
    094
    095        long remainder = milliseconds;
    096
    097        for (String key : UNIT_KEYS)
    098        {
    099            if (remainder == 0)
    100                break;
    101
    102            long value = UNITS.get(key);
    103
    104            long units = remainder / value;
    105
    106            if (units > 0)
    107            {
    108                builder.append(sep);
    109                builder.append(units);
    110                builder.append(key);
    111
    112                sep = " ";
    113
    114                remainder = remainder % value;
    115            }
    116        }
    117
    118        return builder.toString();
    119    }
    120
    121    static long parseMilliseconds(String input)
    122    {
    123        long milliseconds = 0l;
    124
    125        Matcher matcher = PATTERN.matcher(input);
    126
    127        matcher.useAnchoringBounds(true);
    128
    129        // TODO: Notice non matching characters and reject input, including at end
    130
    131        int lastMatchEnd = -1;
    132
    133        while (matcher.find())
    134        {
    135            int start = matcher.start();
    136
    137            if (lastMatchEnd + 1 < start)
    138            {
    139                String invalid = input.substring(lastMatchEnd + 1, start);
    140                throw new RuntimeException(String.format("Unexpected string '%s' (in time interval '%s').", invalid, input));
    141            }
    142
    143            lastMatchEnd = matcher.end();
    144
    145            long count = Long.parseLong(matcher.group(1));
    146            String units = matcher.group(2);
    147
    148            if (units.length() == 0)
    149            {
    150                milliseconds += count;
    151                continue;
    152            }
    153
    154            Long unitValue = UNITS.get(units);
    155
    156            if (unitValue == null)
    157                throw new RuntimeException(String.format("Unknown time interval unit '%s' (in '%s').  Defined units: %s.", units, input, InternalCommonsUtils.joinSorted(UNITS.keySet())));
    158
    159            milliseconds += count * unitValue;
    160        }
    161
    162        if (lastMatchEnd + 1 < input.length())
    163        {
    164            String invalid = input.substring(lastMatchEnd + 1);
    165            throw new RuntimeException(String.format("Unexpected string '%s' (in time interval '%s').", invalid, input));
    166        }
    167
    168        return milliseconds;
    169    }
    170
    171    @Override
    172    public String toString()
    173    {
    174        return String.format("TimeInterval[%d ms]", milliseconds);
    175    }
    176
    177    @Override
    178    public boolean equals(Object obj)
    179    {
    180        if (obj == null)
    181            return false;
    182
    183        if (obj instanceof TimeInterval)
    184        {
    185            TimeInterval tp = (TimeInterval) obj;
    186
    187            return milliseconds == tp.milliseconds;
    188        }
    189
    190        return false;
    191    }
    192}