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}