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