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}