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 org.apache.tapestry5.ioc.internal.util.CollectionFactory;
018import org.apache.tapestry5.ioc.internal.util.InternalUtils;
019
020import java.util.Map;
021import java.util.regex.Matcher;
022import java.util.regex.Pattern;
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, InternalUtils.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}