001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.marshall; 018 019import static org.apache.juneau.commons.reflect.ReflectionUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021import static org.apache.juneau.commons.utils.ThrowableUtils.*; 022 023import java.lang.reflect.*; 024import java.time.*; 025import java.time.chrono.*; 026import java.time.format.*; 027import java.time.temporal.*; 028import java.util.*; 029import java.util.concurrent.*; 030 031import org.apache.juneau.commons.reflect.*; 032import org.apache.juneau.marshall.swaps.*; 033 034/** 035 * Supported wire formats for {@link TemporalAccessor} values. 036 * 037 * <p> 038 * Used by {@link MarshallingContext.Builder#temporalFormat(TemporalFormat)}, 039 * {@link Marshalled#temporalFormat()}, 040 * {@link MarshalledProp#temporalFormat()}, and 041 * {@link MarshalledConfig#temporalFormat()} to control how 042 * {@link TemporalAccessor} values ({@link Instant}, {@link LocalDate}, {@link LocalDateTime}, 043 * {@link LocalTime}, {@link OffsetDateTime}, {@link OffsetTime}, {@link ZonedDateTime}, {@link Year}, 044 * {@link YearMonth}, {@link MonthDay}, …) are written to and read from the wire. 045 * 046 * <p> 047 * The default is {@link #DEFAULT} which uses the per-subtype default formatter 048 * (e.g. {@link DateTimeFormatter#ISO_INSTANT} for {@link Instant}, {@link DateTimeFormatter#ISO_LOCAL_DATE} 049 * for {@link LocalDate}, etc.) — preserving the historical wire output produced before this enum existed. 050 * 051 * <p> 052 * Numeric formats ({@link #MILLIS}) emit native numeric wire types on binary serializers 053 * (BSON / CBOR / MsgPack) and bare numbers on text formats. Text formats emit a quoted ISO 8601 string for 054 * every other constant. 055 * 056 * <p> 057 * Most subtypes implement {@link Temporal}, but {@link MonthDay} only implements {@link TemporalAccessor}. 058 * Format/parse methods accept any {@link TemporalAccessor} so that {@link MonthDay} round-trips through the 059 * same dispatch path as the {@link Temporal} subtypes. 060 */ 061@SuppressWarnings({ 062 "java:S3776", // formatter(), parse() and convertForFormatter() dispatch over the many java.time subtypes and format modes; splitting them would obscure the mapping 063 "unchecked" // parse() casts MonthDay.parse()/invokeFrom() results to T; they are only produced when targetType matches that subtype 064}) 065public enum TemporalFormat { 066 067 /** Sentinel meaning "no value configured" — falls through to the next-higher precedence level. */ 068 NOT_SET, 069 070 /** 071 * Per-subtype default formatter (the historical wire output). 072 * 073 * <p> 074 * Uses the ISO formatter most appropriate for each {@link Temporal} subtype: 075 * {@link DateTimeFormatter#ISO_INSTANT} for {@link Instant}, {@link DateTimeFormatter#ISO_LOCAL_DATE} 076 * for {@link LocalDate}, {@link DateTimeFormatter#ISO_OFFSET_DATE_TIME} for {@link ZonedDateTime} / 077 * {@link OffsetDateTime}, etc. 078 */ 079 DEFAULT, 080 081 /** {@link DateTimeFormatter#BASIC_ISO_DATE}: <js>"20111203"</js>. */ 082 BASIC_ISO_DATE, 083 084 /** {@link DateTimeFormatter#ISO_DATE}: <js>"2011-12-03+01:00"</js> or <js>"2011-12-03"</js>. */ 085 ISO_DATE, 086 087 /** {@link DateTimeFormatter#ISO_DATE_TIME}: <js>"2011-12-03T10:15:30+01:00[Europe/Paris]"</js>. */ 088 ISO_DATE_TIME, 089 090 /** {@link DateTimeFormatter#ISO_INSTANT}: <js>"2011-12-03T10:15:30Z"</js>. */ 091 ISO_INSTANT, 092 093 /** {@link DateTimeFormatter#ISO_LOCAL_DATE}: <js>"2011-12-03"</js>. */ 094 ISO_LOCAL_DATE, 095 096 /** {@link DateTimeFormatter#ISO_LOCAL_DATE_TIME}: <js>"2011-12-03T10:15:30"</js>. */ 097 ISO_LOCAL_DATE_TIME, 098 099 /** {@link DateTimeFormatter#ISO_LOCAL_TIME}: <js>"10:15:30"</js>. */ 100 ISO_LOCAL_TIME, 101 102 /** {@link DateTimeFormatter#ISO_OFFSET_DATE}: <js>"2011-12-03+01:00"</js>. */ 103 ISO_OFFSET_DATE, 104 105 /** {@link DateTimeFormatter#ISO_OFFSET_DATE_TIME}: <js>"2011-12-03T10:15:30+01:00"</js>. */ 106 ISO_OFFSET_DATE_TIME, 107 108 /** {@link DateTimeFormatter#ISO_OFFSET_TIME}: <js>"10:15:30+01:00"</js>. */ 109 ISO_OFFSET_TIME, 110 111 /** {@link DateTimeFormatter#ISO_ORDINAL_DATE}: <js>"2012-337"</js>. */ 112 ISO_ORDINAL_DATE, 113 114 /** {@link DateTimeFormatter#ISO_TIME}: <js>"10:15:30+01:00"</js> or <js>"10:15:30"</js>. */ 115 ISO_TIME, 116 117 /** {@link DateTimeFormatter#ISO_WEEK_DATE}: <js>"2012-W48-6"</js>. */ 118 ISO_WEEK_DATE, 119 120 /** Year only — pattern {@code "uuuu"}: <js>"2012"</js>. */ 121 ISO_YEAR, 122 123 /** Year and month — pattern {@code "uuuu-MM"}: <js>"2012-12"</js>. */ 124 ISO_YEAR_MONTH, 125 126 /** {@link DateTimeFormatter#ISO_ZONED_DATE_TIME}: <js>"2011-12-03T10:15:30+01:00[Europe/Paris]"</js>. */ 127 ISO_ZONED_DATE_TIME, 128 129 /** {@link DateTimeFormatter#RFC_1123_DATE_TIME}: <js>"Tue, 3 Jun 2008 11:05:30 GMT"</js>. */ 130 RFC_1123_DATE_TIME, 131 132 /** 133 * Epoch milliseconds as a numeric value. 134 * 135 * <p> 136 * Emitted as a native int64 by binary serializers (BSON / CBOR / MsgPack) and as a bare integer 137 * literal by text serializers. 138 * 139 * <p> 140 * <b>Per-subtype semantics (midnight UTC for non-instant subtypes):</b> 141 * <ul> 142 * <li>{@link Instant} — {@code instant.toEpochMilli()}. 143 * <li>{@link OffsetDateTime}, {@link ZonedDateTime} — {@code value.toInstant().toEpochMilli()}. 144 * <li>{@link LocalDateTime} — {@code ldt.toInstant(ZoneOffset.UTC).toEpochMilli()}. 145 * <li>{@link LocalDate} — {@code ld.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli()} (midnight UTC). 146 * <li>{@link YearMonth} — first day of the month at midnight UTC. 147 * <li>{@link Year} — January 1 at midnight UTC. 148 * <li>{@link LocalTime}, {@link OffsetTime}, {@link MonthDay} — fall back to the type's {@link #DEFAULT} 149 * (ISO string form) because no defensible epoch-millis interpretation exists without an associated 150 * date/year. See {@link #isMillisNumeric(Class)} for the canonical decision predicate. 151 * </ul> 152 * 153 * <p> 154 * <b>The asymmetry is intentional:</b> a configured {@code MILLIS} setting silently produces a string 155 * for {@link LocalTime} / {@link OffsetTime} / {@link MonthDay} and a number for every other subtype. 156 */ 157 MILLIS; 158 159 private static final ZoneId Z = ZoneId.of("Z"); 160 161 private static final DateTimeFormatter YEAR_FORMATTER = DateTimeFormatter.ofPattern("uuuu"); 162 private static final DateTimeFormatter YEAR_MONTH_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM"); 163 164 private static final Map<Class<?>,DateTimeFormatter> DEFAULT_FORMATTERS = Map.ofEntries( 165 Map.entry(Instant.class, DateTimeFormatter.ISO_INSTANT), 166 Map.entry(ZonedDateTime.class, DateTimeFormatter.ISO_OFFSET_DATE_TIME), 167 Map.entry(OffsetDateTime.class, DateTimeFormatter.ISO_OFFSET_DATE_TIME), 168 Map.entry(LocalDate.class, DateTimeFormatter.ISO_LOCAL_DATE), 169 Map.entry(LocalDateTime.class, DateTimeFormatter.ISO_LOCAL_DATE_TIME), 170 Map.entry(LocalTime.class, DateTimeFormatter.ISO_LOCAL_TIME), 171 Map.entry(OffsetTime.class, DateTimeFormatter.ISO_OFFSET_TIME), 172 Map.entry(Year.class, YEAR_FORMATTER), 173 Map.entry(YearMonth.class, YEAR_MONTH_FORMATTER) 174 ); 175 176 private static final Map<Class<? extends TemporalAccessor>,Method> FROM_METHODS = new ConcurrentHashMap<>(); 177 178 /** 179 * Returns <jk>true</jk> when {@link #MILLIS} produces a numeric wire string for the specified subtype. 180 * 181 * <p> 182 * Single source of truth for the {@code MILLIS} wire-type decision shared by {@link #format} and the 183 * marshalling-side {@code temporalSwap} / {@code temporalAccessorSwap} factories. The helper exists so 184 * the swap-side {@code Long.valueOf(s)} coercion only fires when {@link #format} actually emitted a 185 * numeric string — a mismatch on either side is a {@link NumberFormatException} waiting to happen. 186 * 187 * <p> 188 * Returns <jk>false</jk> for {@link LocalTime}, {@link OffsetTime}, and {@link MonthDay} — these 189 * subtypes have no defensible epoch-millis interpretation, so {@link #format}'s {@code MILLIS} branch 190 * falls back to {@link #DEFAULT} (an ISO string) for them and the swap must keep the value as a string. 191 * 192 * @param c The {@link TemporalAccessor} subtype to query. 193 * @return <jk>true</jk> if {@link #format} emits a {@link Long}-parseable numeric string for {@code c} 194 * under {@link #MILLIS}; <jk>false</jk> if the {@code MILLIS} branch falls back to ISO string. 195 */ 196 public static boolean isMillisNumeric(Class<? extends TemporalAccessor> c) { 197 return ! (LocalTime.class.equals(c) || OffsetTime.class.equals(c) || MonthDay.class.equals(c)); 198 } 199 200 /** 201 * Formats the specified value using this format. 202 * 203 * @param value The value to format. Can be <jk>null</jk> (returns <jk>null</jk>). 204 * @param zoneId The default zone id used for time-only / unzoned values. 205 * @return The formatted string, or <jk>null</jk> if {@code value} is <jk>null</jk>. 206 * For {@link #MILLIS} this is a numeric string for every subtype except {@link LocalTime}, 207 * {@link OffsetTime}, and {@link MonthDay} (which fall back to their {@link #DEFAULT} ISO 208 * string form). 209 */ 210 public String format(TemporalAccessor value, ZoneId zoneId) { 211 if (value == null) 212 return null; 213 // MonthDay only implements TemporalAccessor (not Temporal) and has no DateTimeFormatter that 214 // matches its --MM-DD wire form natively. Use its own toString/parse pair regardless of the 215 // configured format — every other TemporalFormat value is structurally meaningless without a year. 216 if (value instanceof MonthDay value2) 217 return value2.toString(); 218 if (this == MILLIS) { 219 Long millis = toEpochMillis(value); 220 if (millis != null) 221 return Long.toString(millis); 222 return DEFAULT.format(value, zoneId); 223 } 224 var zone = zoneId == null ? ZoneId.systemDefault() : zoneId; 225 var tc = value.getClass(); 226 if (!DEFAULT_FORMATTERS.containsKey(tc) && (this == DEFAULT || this == NOT_SET)) 227 return DateTimeFormatter.ISO_INSTANT.format(toInstant(value, zone)); 228 var coerced = convertForFormatter(value, zone); 229 return formatter(tc).format(coerced); 230 } 231 232 private static Instant toInstant(TemporalAccessor value, ZoneId zone) { 233 if (value instanceof Instant value2) 234 return value2; 235 try { 236 return Instant.from(value); 237 } catch (Exception e) { 238 if (value instanceof ChronoLocalDate value2) 239 return value2.atTime(LocalTime.MIDNIGHT).atZone(zone).toInstant(); 240 if (value instanceof ChronoLocalDateTime<?> value2) 241 return value2.atZone(zone).toInstant(); 242 if (value instanceof ChronoZonedDateTime<?> value2) 243 return value2.toInstant(); 244 throw e; 245 } 246 } 247 248 /** 249 * Parses the specified wire value using this format. 250 * 251 * @param <T> The temporal subtype. 252 * @param value The wire value. Can be <jk>null</jk> or blank (returns <jk>null</jk>). 253 * @param targetType The target temporal subtype. Must be either a {@link Temporal} subtype or 254 * {@link MonthDay} (the only non-{@link Temporal} {@link TemporalAccessor} the JDK 255 * currently exposes). 256 * @param zoneId The default zone id used for time-only / unzoned values. 257 * @return The parsed value, or <jk>null</jk> if {@code value} is <jk>null</jk> or blank. 258 */ 259 public <T extends TemporalAccessor> T parse(String value, Class<T> targetType, ZoneId zoneId) { 260 if (value == null) 261 return null; 262 var s = value.trim(); 263 if (s.isEmpty()) 264 return null; 265 var zone = zoneId == null ? ZoneId.systemDefault() : zoneId; 266 if (this == MILLIS) { 267 // MonthDay reverses what format() emits at MILLIS — its native --MM-DD shape; numeric input 268 // at MILLIS×MonthDay is unreachable on the round-trip path because format() emits a string. 269 if (MonthDay.class.equals(targetType)) 270 return (T) MonthDay.parse(s); 271 // LocalTime never had a defensible epoch-millis interpretation; legacy callers get DEFAULT.parse. 272 if (LocalTime.class.equals(targetType)) 273 return DEFAULT.parse(s, targetType, zone); 274 // OffsetTime: format() falls back to DEFAULT (ISO_OFFSET_TIME string). Be lenient on parse — 275 // route numeric input through fromEpochMillis (legacy lenient support) and route the 276 // ISO_OFFSET_TIME wire form back through DEFAULT.parse so the round-trip closes. 277 if (OffsetTime.class.equals(targetType) && ! isAllDigits(s)) 278 return DEFAULT.parse(s, targetType, zone); 279 return fromEpochMillis(Long.parseLong(s), targetType, zone); 280 } 281 // MonthDay's --MM-DD wire shape is its only stable representation; route to its native parser 282 // regardless of the configured format. 283 if (MonthDay.class.equals(targetType)) 284 return (T) MonthDay.parse(s); 285 // Lenient: epoch-millis numeric strings are accepted for DEFAULT/NOT_SET on instant-like subtypes only. 286 // Date-only subtypes (Year, YearMonth, LocalDate, MonthDay, LocalTime) use ISO patterns that are themselves 287 // numeric ("2012", "2012-12") and must not be reinterpreted as epoch millis. 288 if ((this == DEFAULT || this == NOT_SET) && isAllDigits(s) && isInstantLike(targetType)) 289 return fromEpochMillis(Long.parseLong(s), targetType, zone); 290 var offset = (targetType == Instant.class) ? Z : zone; 291 var ta = new DefaultingTemporalAccessor(formatter(targetType).parse(s), offset); 292 return (T) invokeFrom(targetType, ta); 293 } 294 295 private static boolean isInstantLike(Class<?> targetType) { 296 return Instant.class.equals(targetType) 297 || ZonedDateTime.class.equals(targetType) 298 || OffsetDateTime.class.equals(targetType) 299 || LocalDateTime.class.equals(targetType); 300 } 301 302 private static boolean isAllDigits(String s) { 303 var n = s.length(); 304 if (n == 0) 305 return false; 306 var i = 0; 307 if (s.charAt(0) == '-' || s.charAt(0) == '+') { 308 if (n == 1) 309 return false; 310 i = 1; 311 } 312 for (; i < n; i++) 313 if (!Character.isDigit(s.charAt(i))) 314 return false; 315 return true; 316 } 317 318 /** 319 * Returns <jk>true</jk> if this format is numeric on the wire. 320 * 321 * @return <jk>true</jk> if this format emits a numeric wire value. 322 */ 323 public boolean isNumeric() { 324 return this == MILLIS; 325 } 326 327 private static DateTimeFormatter defaultFormatter(Class<?> tc) { 328 var f = DEFAULT_FORMATTERS.get(tc); 329 return or(f, DateTimeFormatter.ISO_INSTANT); 330 } 331 332 @SuppressWarnings({ 333 "java:S1301" // switch over enum: explicit case constants keep the per-format mapping legible 334 }) 335 private DateTimeFormatter formatter(Class<?> tc) { 336 return switch (this) { 337 case NOT_SET, DEFAULT -> defaultFormatter(tc); 338 case BASIC_ISO_DATE -> DateTimeFormatter.BASIC_ISO_DATE; 339 case ISO_DATE -> DateTimeFormatter.ISO_DATE; 340 case ISO_DATE_TIME -> DateTimeFormatter.ISO_DATE_TIME; 341 case ISO_INSTANT -> DateTimeFormatter.ISO_INSTANT; 342 case ISO_LOCAL_DATE -> DateTimeFormatter.ISO_LOCAL_DATE; 343 case ISO_LOCAL_DATE_TIME -> DateTimeFormatter.ISO_LOCAL_DATE_TIME; 344 case ISO_LOCAL_TIME -> DateTimeFormatter.ISO_LOCAL_TIME; 345 case ISO_OFFSET_DATE -> DateTimeFormatter.ISO_OFFSET_DATE; 346 case ISO_OFFSET_DATE_TIME -> DateTimeFormatter.ISO_OFFSET_DATE_TIME; 347 case ISO_OFFSET_TIME -> DateTimeFormatter.ISO_OFFSET_TIME; 348 case ISO_ORDINAL_DATE -> DateTimeFormatter.ISO_ORDINAL_DATE; 349 case ISO_TIME -> DateTimeFormatter.ISO_TIME; 350 case ISO_WEEK_DATE -> DateTimeFormatter.ISO_WEEK_DATE; 351 case ISO_YEAR -> YEAR_FORMATTER; 352 case ISO_YEAR_MONTH -> YEAR_MONTH_FORMATTER; 353 case ISO_ZONED_DATE_TIME -> DateTimeFormatter.ISO_ZONED_DATE_TIME; 354 case RFC_1123_DATE_TIME -> DateTimeFormatter.RFC_1123_DATE_TIME; 355 case MILLIS -> throw new IllegalStateException("MILLIS does not use a DateTimeFormatter"); 356 }; 357 } 358 359 /** 360 * Returns whether the configured formatter accepts a missing zone, mirroring the historical 361 * {@code TemporalSwap.zoneOptional()} flag. 362 */ 363 @SuppressWarnings({ 364 "java:S6541" // exhaustive switch keeps the per-format zone-optional table explicit 365 }) 366 private boolean zoneOptional() { 367 return switch (this) { 368 case BASIC_ISO_DATE, ISO_DATE, ISO_DATE_TIME, ISO_LOCAL_DATE, ISO_LOCAL_DATE_TIME, ISO_LOCAL_TIME, 369 ISO_ORDINAL_DATE, ISO_TIME, ISO_WEEK_DATE, ISO_YEAR, ISO_YEAR_MONTH -> true; 370 default -> false; 371 }; 372 } 373 374 private Temporal convertForFormatter(TemporalAccessor value, ZoneId zoneId) { 375 if (value instanceof Temporal value2 && (this == DEFAULT || this == NOT_SET)) 376 return value2; 377 var tc = value.getClass(); 378 // Instant always serializes in GMT. 379 if (tc == Instant.class) 380 return ZonedDateTime.from(new DefaultingTemporalAccessor(value, Z)); 381 // Zoned / offset variants are already serializable by any pattern. 382 if (value instanceof Temporal value2 && (tc == ZonedDateTime.class || tc == OffsetDateTime.class)) 383 return value2; 384 if (zoneOptional()) { 385 if (value instanceof LocalDateTime value2) 386 return value2; 387 if (tc == OffsetTime.class) 388 return ZonedDateTime.from(new DefaultingTemporalAccessor(value, zoneId)); 389 return LocalDateTime.from(new DefaultingTemporalAccessor(value, zoneId)); 390 } 391 return ZonedDateTime.from(new DefaultingTemporalAccessor(value, zoneId)); 392 } 393 394 private static Long toEpochMillis(TemporalAccessor value) { 395 if (value instanceof Instant value2) 396 return value2.toEpochMilli(); 397 if (value instanceof OffsetDateTime value2) 398 return value2.toInstant().toEpochMilli(); 399 if (value instanceof ZonedDateTime value2) 400 return value2.toInstant().toEpochMilli(); 401 if (value instanceof LocalDateTime value2) 402 return value2.toInstant(ZoneOffset.UTC).toEpochMilli(); 403 if (value instanceof LocalDate value2) 404 return value2.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli(); 405 if (value instanceof YearMonth value2) 406 return value2.atDay(1).atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli(); 407 if (value instanceof Year value2) 408 return value2.atDay(1).atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli(); 409 return null; 410 } 411 412 private static <T extends TemporalAccessor> T fromEpochMillis(long millis, Class<T> targetType, ZoneId zoneId) { 413 var instant = Instant.ofEpochMilli(millis); 414 if (targetType == Instant.class) return (T) instant; 415 if (targetType == ZonedDateTime.class) return (T) instant.atZone(zoneId); 416 if (targetType == OffsetDateTime.class) return (T) instant.atZone(zoneId).toOffsetDateTime(); 417 if (targetType == LocalDateTime.class) return (T) instant.atZone(ZoneOffset.UTC).toLocalDateTime(); 418 if (targetType == LocalDate.class) return (T) instant.atZone(ZoneOffset.UTC).toLocalDate(); 419 if (targetType == OffsetTime.class) return (T) instant.atZone(zoneId).toOffsetDateTime().toOffsetTime(); 420 if (targetType == YearMonth.class) return (T) YearMonth.from(instant.atZone(ZoneOffset.UTC)); 421 if (targetType == Year.class) return (T) Year.from(instant.atZone(ZoneOffset.UTC)); 422 throw iaex("Cannot convert epoch millis to %s", targetType.getName()); 423 } 424 425 private static Method findFromMethod(Class<? extends TemporalAccessor> tc) { 426 // @formatter:off 427 return FROM_METHODS.computeIfAbsent(tc, k -> info(tc).getPublicMethod( 428 x -> x.isStatic() 429 && x.isNotDeprecated() 430 && x.hasName("from") 431 && x.hasReturnType(tc) 432 && x.hasParameterTypes(TemporalAccessor.class) 433 ) 434 .map(MethodInfo::inner) 435 .orElseThrow(() -> iaex("No static from(TemporalAccessor) on %s", tc.getName()))); 436 // @formatter:on 437 } 438 439 private static Object invokeFrom(Class<? extends TemporalAccessor> tc, TemporalAccessor ta) { 440 try { 441 return findFromMethod(tc).invoke(null, ta); 442 } catch (IllegalAccessException | InvocationTargetException e) { 443 throw toRex(e); 444 } 445 } 446}