1 /* 2 * Copyright (C) 2012 Christian Halstrick 3 * and other copyright owners as documented in the project's IP log. 4 * 5 * This program and the accompanying materials are made available 6 * under the terms of the Eclipse Distribution License v1.0 which 7 * accompanies this distribution, is reproduced below, and is 8 * available at http://www.eclipse.org/org/documents/edl-v10.php 9 * 10 * All rights reserved. 11 * 12 * Redistribution and use in source and binary forms, with or 13 * without modification, are permitted provided that the following 14 * conditions are met: 15 * 16 * - Redistributions of source code must retain the above copyright 17 * notice, this list of conditions and the following disclaimer. 18 * 19 * - Redistributions in binary form must reproduce the above 20 * copyright notice, this list of conditions and the following 21 * disclaimer in the documentation and/or other materials provided 22 * with the distribution. 23 * 24 * - Neither the name of the Eclipse Foundation, Inc. nor the 25 * names of its contributors may be used to endorse or promote 26 * products derived from this software without specific prior 27 * written permission. 28 * 29 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 30 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 31 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 32 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 33 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 34 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 35 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 36 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 37 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 38 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 39 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 40 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 41 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 42 */ 43 package org.eclipse.jgit.util; 44 45 import java.text.MessageFormat; 46 import java.text.ParseException; 47 import java.text.SimpleDateFormat; 48 import java.util.Calendar; 49 import java.util.Date; 50 import java.util.GregorianCalendar; 51 import java.util.HashMap; 52 import java.util.Locale; 53 import java.util.Map; 54 55 import org.eclipse.jgit.internal.JGitText; 56 57 /** 58 * Parses strings with time and date specifications into {@link java.util.Date}. 59 * 60 * When git needs to parse strings specified by the user this parser can be 61 * used. One example is the parsing of the config parameter gc.pruneexpire. The 62 * parser can handle only subset of what native gits approxidate parser 63 * understands. 64 */ 65 public class GitDateParser { 66 /** 67 * The Date representing never. Though this is a concrete value, most 68 * callers are adviced to avoid depending on the actual value. 69 */ 70 public static final Date NEVER = new Date(Long.MAX_VALUE); 71 72 // Since SimpleDateFormat instances are expensive to instantiate they should 73 // be cached. Since they are also not threadsafe they are cached using 74 // ThreadLocal. 75 private static ThreadLocal<Map<Locale, Map<ParseableSimpleDateFormat, SimpleDateFormat>>> formatCache = 76 new ThreadLocal<Map<Locale, Map<ParseableSimpleDateFormat, SimpleDateFormat>>>() { 77 78 @Override 79 protected Map<Locale, Map<ParseableSimpleDateFormat, SimpleDateFormat>> initialValue() { 80 return new HashMap<>(); 81 } 82 }; 83 84 // Gets an instance of a SimpleDateFormat for the specified locale. If there 85 // is not already an appropriate instance in the (ThreadLocal) cache then 86 // create one and put it into the cache. 87 private static SimpleDateFormat getDateFormat(ParseableSimpleDateFormat f, 88 Locale locale) { 89 Map<Locale, Map<ParseableSimpleDateFormat, SimpleDateFormat>> cache = formatCache 90 .get(); 91 Map<ParseableSimpleDateFormat, SimpleDateFormat> map = cache 92 .get(locale); 93 if (map == null) { 94 map = new HashMap<>(); 95 cache.put(locale, map); 96 return getNewSimpleDateFormat(f, locale, map); 97 } 98 SimpleDateFormat dateFormat = map.get(f); 99 if (dateFormat != null) 100 return dateFormat; 101 SimpleDateFormat df = getNewSimpleDateFormat(f, locale, map); 102 return df; 103 } 104 105 private static SimpleDateFormat getNewSimpleDateFormat( 106 ParseableSimpleDateFormat f, Locale locale, 107 Map<ParseableSimpleDateFormat, SimpleDateFormat> map) { 108 SimpleDateFormat df = SystemReader.getInstance().getSimpleDateFormat( 109 f.formatStr, locale); 110 map.put(f, df); 111 return df; 112 } 113 114 // An enum of all those formats which this parser can parse with the help of 115 // a SimpleDateFormat. There are other formats (e.g. the relative formats 116 // like "yesterday" or "1 week ago") which this parser can parse but which 117 // are not listed here because they are parsed without the help of a 118 // SimpleDateFormat. 119 enum ParseableSimpleDateFormat { 120 ISO("yyyy-MM-dd HH:mm:ss Z"), // //$NON-NLS-1$ 121 RFC("EEE, dd MMM yyyy HH:mm:ss Z"), // //$NON-NLS-1$ 122 SHORT("yyyy-MM-dd"), // //$NON-NLS-1$ 123 SHORT_WITH_DOTS_REVERSE("dd.MM.yyyy"), // //$NON-NLS-1$ 124 SHORT_WITH_DOTS("yyyy.MM.dd"), // //$NON-NLS-1$ 125 SHORT_WITH_SLASH("MM/dd/yyyy"), // //$NON-NLS-1$ 126 DEFAULT("EEE MMM dd HH:mm:ss yyyy Z"), // //$NON-NLS-1$ 127 LOCAL("EEE MMM dd HH:mm:ss yyyy"); //$NON-NLS-1$ 128 129 String formatStr; 130 131 private ParseableSimpleDateFormat(String formatStr) { 132 this.formatStr = formatStr; 133 } 134 } 135 136 /** 137 * Parses a string into a {@link java.util.Date} using the default locale. 138 * Since this parser also supports relative formats (e.g. "yesterday") the 139 * caller can specify the reference date. These types of strings can be 140 * parsed: 141 * <ul> 142 * <li>"never"</li> 143 * <li>"now"</li> 144 * <li>"yesterday"</li> 145 * <li>"(x) years|months|weeks|days|hours|minutes|seconds ago"<br> 146 * Multiple specs can be combined like in "2 weeks 3 days ago". Instead of ' 147 * ' one can use '.' to seperate the words</li> 148 * <li>"yyyy-MM-dd HH:mm:ss Z" (ISO)</li> 149 * <li>"EEE, dd MMM yyyy HH:mm:ss Z" (RFC)</li> 150 * <li>"yyyy-MM-dd"</li> 151 * <li>"yyyy.MM.dd"</li> 152 * <li>"MM/dd/yyyy",</li> 153 * <li>"dd.MM.yyyy"</li> 154 * <li>"EEE MMM dd HH:mm:ss yyyy Z" (DEFAULT)</li> 155 * <li>"EEE MMM dd HH:mm:ss yyyy" (LOCAL)</li> 156 * </ul> 157 * 158 * @param dateStr 159 * the string to be parsed 160 * @param now 161 * the base date which is used for the calculation of relative 162 * formats. E.g. if baseDate is "25.8.2012" then parsing of the 163 * string "1 week ago" would result in a date corresponding to 164 * "18.8.2012". This is used when a JGit command calls this 165 * parser often but wants a consistent starting point for 166 * calls.<br> 167 * If set to <code>null</code> then the current time will be used 168 * instead. 169 * @return the parsed {@link java.util.Date} 170 * @throws java.text.ParseException 171 * if the given dateStr was not recognized 172 */ 173 public static Date parse(String dateStr, Calendar now) 174 throws ParseException { 175 return parse(dateStr, now, Locale.getDefault()); 176 } 177 178 /** 179 * Parses a string into a {@link java.util.Date} using the given locale. 180 * Since this parser also supports relative formats (e.g. "yesterday") the 181 * caller can specify the reference date. These types of strings can be 182 * parsed: 183 * <ul> 184 * <li>"never"</li> 185 * <li>"now"</li> 186 * <li>"yesterday"</li> 187 * <li>"(x) years|months|weeks|days|hours|minutes|seconds ago"<br> 188 * Multiple specs can be combined like in "2 weeks 3 days ago". Instead of ' 189 * ' one can use '.' to seperate the words</li> 190 * <li>"yyyy-MM-dd HH:mm:ss Z" (ISO)</li> 191 * <li>"EEE, dd MMM yyyy HH:mm:ss Z" (RFC)</li> 192 * <li>"yyyy-MM-dd"</li> 193 * <li>"yyyy.MM.dd"</li> 194 * <li>"MM/dd/yyyy",</li> 195 * <li>"dd.MM.yyyy"</li> 196 * <li>"EEE MMM dd HH:mm:ss yyyy Z" (DEFAULT)</li> 197 * <li>"EEE MMM dd HH:mm:ss yyyy" (LOCAL)</li> 198 * </ul> 199 * 200 * @param dateStr 201 * the string to be parsed 202 * @param now 203 * the base date which is used for the calculation of relative 204 * formats. E.g. if baseDate is "25.8.2012" then parsing of the 205 * string "1 week ago" would result in a date corresponding to 206 * "18.8.2012". This is used when a JGit command calls this 207 * parser often but wants a consistent starting point for 208 * calls.<br> 209 * If set to <code>null</code> then the current time will be used 210 * instead. 211 * @param locale 212 * locale to be used to parse the date string 213 * @return the parsed {@link java.util.Date} 214 * @throws java.text.ParseException 215 * if the given dateStr was not recognized 216 * @since 3.2 217 */ 218 public static Date parse(String dateStr, Calendar now, Locale locale) 219 throws ParseException { 220 dateStr = dateStr.trim(); 221 Date ret; 222 223 if ("never".equalsIgnoreCase(dateStr)) //$NON-NLS-1$ 224 return NEVER; 225 ret = parse_relative(dateStr, now); 226 if (ret != null) 227 return ret; 228 for (ParseableSimpleDateFormat f : ParseableSimpleDateFormat.values()) { 229 try { 230 return parse_simple(dateStr, f, locale); 231 } catch (ParseException e) { 232 // simply proceed with the next parser 233 } 234 } 235 ParseableSimpleDateFormat[] values = ParseableSimpleDateFormat.values(); 236 StringBuilder allFormats = new StringBuilder("\"") //$NON-NLS-1$ 237 .append(values[0].formatStr); 238 for (int i = 1; i < values.length; i++) 239 allFormats.append("\", \"").append(values[i].formatStr); //$NON-NLS-1$ 240 allFormats.append("\""); //$NON-NLS-1$ 241 throw new ParseException(MessageFormat.format( 242 JGitText.get().cannotParseDate, dateStr, allFormats.toString()), 0); 243 } 244 245 // tries to parse a string with the formats supported by SimpleDateFormat 246 private static Date parse_simple(String dateStr, 247 ParseableSimpleDateFormat f, Locale locale) 248 throws ParseException { 249 SimpleDateFormat dateFormat = getDateFormat(f, locale); 250 dateFormat.setLenient(false); 251 return dateFormat.parse(dateStr); 252 } 253 254 // tries to parse a string with a relative time specification 255 private static Date parse_relative(String dateStr, Calendar now) { 256 Calendar cal; 257 SystemReader sysRead = SystemReader.getInstance(); 258 259 // check for the static words "yesterday" or "now" 260 if ("now".equals(dateStr)) { //$NON-NLS-1$ 261 return ((now == null) ? new Date(sysRead.getCurrentTime()) : now 262 .getTime()); 263 } 264 265 if (now == null) { 266 cal = new GregorianCalendar(sysRead.getTimeZone(), 267 sysRead.getLocale()); 268 cal.setTimeInMillis(sysRead.getCurrentTime()); 269 } else 270 cal = (Calendar) now.clone(); 271 272 if ("yesterday".equals(dateStr)) { //$NON-NLS-1$ 273 cal.add(Calendar.DATE, -1); 274 cal.set(Calendar.HOUR_OF_DAY, 0); 275 cal.set(Calendar.MINUTE, 0); 276 cal.set(Calendar.SECOND, 0); 277 cal.set(Calendar.MILLISECOND, 0); 278 cal.set(Calendar.MILLISECOND, 0); 279 return cal.getTime(); 280 } 281 282 // parse constructs like "3 days ago", "5.week.2.day.ago" 283 String[] parts = dateStr.split("\\.| "); //$NON-NLS-1$ 284 int partsLength = parts.length; 285 // check we have an odd number of parts (at least 3) and that the last 286 // part is "ago" 287 if (partsLength < 3 || (partsLength & 1) == 0 288 || !"ago".equals(parts[parts.length - 1])) //$NON-NLS-1$ 289 return null; 290 int number; 291 for (int i = 0; i < parts.length - 2; i += 2) { 292 try { 293 number = Integer.parseInt(parts[i]); 294 } catch (NumberFormatException e) { 295 return null; 296 } 297 if ("year".equals(parts[i + 1]) || "years".equals(parts[i + 1])) //$NON-NLS-1$ //$NON-NLS-2$ 298 cal.add(Calendar.YEAR, -number); 299 else if ("month".equals(parts[i + 1]) //$NON-NLS-1$ 300 || "months".equals(parts[i + 1])) //$NON-NLS-1$ 301 cal.add(Calendar.MONTH, -number); 302 else if ("week".equals(parts[i + 1]) //$NON-NLS-1$ 303 || "weeks".equals(parts[i + 1])) //$NON-NLS-1$ 304 cal.add(Calendar.WEEK_OF_YEAR, -number); 305 else if ("day".equals(parts[i + 1]) || "days".equals(parts[i + 1])) //$NON-NLS-1$ //$NON-NLS-2$ 306 cal.add(Calendar.DATE, -number); 307 else if ("hour".equals(parts[i + 1]) //$NON-NLS-1$ 308 || "hours".equals(parts[i + 1])) //$NON-NLS-1$ 309 cal.add(Calendar.HOUR_OF_DAY, -number); 310 else if ("minute".equals(parts[i + 1]) //$NON-NLS-1$ 311 || "minutes".equals(parts[i + 1])) //$NON-NLS-1$ 312 cal.add(Calendar.MINUTE, -number); 313 else if ("second".equals(parts[i + 1]) //$NON-NLS-1$ 314 || "seconds".equals(parts[i + 1])) //$NON-NLS-1$ 315 cal.add(Calendar.SECOND, -number); 316 else 317 return null; 318 } 319 return cal.getTime(); 320 } 321 }