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