| /* |
| * Copyright (C) 2012 Christian Halstrick and others |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Distribution License v. 1.0 which is available at |
| * https://www.eclipse.org/org/documents/edl-v10.php. |
| * |
| * SPDX-License-Identifier: BSD-3-Clause |
| */ |
| package org.eclipse.jgit.util; |
| |
| import java.text.MessageFormat; |
| 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.Locale; |
| import java.util.Map; |
| |
| import org.eclipse.jgit.internal.JGitText; |
| |
| /** |
| * Parses strings with time and date specifications into {@link java.util.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 { |
| /** |
| * The Date representing never. Though this is a concrete value, most |
| * callers are adviced to avoid depending on the actual value. |
| */ |
| public static final Date NEVER = new Date(Long.MAX_VALUE); |
| |
| // 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<Locale, Map<ParseableSimpleDateFormat, SimpleDateFormat>>> formatCache = |
| new ThreadLocal<Map<Locale, Map<ParseableSimpleDateFormat, SimpleDateFormat>>>() { |
| |
| @Override |
| protected Map<Locale, Map<ParseableSimpleDateFormat, SimpleDateFormat>> initialValue() { |
| return new HashMap<>(); |
| } |
| }; |
| |
| // Gets an instance of a SimpleDateFormat for the specified locale. If there |
| // is not already an appropriate instance in the (ThreadLocal) cache then |
| // create one and put it into the cache. |
| private static SimpleDateFormat getDateFormat(ParseableSimpleDateFormat f, |
| Locale locale) { |
| Map<Locale, Map<ParseableSimpleDateFormat, SimpleDateFormat>> cache = formatCache |
| .get(); |
| Map<ParseableSimpleDateFormat, SimpleDateFormat> map = cache |
| .get(locale); |
| if (map == null) { |
| map = new HashMap<>(); |
| cache.put(locale, map); |
| return getNewSimpleDateFormat(f, locale, map); |
| } |
| SimpleDateFormat dateFormat = map.get(f); |
| if (dateFormat != null) |
| return dateFormat; |
| SimpleDateFormat df = getNewSimpleDateFormat(f, locale, map); |
| return df; |
| } |
| |
| private static SimpleDateFormat getNewSimpleDateFormat( |
| ParseableSimpleDateFormat f, Locale locale, |
| Map<ParseableSimpleDateFormat, SimpleDateFormat> map) { |
| SimpleDateFormat df = SystemReader.getInstance().getSimpleDateFormat( |
| f.formatStr, locale); |
| 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"), // //$NON-NLS-1$ |
| RFC("EEE, dd MMM yyyy HH:mm:ss Z"), // //$NON-NLS-1$ |
| SHORT("yyyy-MM-dd"), // //$NON-NLS-1$ |
| SHORT_WITH_DOTS_REVERSE("dd.MM.yyyy"), // //$NON-NLS-1$ |
| SHORT_WITH_DOTS("yyyy.MM.dd"), // //$NON-NLS-1$ |
| SHORT_WITH_SLASH("MM/dd/yyyy"), // //$NON-NLS-1$ |
| DEFAULT("EEE MMM dd HH:mm:ss yyyy Z"), // //$NON-NLS-1$ |
| LOCAL("EEE MMM dd HH:mm:ss yyyy"); //$NON-NLS-1$ |
| |
| private final String formatStr; |
| |
| private ParseableSimpleDateFormat(String formatStr) { |
| this.formatStr = formatStr; |
| } |
| } |
| |
| /** |
| * Parses a string into a {@link java.util.Date} using the default locale. |
| * 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>"never"</li> |
| * <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 separate 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 java.util.Date} |
| * @throws java.text.ParseException |
| * if the given dateStr was not recognized |
| */ |
| public static Date parse(String dateStr, Calendar now) |
| throws ParseException { |
| return parse(dateStr, now, Locale.getDefault()); |
| } |
| |
| /** |
| * Parses a string into a {@link java.util.Date} using the given locale. |
| * 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>"never"</li> |
| * <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 separate 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. |
| * @param locale |
| * locale to be used to parse the date string |
| * @return the parsed {@link java.util.Date} |
| * @throws java.text.ParseException |
| * if the given dateStr was not recognized |
| * @since 3.2 |
| */ |
| public static Date parse(String dateStr, Calendar now, Locale locale) |
| throws ParseException { |
| dateStr = dateStr.trim(); |
| Date ret; |
| |
| if ("never".equalsIgnoreCase(dateStr)) //$NON-NLS-1$ |
| return NEVER; |
| ret = parse_relative(dateStr, now); |
| if (ret != null) |
| return ret; |
| for (ParseableSimpleDateFormat f : ParseableSimpleDateFormat.values()) { |
| try { |
| return parse_simple(dateStr, f, locale); |
| } catch (ParseException e) { |
| // simply proceed with the next parser |
| } |
| } |
| ParseableSimpleDateFormat[] values = ParseableSimpleDateFormat.values(); |
| StringBuilder allFormats = new StringBuilder("\"") //$NON-NLS-1$ |
| .append(values[0].formatStr); |
| for (int i = 1; i < values.length; i++) |
| allFormats.append("\", \"").append(values[i].formatStr); //$NON-NLS-1$ |
| allFormats.append("\""); //$NON-NLS-1$ |
| throw new ParseException(MessageFormat.format( |
| JGitText.get().cannotParseDate, dateStr, allFormats.toString()), 0); |
| } |
| |
| // tries to parse a string with the formats supported by SimpleDateFormat |
| private static Date parse_simple(String dateStr, |
| ParseableSimpleDateFormat f, Locale locale) |
| throws ParseException { |
| SimpleDateFormat dateFormat = getDateFormat(f, locale); |
| dateFormat.setLenient(false); |
| return dateFormat.parse(dateStr); |
| } |
| |
| // tries to parse a string with a relative time specification |
| @SuppressWarnings("nls") |
| 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 (parts[i + 1] == null){ |
| return null; |
| } |
| switch (parts[i + 1]) { |
| case "year": |
| case "years": |
| cal.add(Calendar.YEAR, -number); |
| break; |
| case "month": |
| case "months": |
| cal.add(Calendar.MONTH, -number); |
| break; |
| case "week": |
| case "weeks": |
| cal.add(Calendar.WEEK_OF_YEAR, -number); |
| break; |
| case "day": |
| case "days": |
| cal.add(Calendar.DATE, -number); |
| break; |
| case "hour": |
| case "hours": |
| cal.add(Calendar.HOUR_OF_DAY, -number); |
| break; |
| case "minute": |
| case "minutes": |
| cal.add(Calendar.MINUTE, -number); |
| break; |
| case "second": |
| case "seconds": |
| cal.add(Calendar.SECOND, -number); |
| break; |
| default: |
| return null; |
| } |
| } |
| return cal.getTime(); |
| } |
| } |