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.temporal.TemporalAccessor;
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.List;
022import java.util.Objects;
023
024import javax.annotation.Nullable;
025import javax.validation.constraints.NotNull;
026
027/**
028 * Supports multiple {@link DateTimeParser} that are considered ambiguous. Two {@link DateTimeParser} are considered
029 * ambiguous when they can potentially produce 2 different {@link TemporalAccessor}.
030 * e.g. "dd/MM/yyyy" and "MM/dd/yyyy"
031 *
032 * <p>This class will try all the parsers and keep the all the successful results.
033 *
034 * <p>This class is thread-safe once an instance is created.
035 */
036public class DateTimeMultiParser {
037
038  private final DateTimeParser preferred;
039  private final List<DateTimeParser> otherParsers;
040  private final List<DateTimeParser> allParsers;
041
042  /**
043   * Create a new instance of {@link DateTimeMultiParser}.
044   * @param parsers requires more than 1 element in list
045   */
046  DateTimeMultiParser(@NotNull List<DateTimeParser> parsers){
047    this(null, parsers);
048  }
049
050  /**
051   *
052   * Create a new instance of {@link DateTimeMultiParser}.
053   * At least 2 {@link DateTimeParser} must be provided see details on parameters.
054   *
055   * @param preferred the preferred {@link DateTimeParser} or null
056   * @param otherParsers list of {@link DateTimeParser} containing more than 1 element if no
057   *                     preferred {@link DateTimeParser} is provided. Otherwise, the list must contain at least 1 element.
058   */
059  DateTimeMultiParser(@Nullable DateTimeParser preferred, @NotNull List<DateTimeParser> otherParsers) {
060    Objects.requireNonNull(otherParsers, "otherParsers list can not be null");
061    PreconditionUtils.checkArgument(otherParsers.size() > 0, "otherParsers must contain at least 1 element");
062
063    if (preferred == null) {
064      PreconditionUtils.checkArgument(otherParsers.size() > 1, "If no preferred DateTimeParser is provided, " +
065              "the otherParsers list must contain more than 1 element");
066    }
067
068    this.preferred = preferred;
069    this.otherParsers = new ArrayList<>(otherParsers);
070
071    List<DateTimeParser> resultList = new ArrayList<>();
072
073    if (preferred != null) {
074      resultList.add(preferred);
075    }
076    resultList.addAll(otherParsers);
077
078    this.allParsers = Collections.unmodifiableList(resultList);
079  }
080
081  /**
082   * Get the list of all parsers: the preferred (if specified in the constructor) + otherParsers.
083   *
084   * @return never null
085   */
086  public List<DateTimeParser> getAllParsers(){
087    return allParsers;
088  }
089
090  /**
091   * Try to parse the input using all the parsers specified in the constructor.
092   *
093   * @param input
094   * @return {@link MultipleParseResult} instance, never null.
095   */
096  public MultipleParseResult parse(String input) {
097
098    int numberParsed = 0;
099    TemporalAccessor lastParsed = null;
100    TemporalAccessor preferredResult = null;
101    List<String> usedFormats = new ArrayList<>();
102
103    // lazily initialized assuming it should not be used most of the time
104    List<TemporalAccessor> otherResults = null;
105    for (DateTimeParser currParser : otherParsers) {
106      lastParsed = currParser.parse(input);
107      if (lastParsed != null) {
108        numberParsed++;
109        if (otherResults == null) {
110          otherResults = new ArrayList<>();
111        }
112        otherResults.add(lastParsed);
113        usedFormats.add(currParser.getOrdering().name());
114      }
115    }
116
117    // try the preferred DateTimeParser
118    if (this.preferred != null) {
119      lastParsed = this.preferred.parse(input);
120      if (lastParsed != null) {
121        numberParsed++;
122        preferredResult = lastParsed;
123      }
124    }
125
126    return new MultipleParseResult(numberParsed, usedFormats, preferredResult, otherResults);
127  }
128
129  /**
130   * Nested class representing the result of a multi-parse.
131   */
132  public static class MultipleParseResult {
133    private int numberParsed;
134    private TemporalAccessor preferredResult;
135    private List<TemporalAccessor> otherResults;
136    public List<String> formats;
137
138    public MultipleParseResult(int numberParsed, List<String> formats, TemporalAccessor preferredResult, List<TemporalAccessor> otherResults) {
139      this.numberParsed = numberParsed;
140      this.formats = formats;
141      this.preferredResult = preferredResult;
142      this.otherResults = otherResults;
143    }
144
145    public int getNumberParsed() {
146      return numberParsed;
147    }
148
149    public List<String> getFormats() {
150      return formats;
151    }
152
153    public TemporalAccessor getPreferredResult() {
154      return preferredResult;
155    }
156
157    public List<TemporalAccessor> getOtherResults() {
158      return otherResults;
159    }
160
161    /**
162     * Return the preferredResult if available otherwise the first element of otherResults.
163     * If otherResults is empty, null is returned.
164     */
165    public TemporalAccessor getResult() {
166      if (preferredResult != null) {
167        return preferredResult;
168      }
169
170      if (otherResults != null && otherResults.size() > 0) {
171        return otherResults.get(0);
172      }
173      return null;
174    }
175  }
176}