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}