GitDateParser.java

  1. /*
  2.  * Copyright (C) 2012 Christian Halstrick and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */
  10. package org.eclipse.jgit.util;

  11. import java.text.MessageFormat;
  12. import java.text.ParseException;
  13. import java.text.SimpleDateFormat;
  14. import java.util.Calendar;
  15. import java.util.Date;
  16. import java.util.GregorianCalendar;
  17. import java.util.HashMap;
  18. import java.util.Locale;
  19. import java.util.Map;

  20. import org.eclipse.jgit.internal.JGitText;

  21. /**
  22.  * Parses strings with time and date specifications into {@link java.util.Date}.
  23.  *
  24.  * When git needs to parse strings specified by the user this parser can be
  25.  * used. One example is the parsing of the config parameter gc.pruneexpire. The
  26.  * parser can handle only subset of what native gits approxidate parser
  27.  * understands.
  28.  */
  29. public class GitDateParser {
  30.     /**
  31.      * The Date representing never. Though this is a concrete value, most
  32.      * callers are adviced to avoid depending on the actual value.
  33.      */
  34.     public static final Date NEVER = new Date(Long.MAX_VALUE);

  35.     // Since SimpleDateFormat instances are expensive to instantiate they should
  36.     // be cached. Since they are also not threadsafe they are cached using
  37.     // ThreadLocal.
  38.     private static ThreadLocal<Map<Locale, Map<ParseableSimpleDateFormat, SimpleDateFormat>>> formatCache =
  39.             new ThreadLocal<>() {

  40.         @Override
  41.         protected Map<Locale, Map<ParseableSimpleDateFormat, SimpleDateFormat>> initialValue() {
  42.             return new HashMap<>();
  43.         }
  44.     };

  45.     // Gets an instance of a SimpleDateFormat for the specified locale. If there
  46.     // is not already an appropriate instance in the (ThreadLocal) cache then
  47.     // create one and put it into the cache.
  48.     private static SimpleDateFormat getDateFormat(ParseableSimpleDateFormat f,
  49.             Locale locale) {
  50.         Map<Locale, Map<ParseableSimpleDateFormat, SimpleDateFormat>> cache = formatCache
  51.                 .get();
  52.         Map<ParseableSimpleDateFormat, SimpleDateFormat> map = cache
  53.                 .get(locale);
  54.         if (map == null) {
  55.             map = new HashMap<>();
  56.             cache.put(locale, map);
  57.             return getNewSimpleDateFormat(f, locale, map);
  58.         }
  59.         SimpleDateFormat dateFormat = map.get(f);
  60.         if (dateFormat != null)
  61.             return dateFormat;
  62.         SimpleDateFormat df = getNewSimpleDateFormat(f, locale, map);
  63.         return df;
  64.     }

  65.     private static SimpleDateFormat getNewSimpleDateFormat(
  66.             ParseableSimpleDateFormat f, Locale locale,
  67.             Map<ParseableSimpleDateFormat, SimpleDateFormat> map) {
  68.         SimpleDateFormat df = SystemReader.getInstance().getSimpleDateFormat(
  69.                 f.formatStr, locale);
  70.         map.put(f, df);
  71.         return df;
  72.     }

  73.     // An enum of all those formats which this parser can parse with the help of
  74.     // a SimpleDateFormat. There are other formats (e.g. the relative formats
  75.     // like "yesterday" or "1 week ago") which this parser can parse but which
  76.     // are not listed here because they are parsed without the help of a
  77.     // SimpleDateFormat.
  78.     enum ParseableSimpleDateFormat {
  79.         ISO("yyyy-MM-dd HH:mm:ss Z"), // //$NON-NLS-1$
  80.         RFC("EEE, dd MMM yyyy HH:mm:ss Z"), // //$NON-NLS-1$
  81.         SHORT("yyyy-MM-dd"), // //$NON-NLS-1$
  82.         SHORT_WITH_DOTS_REVERSE("dd.MM.yyyy"), // //$NON-NLS-1$
  83.         SHORT_WITH_DOTS("yyyy.MM.dd"), // //$NON-NLS-1$
  84.         SHORT_WITH_SLASH("MM/dd/yyyy"), // //$NON-NLS-1$
  85.         DEFAULT("EEE MMM dd HH:mm:ss yyyy Z"), // //$NON-NLS-1$
  86.         LOCAL("EEE MMM dd HH:mm:ss yyyy"); //$NON-NLS-1$

  87.         private final String formatStr;

  88.         private ParseableSimpleDateFormat(String formatStr) {
  89.             this.formatStr = formatStr;
  90.         }
  91.     }

  92.     /**
  93.      * Parses a string into a {@link java.util.Date} using the default locale.
  94.      * Since this parser also supports relative formats (e.g. "yesterday") the
  95.      * caller can specify the reference date. These types of strings can be
  96.      * parsed:
  97.      * <ul>
  98.      * <li>"never"</li>
  99.      * <li>"now"</li>
  100.      * <li>"yesterday"</li>
  101.      * <li>"(x) years|months|weeks|days|hours|minutes|seconds ago"<br>
  102.      * Multiple specs can be combined like in "2 weeks 3 days ago". Instead of '
  103.      * ' one can use '.' to separate the words</li>
  104.      * <li>"yyyy-MM-dd HH:mm:ss Z" (ISO)</li>
  105.      * <li>"EEE, dd MMM yyyy HH:mm:ss Z" (RFC)</li>
  106.      * <li>"yyyy-MM-dd"</li>
  107.      * <li>"yyyy.MM.dd"</li>
  108.      * <li>"MM/dd/yyyy",</li>
  109.      * <li>"dd.MM.yyyy"</li>
  110.      * <li>"EEE MMM dd HH:mm:ss yyyy Z" (DEFAULT)</li>
  111.      * <li>"EEE MMM dd HH:mm:ss yyyy" (LOCAL)</li>
  112.      * </ul>
  113.      *
  114.      * @param dateStr
  115.      *            the string to be parsed
  116.      * @param now
  117.      *            the base date which is used for the calculation of relative
  118.      *            formats. E.g. if baseDate is "25.8.2012" then parsing of the
  119.      *            string "1 week ago" would result in a date corresponding to
  120.      *            "18.8.2012". This is used when a JGit command calls this
  121.      *            parser often but wants a consistent starting point for
  122.      *            calls.<br>
  123.      *            If set to <code>null</code> then the current time will be used
  124.      *            instead.
  125.      * @return the parsed {@link java.util.Date}
  126.      * @throws java.text.ParseException
  127.      *             if the given dateStr was not recognized
  128.      */
  129.     public static Date parse(String dateStr, Calendar now)
  130.             throws ParseException {
  131.         return parse(dateStr, now, Locale.getDefault());
  132.     }

  133.     /**
  134.      * Parses a string into a {@link java.util.Date} using the given locale.
  135.      * Since this parser also supports relative formats (e.g. "yesterday") the
  136.      * caller can specify the reference date. These types of strings can be
  137.      * parsed:
  138.      * <ul>
  139.      * <li>"never"</li>
  140.      * <li>"now"</li>
  141.      * <li>"yesterday"</li>
  142.      * <li>"(x) years|months|weeks|days|hours|minutes|seconds ago"<br>
  143.      * Multiple specs can be combined like in "2 weeks 3 days ago". Instead of '
  144.      * ' one can use '.' to separate the words</li>
  145.      * <li>"yyyy-MM-dd HH:mm:ss Z" (ISO)</li>
  146.      * <li>"EEE, dd MMM yyyy HH:mm:ss Z" (RFC)</li>
  147.      * <li>"yyyy-MM-dd"</li>
  148.      * <li>"yyyy.MM.dd"</li>
  149.      * <li>"MM/dd/yyyy",</li>
  150.      * <li>"dd.MM.yyyy"</li>
  151.      * <li>"EEE MMM dd HH:mm:ss yyyy Z" (DEFAULT)</li>
  152.      * <li>"EEE MMM dd HH:mm:ss yyyy" (LOCAL)</li>
  153.      * </ul>
  154.      *
  155.      * @param dateStr
  156.      *            the string to be parsed
  157.      * @param now
  158.      *            the base date which is used for the calculation of relative
  159.      *            formats. E.g. if baseDate is "25.8.2012" then parsing of the
  160.      *            string "1 week ago" would result in a date corresponding to
  161.      *            "18.8.2012". This is used when a JGit command calls this
  162.      *            parser often but wants a consistent starting point for
  163.      *            calls.<br>
  164.      *            If set to <code>null</code> then the current time will be used
  165.      *            instead.
  166.      * @param locale
  167.      *            locale to be used to parse the date string
  168.      * @return the parsed {@link java.util.Date}
  169.      * @throws java.text.ParseException
  170.      *             if the given dateStr was not recognized
  171.      * @since 3.2
  172.      */
  173.     public static Date parse(String dateStr, Calendar now, Locale locale)
  174.             throws ParseException {
  175.         dateStr = dateStr.trim();
  176.         Date ret;

  177.         if ("never".equalsIgnoreCase(dateStr)) //$NON-NLS-1$
  178.             return NEVER;
  179.         ret = parse_relative(dateStr, now);
  180.         if (ret != null)
  181.             return ret;
  182.         for (ParseableSimpleDateFormat f : ParseableSimpleDateFormat.values()) {
  183.             try {
  184.                 return parse_simple(dateStr, f, locale);
  185.             } catch (ParseException e) {
  186.                 // simply proceed with the next parser
  187.             }
  188.         }
  189.         ParseableSimpleDateFormat[] values = ParseableSimpleDateFormat.values();
  190.         StringBuilder allFormats = new StringBuilder("\"") //$NON-NLS-1$
  191.                 .append(values[0].formatStr);
  192.         for (int i = 1; i < values.length; i++)
  193.             allFormats.append("\", \"").append(values[i].formatStr); //$NON-NLS-1$
  194.         allFormats.append("\""); //$NON-NLS-1$
  195.         throw new ParseException(MessageFormat.format(
  196.                 JGitText.get().cannotParseDate, dateStr, allFormats.toString()), 0);
  197.     }

  198.     // tries to parse a string with the formats supported by SimpleDateFormat
  199.     private static Date parse_simple(String dateStr,
  200.             ParseableSimpleDateFormat f, Locale locale)
  201.             throws ParseException {
  202.         SimpleDateFormat dateFormat = getDateFormat(f, locale);
  203.         dateFormat.setLenient(false);
  204.         return dateFormat.parse(dateStr);
  205.     }

  206.     // tries to parse a string with a relative time specification
  207.     @SuppressWarnings("nls")
  208.     private static Date parse_relative(String dateStr, Calendar now) {
  209.         Calendar cal;
  210.         SystemReader sysRead = SystemReader.getInstance();

  211.         // check for the static words "yesterday" or "now"
  212.         if ("now".equals(dateStr)) {
  213.             return ((now == null) ? new Date(sysRead.getCurrentTime()) : now
  214.                     .getTime());
  215.         }

  216.         if (now == null) {
  217.             cal = new GregorianCalendar(sysRead.getTimeZone(),
  218.                     sysRead.getLocale());
  219.             cal.setTimeInMillis(sysRead.getCurrentTime());
  220.         } else
  221.             cal = (Calendar) now.clone();

  222.         if ("yesterday".equals(dateStr)) {
  223.             cal.add(Calendar.DATE, -1);
  224.             cal.set(Calendar.HOUR_OF_DAY, 0);
  225.             cal.set(Calendar.MINUTE, 0);
  226.             cal.set(Calendar.SECOND, 0);
  227.             cal.set(Calendar.MILLISECOND, 0);
  228.             cal.set(Calendar.MILLISECOND, 0);
  229.             return cal.getTime();
  230.         }

  231.         // parse constructs like "3 days ago", "5.week.2.day.ago"
  232.         String[] parts = dateStr.split("\\.| ");
  233.         int partsLength = parts.length;
  234.         // check we have an odd number of parts (at least 3) and that the last
  235.         // part is "ago"
  236.         if (partsLength < 3 || (partsLength & 1) == 0
  237.                 || !"ago".equals(parts[parts.length - 1]))
  238.             return null;
  239.         int number;
  240.         for (int i = 0; i < parts.length - 2; i += 2) {
  241.             try {
  242.                 number = Integer.parseInt(parts[i]);
  243.             } catch (NumberFormatException e) {
  244.                 return null;
  245.             }
  246.             if (parts[i + 1] == null){
  247.                 return null;
  248.             }
  249.             switch (parts[i + 1]) {
  250.             case "year":
  251.             case "years":
  252.                 cal.add(Calendar.YEAR, -number);
  253.                 break;
  254.             case "month":
  255.             case "months":
  256.                 cal.add(Calendar.MONTH, -number);
  257.                 break;
  258.             case "week":
  259.             case "weeks":
  260.                 cal.add(Calendar.WEEK_OF_YEAR, -number);
  261.                 break;
  262.             case "day":
  263.             case "days":
  264.                 cal.add(Calendar.DATE, -number);
  265.                 break;
  266.             case "hour":
  267.             case "hours":
  268.                 cal.add(Calendar.HOUR_OF_DAY, -number);
  269.                 break;
  270.             case "minute":
  271.             case "minutes":
  272.                 cal.add(Calendar.MINUTE, -number);
  273.                 break;
  274.             case "second":
  275.             case "seconds":
  276.                 cal.add(Calendar.SECOND, -number);
  277.                 break;
  278.             default:
  279.                 return null;
  280.             }
  281.         }
  282.         return cal.getTime();
  283.     }
  284. }