--- /dev/null
+/*
+ * Copyright (C) 2012, Christian Halstrick
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.util;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+
+import org.eclipse.jgit.junit.MockSystemReader;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class GitDateParserTest {
+ @Before
+ public void setUp() {
+ MockSystemReader mockSystemReader = new MockSystemReader();
+ SystemReader.setInstance(mockSystemReader);
+ }
+
+ @Test
+ public void badlyFormatted() {
+ Calendar ref = new GregorianCalendar(SystemReader.getInstance()
+ .getTimeZone(), SystemReader.getInstance().getLocale());
+ Assert.assertNull(GitDateParser.parse("foo", ref));
+ Assert.assertNull(GitDateParser.parse("", ref));
+ Assert.assertNull(GitDateParser.parse("", null));
+ Assert.assertNull(GitDateParser.parse("1970", ref));
+ Assert.assertNull(GitDateParser.parse("3000.3000.3000", ref));
+ Assert.assertNull(GitDateParser.parse("3 yesterday ago", ref));
+ Assert.assertNull(GitDateParser.parse("now yesterday ago", ref));
+ Assert.assertNull(GitDateParser.parse("yesterdays", ref));
+ Assert.assertNull(GitDateParser.parse("3.day. 2.week.ago", ref));
+ Assert.assertNull(GitDateParser.parse("day ago", ref));
+ Assert.assertNull(GitDateParser.parse("Gra Feb 21 15:35:00 2007 +0100",
+ null));
+ Assert.assertNull(GitDateParser.parse("Sun Feb 21 15:35:00 2007 +0100",
+ null));
+ Assert.assertNull(GitDateParser.parse(
+ "Wed Feb 21 15:35:00 Grand +0100",
+ null));
+ }
+
+ @Test
+ public void yesterday() {
+ GregorianCalendar cal = new GregorianCalendar(SystemReader
+ .getInstance().getTimeZone(), SystemReader.getInstance()
+ .getLocale());
+ Date parse = GitDateParser.parse("yesterday", cal);
+ cal.add(Calendar.DATE, -1);
+ cal.set(Calendar.HOUR_OF_DAY, 0);
+ cal.set(Calendar.MINUTE, 0);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ Assert.assertEquals(cal.getTime(), parse);
+ }
+
+ @Test
+ public void now() throws ParseException {
+ String dateStr = "2007-02-21 15:35:00 +0100";
+ Date refDate = SystemReader.getInstance()
+ .getSimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").parse(dateStr);
+
+ GregorianCalendar cal = new GregorianCalendar(SystemReader
+ .getInstance().getTimeZone(), SystemReader.getInstance()
+ .getLocale());
+ cal.setTime(refDate);
+
+ Date parse = GitDateParser.parse("now", cal);
+ Assert.assertEquals(refDate, parse);
+ long t1 = SystemReader.getInstance().getCurrentTime();
+ parse = GitDateParser.parse("now", null);
+ long t2 = SystemReader.getInstance().getCurrentTime();
+ Assert.assertTrue(t2 >= parse.getTime() && parse.getTime() >= t1);
+ }
+
+ @Test
+ public void weeksAgo() throws ParseException {
+ String dateStr = "2007-02-21 15:35:00 +0100";
+ SimpleDateFormat df = SystemReader.getInstance()
+ .getSimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
+ Date refDate = df.parse(dateStr);
+ GregorianCalendar cal = new GregorianCalendar(SystemReader
+ .getInstance().getTimeZone(), SystemReader.getInstance()
+ .getLocale());
+ cal.setTime(refDate);
+
+ Date parse = GitDateParser.parse("2 weeks ago", cal);
+ Assert.assertEquals(df.parse("2007-02-07 15:35:00 +0100"), parse);
+ }
+
+ @Test
+ public void daysAndWeeksAgo() throws ParseException {
+ String dateStr = "2007-02-21 15:35:00 +0100";
+ SimpleDateFormat df = SystemReader.getInstance().getSimpleDateFormat(
+ "yyyy-MM-dd HH:mm:ss Z");
+ Date refDate = df.parse(dateStr);
+ GregorianCalendar cal = new GregorianCalendar(SystemReader
+ .getInstance().getTimeZone(), SystemReader.getInstance()
+ .getLocale());
+ cal.setTime(refDate);
+
+ Date parse = GitDateParser.parse("2 weeks ago", cal);
+ Assert.assertEquals(df.parse("2007-02-07 15:35:00 +0100"), parse);
+ parse = GitDateParser.parse("3 days 2 weeks ago", cal);
+ Assert.assertEquals(df.parse("2007-02-04 15:35:00 +0100"), parse);
+ parse = GitDateParser.parse("3.day.2.week.ago", cal);
+ Assert.assertEquals(df.parse("2007-02-04 15:35:00 +0100"), parse);
+ }
+
+ @Test
+ public void iso() throws ParseException {
+ String dateStr = "2007-02-21 15:35:00 +0100";
+ Date exp = SystemReader.getInstance()
+ .getSimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").parse(dateStr);
+ Date parse = GitDateParser.parse(dateStr, null);
+ Assert.assertEquals(exp, parse);
+ }
+
+ @Test
+ public void rfc() throws ParseException {
+ String dateStr = "Wed, 21 Feb 2007 15:35:00 +0100";
+ Date exp = SystemReader.getInstance()
+ .getSimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z")
+ .parse(dateStr);
+ Date parse = GitDateParser.parse(dateStr, null);
+ Assert.assertEquals(exp, parse);
+ }
+
+ @Test
+ public void shortFmt() throws ParseException {
+ String dateStr = "2007-02-21";
+ Date exp = SystemReader.getInstance().getSimpleDateFormat("yyyy-MM-dd")
+ .parse(dateStr);
+ Date parse = GitDateParser.parse(dateStr, null);
+ Assert.assertEquals(exp, parse);
+ }
+
+ @Test
+ public void shortWithDots() throws ParseException {
+ String dateStr = "2007.02.21";
+ Date exp = SystemReader.getInstance().getSimpleDateFormat("yyyy.MM.dd")
+ .parse(dateStr);
+ Date parse = GitDateParser.parse(dateStr, null);
+ Assert.assertEquals(exp, parse);
+ }
+
+ @Test
+ public void shortWithSlash() throws ParseException {
+ String dateStr = "02/21/2007";
+ Date exp = SystemReader.getInstance().getSimpleDateFormat("MM/dd/yyyy")
+ .parse(dateStr);
+ Date parse = GitDateParser.parse(dateStr, null);
+ Assert.assertEquals(exp, parse);
+ }
+
+ @Test
+ public void shortWithDotsReverse() throws ParseException {
+ String dateStr = "21.02.2007";
+ Date exp = SystemReader.getInstance().getSimpleDateFormat("dd.MM.yyyy")
+ .parse(dateStr);
+ Date parse = GitDateParser.parse(dateStr, null);
+ Assert.assertEquals(exp, parse);
+ }
+
+ @Test
+ public void defaultFmt() throws ParseException {
+ String dateStr = "Wed Feb 21 15:35:00 2007 +0100";
+ Date exp = SystemReader.getInstance()
+ .getSimpleDateFormat("EEE MMM dd HH:mm:ss yyyy Z")
+ .parse(dateStr);
+ Date parse = GitDateParser.parse(dateStr, null);
+ Assert.assertEquals(exp, parse);
+ }
+
+ @Test
+ public void local() throws ParseException {
+ String dateStr = "Wed Feb 21 15:35:00 2007";
+ Date exp = SystemReader.getInstance()
+ .getSimpleDateFormat("EEE MMM dd HH:mm:ss yyyy").parse(dateStr);
+ Date parse = GitDateParser.parse(dateStr, null);
+ Assert.assertEquals(exp, parse);
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2012 Christian Halstrick
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.util;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Parses strings with time and date specifications into {@link Date}.
+ *
+ * When git needs to parse strings specified by the user this parser can be
+ * used. One example is the parsing of the config parameter gc.pruneexpire. The
+ * parser can handle only subset of what native gits approxidate parser
+ * understands.
+ */
+public class GitDateParser {
+ // Since SimpleDateFormat instances are expensive to instantiate they should
+ // be cached. Since they are also not threadsafe they are cached using
+ // ThreadLocal.
+ private static ThreadLocal<Map<ParseableSimpleDateFormat, SimpleDateFormat>> formatCache = new ThreadLocal<Map<ParseableSimpleDateFormat, SimpleDateFormat>>() {
+ protected Map<ParseableSimpleDateFormat, SimpleDateFormat> initialValue() {
+ return new HashMap<ParseableSimpleDateFormat, SimpleDateFormat>();
+ }
+ };
+
+ // Gets an instance of a SimpleDateFormat. If there is not already an
+ // appropriate instance in the (ThreadLocal) cache the create one and put in
+ // into the cache
+ private static SimpleDateFormat getDateFormat(ParseableSimpleDateFormat f) {
+ Map<ParseableSimpleDateFormat, SimpleDateFormat> map = formatCache
+ .get();
+ SimpleDateFormat dateFormat = map.get(f);
+ if (dateFormat != null)
+ return dateFormat;
+ SimpleDateFormat df = SystemReader.getInstance().getSimpleDateFormat(
+ f.formatStr);
+ map.put(f, df);
+ return df;
+ }
+
+ // An enum of all those formats which this parser can parse with the help of
+ // a SimpleDateFormat. There are other formats (e.g. the relative formats
+ // like "yesterday" or "1 week ago") which this parser can parse but which
+ // are not listed here because they are parsed without the help of a
+ // SimpleDateFormat.
+ enum ParseableSimpleDateFormat {
+ ISO("yyyy-MM-dd HH:mm:ss Z"), //
+ RFC("EEE, dd MMM yyyy HH:mm:ss Z"), //
+ SHORT("yyyy-MM-dd"), //
+ SHORT_WITH_DOTS_REVERSE("dd.MM.yyyy"), //
+ SHORT_WITH_DOTS("yyyy.MM.dd"), //
+ SHORT_WITH_SLASH("MM/dd/yyyy"), //
+ DEFAULT("EEE MMM dd HH:mm:ss yyyy Z"), //
+ LOCAL("EEE MMM dd HH:mm:ss yyyy");
+
+ String formatStr;
+
+ private ParseableSimpleDateFormat(String formatStr) {
+ this.formatStr = formatStr;
+ }
+ }
+
+ /**
+ * Parses a string into a {@link Date}. Since this parser also supports
+ * relative formats (e.g. "yesterday") the caller can specify the reference
+ * date. These types of strings can be parsed:
+ * <ul>
+ * <li>"now"</li>
+ * <li>"yesterday"</li>
+ * <li>"(x) years|months|weeks|days|hours|minutes|seconds ago"<br>
+ * Multiple specs can be combined like in "2 weeks 3 days ago". Instead of
+ * ' ' one can use '.' to seperate the words</li>
+ * <li>"yyyy-MM-dd HH:mm:ss Z" (ISO)</li>
+ * <li>"EEE, dd MMM yyyy HH:mm:ss Z" (RFC)</li>
+ * <li>"yyyy-MM-dd"</li>
+ * <li>"yyyy.MM.dd"</li>
+ * <li>"MM/dd/yyyy",</li>
+ * <li>"dd.MM.yyyy"</li>
+ * <li>"EEE MMM dd HH:mm:ss yyyy Z" (DEFAULT)</li>
+ * <li>"EEE MMM dd HH:mm:ss yyyy" (LOCAL)</li>
+ * </ul>
+ *
+ * @param dateStr
+ * the string to be parsed
+ * @param now
+ * the base date which is used for the calculation of relative
+ * formats. E.g. if baseDate is "25.8.2012" then parsing of the
+ * string "1 week ago" would result in a date corresponding to
+ * "18.8.2012". This is used when a JGit command calls this
+ * parser often but wants a consistent starting point for calls.<br>
+ * If set to <code>null</code> then the current time will be used
+ * instead.
+ * @return the parsed {@link Date} or <code>null</code> if this string was
+ * not parseable.
+ */
+ public static Date parse(String dateStr, Calendar now) {
+ dateStr = dateStr.trim();
+ Date ret;
+ ret = parse_relative(dateStr, now);
+ if (ret != null)
+ return ret;
+ for (ParseableSimpleDateFormat f : ParseableSimpleDateFormat.values()) {
+ ret = parse_simple(dateStr, f);
+ if (ret != null)
+ return ret;
+ }
+ return null;
+ }
+
+ // tries to parse a string with the formats supported by SimpleDateFormat
+ private static Date parse_simple(String dateStr, ParseableSimpleDateFormat f) {
+ SimpleDateFormat dateFormat = getDateFormat(f);
+ try {
+ dateFormat.setLenient(false);
+ return dateFormat.parse(dateStr);
+ } catch (ParseException e) {
+ return null;
+ }
+ }
+
+ // tries to parse a string with a relative time specification
+ private static Date parse_relative(String dateStr, Calendar now) {
+ Calendar cal;
+ SystemReader sysRead = SystemReader.getInstance();
+
+ // check for the static words "yesterday" or "now"
+ if ("now".equals(dateStr)) {
+ return ((now == null) ? new Date(sysRead.getCurrentTime()) : now
+ .getTime());
+ }
+
+ if (now == null) {
+ cal = new GregorianCalendar(sysRead.getTimeZone(),
+ sysRead.getLocale());
+ cal.setTimeInMillis(sysRead.getCurrentTime());
+ } else
+ cal = (Calendar) now.clone();
+
+ if ("yesterday".equals(dateStr)) {
+ cal.add(Calendar.DATE, -1);
+ cal.set(Calendar.HOUR_OF_DAY, 0);
+ cal.set(Calendar.MINUTE, 0);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ return cal.getTime();
+ }
+
+ // parse constructs like "3 days ago", "5.week.2.day.ago"
+ String[] parts = dateStr.split("\\.| ");
+ int partsLength = parts.length;
+ // check we have an odd number of parts (at least 3) and that the last
+ // part is "ago"
+ if (partsLength < 3 || (partsLength & 1) == 0
+ || !"ago".equals(parts[parts.length - 1]))
+ return null;
+ int number;
+ for (int i = 0; i < parts.length - 2; i += 2) {
+ try {
+ number = Integer.parseInt(parts[i]);
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ if ("year".equals(parts[i + 1]) || "years".equals(parts[i + 1]))
+ cal.add(Calendar.YEAR, -number);
+ else if ("month".equals(parts[i + 1])
+ || "months".equals(parts[i + 1]))
+ cal.add(Calendar.MONTH, -number);
+ else if ("week".equals(parts[i + 1])
+ || "weeks".equals(parts[i + 1]))
+ cal.add(Calendar.WEEK_OF_YEAR, -number);
+ else if ("day".equals(parts[i + 1]) || "days".equals(parts[i + 1]))
+ cal.add(Calendar.DATE, -number);
+ else if ("hour".equals(parts[i + 1])
+ || "hours".equals(parts[i + 1]))
+ cal.add(Calendar.HOUR_OF_DAY, -number);
+ else if ("minute".equals(parts[i + 1])
+ || "minutes".equals(parts[i + 1]))
+ cal.add(Calendar.MINUTE, -number);
+ else if ("second".equals(parts[i + 1])
+ || "seconds".equals(parts[i + 1]))
+ cal.add(Calendar.SECOND, -number);
+ else
+ return null;
+ }
+ return cal.getTime();
+ }
+}