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 }