View Javadoc
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 }