diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitDateParserTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitDateParserTest.java new file mode 100644 index 000000000..11e2e513c --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitDateParserTest.java @@ -0,0 +1,228 @@ +/* + * 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); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateParser.java new file mode 100644 index 000000000..04ef553b0 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateParser.java @@ -0,0 +1,233 @@ +/* + * 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> formatCache = new ThreadLocal>() { + protected Map initialValue() { + return new HashMap(); + } + }; + + // 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 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: + *
    + *
  • "now"
  • + *
  • "yesterday"
  • + *
  • "(x) years|months|weeks|days|hours|minutes|seconds ago"
    + * Multiple specs can be combined like in "2 weeks 3 days ago". Instead of + * ' ' one can use '.' to seperate the words
  • + *
  • "yyyy-MM-dd HH:mm:ss Z" (ISO)
  • + *
  • "EEE, dd MMM yyyy HH:mm:ss Z" (RFC)
  • + *
  • "yyyy-MM-dd"
  • + *
  • "yyyy.MM.dd"
  • + *
  • "MM/dd/yyyy",
  • + *
  • "dd.MM.yyyy"
  • + *
  • "EEE MMM dd HH:mm:ss yyyy Z" (DEFAULT)
  • + *
  • "EEE MMM dd HH:mm:ss yyyy" (LOCAL)
  • + *
+ * + * @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.
+ * If set to null then the current time will be used + * instead. + * @return the parsed {@link Date} or null 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(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java index 7cddccc29..e9d9953f4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java @@ -68,7 +68,7 @@ *

*/ public abstract class SystemReader { - private static SystemReader INSTANCE = new SystemReader() { + private static SystemReader DEFAULT = new SystemReader() { private volatile String hostname; public String getenv(String variable) { @@ -128,6 +128,8 @@ public int getTimezone(long when) { } }; + private static SystemReader INSTANCE = DEFAULT; + /** @return the live instance to read system properties. */ public static SystemReader getInstance() { return INSTANCE; @@ -138,7 +140,10 @@ public static SystemReader getInstance() { * the new instance to use when accessing properties. */ public static void setInstance(SystemReader newReader) { - INSTANCE = newReader; + if (newReader == null) + INSTANCE = DEFAULT; + else + INSTANCE = newReader; } /**