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}