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}