org.eclipse.jgit.util.GitDateParser.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.jgit.util.GitDateParser.java

Source

/*
 * 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.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();
    }
}