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