001/*
002 * Licensed under the Apache License, Version 2.0 (the "License");
003 * you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at
005 *
006 *     http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software
009 * distributed under the License is distributed on an "AS IS" BASIS,
010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011 * See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014package org.gbif.common.parsers.date;
015
016import org.gbif.utils.PreconditionUtils;
017
018import java.time.Year;
019import java.time.ZoneId;
020import java.time.format.DateTimeFormatter;
021import java.time.format.DateTimeFormatterBuilder;
022import java.time.format.ResolverStyle;
023import java.time.temporal.ChronoField;
024import java.time.temporal.TemporalQuery;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.HashSet;
028import java.util.List;
029import java.util.Objects;
030import java.util.Set;
031
032import javax.validation.constraints.NotNull;
033
034import org.apache.commons.lang3.StringUtils;
035
036/**
037 * The DateTimeParserBuilder can build objects directly (build(..) methods) or return an instance
038 * of itself to create more complex object.
039 */
040public class DateTimeParserBuilder {
041
042  // The letter 'u' in all the patterns refers to YEAR as opposed to 'y' who refers to YEAR_OF_ERA
043  private final static String YEAR_2_DIGITS_PATTERN_SUFFIX = "uu";
044  private final static String IS_YEAR_2_DIGITS_PATTERN = "^.+[^u]"+YEAR_2_DIGITS_PATTERN_SUFFIX+"$";
045
046  private DateTimeParserBuilder() {}
047
048  /**
049   * Get a new builder to create a list of DateTimeParser.
050   */
051  public static ThreeTenDateParserListBuilder newParserListBuilder() {
052    return new ThreeTenDateParserListBuilder();
053  }
054
055  /**
056   * Get a new builder to create a list of DateTimeMultiParser.
057   */
058  public static ThreeTenDateMultiParserListBuilder newMultiParserListBuilder() {
059    return new ThreeTenDateMultiParserListBuilder();
060  }
061
062  /**
063   * Build a single, strict,  DateTimeParser.
064   */
065  private static DateTimeParser build(@NotNull String pattern, @NotNull DateComponentOrdering ordering,
066                                     @NotNull TemporalQuery<?> type) {
067    Objects.requireNonNull(type);
068    return build(pattern, ordering, new TemporalQuery[]{type});
069  }
070
071  /**
072   * Build a single, strict, DateTimeParser with a specific ZoneId.
073   */
074  private static DateTimeParser build(@NotNull String pattern, @NotNull DateComponentOrdering ordering,
075                                      @NotNull TemporalQuery<?> type, ZoneId zoneId) {
076    Objects.requireNonNull(type);
077    return build(pattern, ordering, new TemporalQuery[]{type}, zoneId);
078  }
079
080  /**
081   * Build a single, possibly lenient, DateTimeParser.
082   */
083  private static DateTimeParser build(@NotNull String pattern, @NotNull DateComponentOrdering ordering, @NotNull TemporalQuery<?>[] type) {
084    Objects.requireNonNull(pattern);
085    Objects.requireNonNull(ordering);
086
087    int minLength = getMinimumStringLengthForPattern(pattern);
088    DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern).withResolverStyle(ResolverStyle.STRICT);
089    return new DateTimeParser(dateTimeFormatter, null, ordering, type, minLength);
090  }
091
092  private static DateTimeParser build(@NotNull String pattern, @NotNull DateComponentOrdering ordering,
093                                      @NotNull TemporalQuery<?>[] type, ZoneId zoneId) {
094    Objects.requireNonNull(pattern);
095    Objects.requireNonNull(ordering);
096
097    int minLength = getMinimumStringLengthForPattern(pattern);
098    DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern).withZone(zoneId)
099            .withResolverStyle(ResolverStyle.STRICT);
100    return new DateTimeParser(dateTimeFormatter, null, ordering, type, minLength);
101  }
102
103  /**
104   * Build a single, strict, DateTimeParser with support for separator normalization.
105   */
106  private static DateTimeParser build(String pattern, DateComponentOrdering ordering, @NotNull TemporalQuery<?> type,
107                                      String separator, String alternativeSeparators) {
108    return build(pattern, ordering, new TemporalQuery[]{type}, separator, alternativeSeparators);
109  }
110
111  /**
112   * Build a single, possibly lenient, DateTimeParser with support for separator normalization.
113   */
114  private static DateTimeParser build(String pattern, DateComponentOrdering ordering, @NotNull TemporalQuery<?>[] type, String separator,
115                                      String alternativeSeparators) {
116    Objects.requireNonNull(pattern);
117    Objects.requireNonNull(ordering);
118    PreconditionUtils.checkArgument(StringUtils.isNotBlank(separator), "separator must NOT be blank");
119    PreconditionUtils.checkArgument(StringUtils.isNotBlank(alternativeSeparators), "alternativeSeparators must NOT be blank");
120
121    DateTimeSeparatorNormalizer dateTimeNormalizer = new DateTimeSeparatorNormalizer(alternativeSeparators, separator);
122    DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern).withResolverStyle(ResolverStyle.STRICT);
123    int minLength = getMinimumStringLengthForPattern(pattern);
124    return new DateTimeParser(dateTimeFormatter, dateTimeNormalizer, ordering, type, minLength);
125  }
126
127  /**
128   * Build a single DateTimeParser from a baseYear.
129   *
130   * @param pattern pattern that includes a 2 digits year (-uu)
131   */
132  private static DateTimeParser build(String pattern, DateComponentOrdering ordering, @NotNull TemporalQuery<?>[] type, Year baseYear) {
133    int minLength = getMinimumStringLengthForPattern(pattern);
134    DateTimeFormatter dateTimeFormatter = build2DigitsYearDateTimeFormatter(pattern, baseYear);
135    return new DateTimeParser(dateTimeFormatter, null, ordering, type, minLength);
136  }
137
138  /**
139   * Build a single DateTimeParser from a baseYear with support for separator normalization.
140   */
141  private static DateTimeParser build(String pattern, DateComponentOrdering ordering, @NotNull TemporalQuery<?>[] type, String separator,
142                                      String alternativeSeparators, Year baseYear) {
143    Objects.requireNonNull(pattern);
144    Objects.requireNonNull(ordering);
145    PreconditionUtils.checkArgument(StringUtils.isNotBlank(separator), "separator must NOT be blank");
146    PreconditionUtils.checkArgument(StringUtils.isNotBlank(alternativeSeparators), "alternativeSeparators must NOT be blank");
147
148    DateTimeSeparatorNormalizer dateTimeNormalizer = new DateTimeSeparatorNormalizer(alternativeSeparators, separator);
149    DateTimeFormatter dateTimeFormatter = build2DigitsYearDateTimeFormatter(pattern, baseYear);
150    int minLength = getMinimumStringLengthForPattern(pattern);
151    return new DateTimeParser(dateTimeFormatter, dateTimeNormalizer, ordering, type, minLength);
152  }
153
154  /**
155   * From a {@link }DateTimeFormatter} pattern in String, get the minimum String length required for an input String to apply
156   * the pattern. This is used to quickly discard DateTimeFormatter simply based on String length of the input.
157   * Minimum length is the length of the pattern String minus the optional section(s) and quotes.
158   */
159  private static int getMinimumStringLengthForPattern(String pattern) {
160    pattern = ThreeTenNumericalDateParser.OPTIONAL_PATTERN_PART.matcher(pattern).replaceAll("").replaceAll("'", "");
161    return pattern.length();
162  }
163
164  private static DateTimeFormatter build2DigitsYearDateTimeFormatter(String pattern, Year baseYear) {
165    PreconditionUtils.checkState(pattern.matches(IS_YEAR_2_DIGITS_PATTERN) || pattern.equals(YEAR_2_DIGITS_PATTERN_SUFFIX),
166            "build2DigitsYearDateTimeFormatter can only be used for patterns with 2 digit year");
167    pattern = StringUtils.removeEnd(pattern, YEAR_2_DIGITS_PATTERN_SUFFIX);
168    return new DateTimeFormatterBuilder().append(DateTimeFormatter.ofPattern(pattern))
169            .appendValueReduced(ChronoField.YEAR, 2, 2, baseYear.getValue()).parseStrict().toFormatter();
170  }
171
172  /**
173   * Builder used to build a List of {@link DateTimeParser}.
174   */
175  public static class ThreeTenDateParserListBuilder {
176    private final List<DateTimeParser> dateTimeParsers = new ArrayList<>();
177
178    /**
179     * @param type expected {@link TemporalQuery} from the provided pattern
180     */
181    public ThreeTenDateParserListBuilder appendDateTimeParser(String pattern, DateComponentOrdering ordering, TemporalQuery<?> type) {
182      dateTimeParsers.add(DateTimeParserBuilder.build(pattern, ordering, type));
183      return this;
184    }
185
186    /**
187     * @param type expected {@link TemporalQuery} from the provided pattern
188     */
189    public ThreeTenDateParserListBuilder appendDateTimeParser(String pattern, DateComponentOrdering ordering, TemporalQuery<?> type, ZoneId zoneId) {
190      dateTimeParsers.add(DateTimeParserBuilder.build(pattern, ordering, type, zoneId));
191      return this;
192    }
193
194    /**
195     * @param type possible {@link TemporalQuery} from the provided pattern (ordered)
196     */
197    public ThreeTenDateParserListBuilder appendDateTimeParser(String pattern, DateComponentOrdering ordering, TemporalQuery<?>[] type) {
198      dateTimeParsers.add(DateTimeParserBuilder.build(pattern, ordering, type));
199      return this;
200    }
201
202    /**
203     * @param alternativeSeparators separator used in the pattern that should be used as replacement for alternativeSeparators
204     */
205    public ThreeTenDateParserListBuilder appendDateTimeParser(String pattern, DateComponentOrdering ordering, TemporalQuery<?> type,
206                                                              String separator, String alternativeSeparators
207    ) {
208      dateTimeParsers.add(DateTimeParserBuilder.build(pattern, ordering, type, separator, alternativeSeparators));
209      return this;
210    }
211
212    /**
213     * @param alternativeSeparators separator used in the pattern that should be used as replacement for alternativeSeparators
214     */
215    public ThreeTenDateParserListBuilder appendDateTimeParser(String pattern, DateComponentOrdering ordering, TemporalQuery<?>[] type,
216                                                              String separator, String alternativeSeparators
217    ) {
218      dateTimeParsers.add(DateTimeParserBuilder.build(pattern, ordering, type, separator, alternativeSeparators));
219      return this;
220    }
221
222    public ThreeTenDateParserListBuilder append2DigitsYearDateTimeParser(String pattern, DateComponentOrdering ordering, TemporalQuery<?> type,
223                                                                         Year baseYear) {
224      dateTimeParsers.add(DateTimeParserBuilder.build(pattern, ordering, new TemporalQuery[]{type}, baseYear));
225      return this;
226    }
227
228    public ThreeTenDateParserListBuilder append2DigitsYearDateTimeParser(String pattern, DateComponentOrdering ordering, TemporalQuery<?> type,
229                                                                         String separator, String alternativeSeparators,
230                                                                         Year baseYear) {
231      dateTimeParsers.add(DateTimeParserBuilder.build(pattern, ordering, new TemporalQuery[]{type}, separator, alternativeSeparators, baseYear));
232      return this;
233    }
234
235    public List<DateTimeParser> build() {
236      return Collections.unmodifiableList(dateTimeParsers);
237    }
238  }
239
240  /**
241   * More specific builder Builder used to build a {@link DateTimeMultiParser}.
242   */
243  public static class ThreeTenDateMultiParserListBuilder {
244    private DateTimeParser preferred;
245    private List<DateTimeParser> otherParsers = new ArrayList<>();
246
247    public ThreeTenDateMultiParserListBuilder preferredDateTimeParser(String pattern, DateComponentOrdering ordering, TemporalQuery<?> type) {
248      preferred = DateTimeParserBuilder.build(pattern, ordering, type);
249      return this;
250    }
251
252    public ThreeTenDateMultiParserListBuilder preferredDateTimeParser(String pattern, DateComponentOrdering ordering, TemporalQuery<?> type, Year year) {
253      preferred = DateTimeParserBuilder.build(pattern, ordering, new TemporalQuery[]{type}, year);
254      return this;
255    }
256
257    public ThreeTenDateMultiParserListBuilder appendDateTimeParser(String pattern, DateComponentOrdering ordering, TemporalQuery<?> type) {
258      otherParsers.add(DateTimeParserBuilder.build(pattern, ordering, type));
259      return this;
260    }
261
262    public ThreeTenDateMultiParserListBuilder appendDateTimeFormatter(DateTimeFormatter dateTimeFormatter, DateComponentOrdering ordering, TemporalQuery<?> type, int minLength) {
263      otherParsers.add(new DateTimeParser(dateTimeFormatter, null, ordering, new TemporalQuery[]{type},  minLength));
264      return this;
265    }
266
267    public ThreeTenDateMultiParserListBuilder appendDateTimeParser(String pattern, DateComponentOrdering ordering, TemporalQuery<?> type, Year year) {
268      otherParsers.add(DateTimeParserBuilder.build(pattern, ordering, new TemporalQuery[]{type}, year));
269      return this;
270    }
271
272    public ThreeTenDateMultiParserListBuilder appendDateTimeParser(String pattern, DateComponentOrdering ordering, TemporalQuery<?> type,
273                                                                   String separator, String alternativeSeparators) {
274      otherParsers.add(DateTimeParserBuilder.build(pattern, ordering, new TemporalQuery[]{type}, separator, alternativeSeparators));
275      return this;
276    }
277
278    public ThreeTenDateMultiParserListBuilder appendDateTimeParser(String pattern, DateComponentOrdering ordering, TemporalQuery<?> type,
279                                                                   String separator, String alternativeSeparators, Year year) {
280      otherParsers.add(DateTimeParserBuilder.build(pattern, ordering, new TemporalQuery[]{type}, separator, alternativeSeparators, year));
281      return this;
282    }
283
284    /**
285     * Ensure the builder is used with content we expect.
286     * Currently (this could change) we should only have one DateTimeParser per DateComponentOrdering.
287     */
288    private void validate() throws IllegalStateException {
289      Set<DateComponentOrdering> orderings = new HashSet<>();
290      if(preferred != null) {
291        orderings.add(preferred.getOrdering());
292      }
293
294      for(DateTimeParser parser : otherParsers) {
295        if(!orderings.add(parser.getOrdering())) {
296          throw new IllegalStateException("DateComponentOrdering can only be used once in a DateTimeMultiParser." +
297                  "[" + parser.getOrdering() + "]");
298        }
299      }
300    }
301
302    public DateTimeMultiParser build() throws IllegalStateException {
303      validate();
304      return new DateTimeMultiParser(preferred, otherParsers);
305    }
306  }
307}