001package org.gbif.api.util;
002
003import java.text.DateFormat;
004import java.text.ParseException;
005import java.text.ParsePosition;
006import java.text.SimpleDateFormat;
007import java.util.Calendar;
008import java.util.Date;
009import java.util.regex.Pattern;
010
011import com.google.common.base.Strings;
012import com.google.common.collect.Range;
013
014import static org.gbif.api.model.common.search.SearchConstants.QUERY_WILDCARD;
015import static org.gbif.api.util.SearchTypeValidator.buildRange;
016
017/**
018 * Utility class that parses date values, the allowed date formats are: "yyyy-MM-dd", "yyyy-MM" or "yyyy".
019 */
020public class IsoDateParsingUtils {
021  private static Pattern SIMPLE_ISO_PATTERN = Pattern.compile("\\d{4}(?:-\\d{2}(?:-\\d{2})?)?");
022  /**
023   * Enumerations with the allowed date formats by the occurrence search service.
024   */
025  public enum IsoDateFormat {
026    FULL("yyyy-MM-dd"),
027    YEAR_MONTH("yyyy-MM"),
028    YEAR("yyyy");
029
030    private final String datePattern;
031
032
033    /**
034     * Private constructors.
035     * Receives a pattern and creates DateFormat instance with the pattern parameter.
036     */
037    private IsoDateFormat(String datePattern) {
038      this.datePattern = datePattern;
039    }
040
041    /**
042     * @return the dateFormat that parses and formats date values.
043     */
044    public DateFormat getDateFormat() {
045      DateFormat dateFormat = new SimpleDateFormat(datePattern);
046      dateFormat.setLenient(false);
047      return dateFormat;
048    }
049
050    /**
051     * Checks if the parameter "value" can be parsed using the date format.
052     */
053    public boolean isValidDate(String value) {
054      try {
055        return (parseDate(value) != null);
056      } catch (ParseException e) {
057        return false;
058      }
059    }
060
061    /**
062     * Try to parse a string with the current date format.
063     */
064    public Date parseDate(String value) throws ParseException {
065      ParsePosition position = new ParsePosition(0);
066      Date date = getDateFormat().parse(value, position);
067      if (position.getIndex() != value.length()) {
068        throw new ParseException(value + " is not a valid date", position.getIndex());
069      }
070      return date;
071    }
072  }
073
074  /**
075   * Default private constructor.
076   */
077  private IsoDateParsingUtils() {
078    // empty block
079  }
080
081
082  /**
083   * Iterates over all the OccurrenceDateFormat's and returns the first one that parses successfully the value.
084   * @throws IllegalArgumentException in case of unparsable dates
085   */
086  public static IsoDateFormat getFirstDateFormatMatch(String value) throws IllegalArgumentException {
087    // at least 4 digits for a year must exist
088    if (SIMPLE_ISO_PATTERN.matcher(value).find()) {
089      for (IsoDateFormat dateFormat : IsoDateFormat.values()) {
090        if (dateFormat.isValidDate(value)) {
091          return dateFormat;
092        }
093      }
094    }
095    throw new IllegalArgumentException(value + " is not a valid date");
096  }
097
098  /**
099   * Parses a single date value. The date is interpreted using the first available date format that successfully parses
100   * the value.
101   * @return the lowest possible date according to the precision given or null in case of *
102   * @throws IllegalArgumentException in case of unparsable dates
103   */
104  public static Date parseDate(String value) {
105    if (Strings.isNullOrEmpty(value)) {
106      throw new IllegalArgumentException("Date parameter can't be null or empty");
107    }
108    try {
109      return getFirstDateFormatMatch(value).parseDate(value);
110
111    } catch (ParseException e) {
112      throw new IllegalArgumentException(String.format("{} is not a valid date parameter", value));
113
114    } catch (IllegalArgumentException e) {
115      // could be a wildcard
116      if (QUERY_WILDCARD.equals(value)) {
117        return null;
118      }
119      throw e;
120    }
121  }
122
123  /**
124   * Parses a range of dates. The date format used is the first date format that successfully parses the lower range
125   * limit.
126   */
127  public static Range<Date> parseDateRange(String value) {
128    if (Strings.isNullOrEmpty(value)) {
129      throw new IllegalArgumentException("Date parameter can't be null or empty");
130    }
131    final String[] dateValues = value.split(",");
132    if (dateValues.length != 2) {
133      throw new IllegalArgumentException("Date value must be a single value or a range");
134    }
135
136    final Date lowerDate = parseDate(dateValues[0]);
137
138    Date upperDate = parseDate(dateValues[1]);
139    // in case we have a real upper date check its precision and use the highest possible date, not lowest
140    if (upperDate != null) {
141      final IsoDateFormat upperDateFormat = getFirstDateFormatMatch(dateValues[1]);
142      if (upperDateFormat == IsoDateFormat.YEAR_MONTH) {
143        upperDate = toLastDayOfMonth(upperDate);
144
145      } else if (upperDateFormat == IsoDateFormat.YEAR) {
146        upperDate = toLastDayOfYear(upperDate);
147      }
148    }
149
150    return buildRange(lowerDate, upperDate);
151  }
152
153  /**
154   * Calculates the last day of the month for the date parameter and return it a new date instance.
155   */
156  public static Date toLastDayOfMonth(Date value) {
157    return toLastDayOf(value, Calendar.DAY_OF_MONTH);
158  }
159
160  /**
161   * Calculates the last day of the year for the date parameter and return it a new date instance.
162   */
163  public static Date toLastDayOfYear(Date value) {
164    return toLastDayOf(value, Calendar.DAY_OF_YEAR);
165  }
166
167  /**
168   * Calculates the last day of a year or month for the date parameter and return it a new date instance.
169   */
170  public static Date toLastDayOf(Date value, IsoDateFormat isoDateFormat) {
171    if (IsoDateFormat.YEAR_MONTH == isoDateFormat) {
172      return toLastDayOf(value, Calendar.DAY_OF_MONTH);
173    } else if(IsoDateFormat.YEAR == isoDateFormat){
174      return toLastDayOf(value, Calendar.DAY_OF_YEAR);
175    }
176    return value;
177  }
178
179  /**
180   * Gets the actual maximum value for a field of a date value.
181   */
182  private static Date toLastDayOf(Date value, int field) {
183    Calendar calendar = Calendar.getInstance();
184    calendar.setTime(value);
185    calendar.set(field, calendar.getActualMaximum(field));
186    return calendar.getTime();
187  }
188
189}