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}