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.swaps;
018
019import static org.apache.juneau.commons.reflect.ReflectionUtils.*;
020import static org.apache.juneau.commons.utils.DateUtils.*;
021
022import java.lang.reflect.*;
023import java.time.*;
024import java.time.chrono.*;
025import java.time.format.*;
026import java.time.temporal.*;
027import java.util.*;
028import java.util.concurrent.*;
029
030import org.apache.juneau.*;
031import org.apache.juneau.commons.reflect.*;
032import org.apache.juneau.swap.*;
033
034/**
035 * Swap that converts {@link Temporal} objects to strings.
036 *
037 * <p>
038 * Uses the {@link DateTimeFormatter} class for converting {@link Temporal} objects to and from strings.
039 *
040 * <p>
041 * Supports any of the following temporal objects:
042 * <ul class='javatree'>
043 *    <li class='jc'>{@link HijrahDate}
044 *    <li class='jc'>{@link Instant}
045 *    <li class='jc'>{@link JapaneseDate}
046 *    <li class='jc'>{@link LocalDate}
047 *    <li class='jc'>{@link LocalDateTime}
048 *    <li class='jc'>{@link LocalTime}
049 *    <li class='jc'>{@link MinguoDate}
050 *    <li class='jc'>{@link OffsetDateTime}
051 *    <li class='jc'>{@link OffsetTime}
052 *    <li class='jc'>{@link ThaiBuddhistDate}
053 *    <li class='jc'>{@link Year}
054 *    <li class='jc'>{@link YearMonth}
055 *    <li class='jc'>{@link ZonedDateTime}
056 * </ul>
057 *
058 * <h5 class='section'>See Also:</h5><ul>
059 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/SwapBasics">Swap Basics</a>
060 * </ul>
061 */
062public class TemporalSwap extends StringSwap<Temporal> {
063
064   /**
065    * Default swap to {@link DateTimeFormatter#BASIC_ISO_DATE}.
066    * <p>
067    * Example: <js>"20111203"</js>
068    */
069   public static class BasicIsoDate extends TemporalSwap {
070
071      /** Default instance.*/
072      public static final TemporalSwap DEFAULT = new BasicIsoDate();
073
074      /** Constructor.*/
075      public BasicIsoDate() {
076         super("BASIC_ISO_DATE", true);
077      }
078   }
079
080   /**
081    * Default swap to {@link DateTimeFormatter#ISO_DATE}.
082    * <p>
083    * Example: <js>"2011-12-03+01:00"</js> or <js>"2011-12-03"</js>
084    */
085   public static class IsoDate extends TemporalSwap {
086
087      /** Default instance.*/
088      public static final TemporalSwap DEFAULT = new IsoDate();
089
090      /** Constructor.*/
091      public IsoDate() {
092         super("ISO_DATE", true);
093      }
094   }
095
096   /**
097    * Default swap to {@link DateTimeFormatter#ISO_DATE_TIME}.
098    * <p>
099    * Example: <js>"2011-12-03T10:15:30+01:00[Europe/Paris]"</js>
100    */
101   public static class IsoDateTime extends TemporalSwap {
102
103      /** Default instance.*/
104      public static final TemporalSwap DEFAULT = new IsoDateTime();
105
106      /** Constructor.*/
107      public IsoDateTime() {
108         super("ISO_DATE_TIME", true);
109      }
110   }
111
112   /**
113    * Default swap to {@link DateTimeFormatter#ISO_INSTANT}.
114    * <p>
115    * Example: <js>"2011-12-03T10:15:30Z"</js>
116    */
117   public static class IsoInstant extends TemporalSwap {
118
119      /** Default instance.*/
120      public static final TemporalSwap DEFAULT = new IsoInstant();
121
122      /** Constructor.*/
123      public IsoInstant() {
124         super("ISO_INSTANT", false);
125      }
126   }
127
128   /**
129    * Default swap to {@link DateTimeFormatter#ISO_LOCAL_DATE}.
130    * <p>
131    * Example: <js>"2011-12-03"</js>
132    */
133   public static class IsoLocalDate extends TemporalSwap {
134
135      /** Default instance.*/
136      public static final TemporalSwap DEFAULT = new IsoLocalDate();
137
138      /** Constructor.*/
139      public IsoLocalDate() {
140         super("ISO_LOCAL_DATE", false);
141      }
142   }
143
144   /**
145    * Default swap to {@link DateTimeFormatter#ISO_LOCAL_DATE_TIME}.
146    * <p>
147    * Example: <js>"2011-12-03T10:15:30"</js>
148    */
149   public static class IsoLocalDateTime extends TemporalSwap {
150
151      /** Default instance.*/
152      public static final TemporalSwap DEFAULT = new IsoLocalDateTime();
153
154      /** Constructor.*/
155      public IsoLocalDateTime() {
156         super("ISO_LOCAL_DATE_TIME", true);
157      }
158   }
159
160   /**
161    * Default swap to {@link DateTimeFormatter#ISO_LOCAL_TIME}.
162    * <p>
163    * Example: <js>"10:15:30"</js>
164    */
165   public static class IsoLocalTime extends TemporalSwap {
166
167      /** Default instance.*/
168      public static final TemporalSwap DEFAULT = new IsoLocalTime();
169
170      /** Constructor.*/
171      public IsoLocalTime() {
172         super("ISO_LOCAL_TIME", true);
173      }
174   }
175
176   /**
177    * Default swap to {@link DateTimeFormatter#ISO_OFFSET_DATE}.
178    * <p>
179    * Example: <js>"2011-12-03"</js>
180    */
181   public static class IsoOffsetDate extends TemporalSwap {
182
183      /** Default instance.*/
184      public static final TemporalSwap DEFAULT = new IsoOffsetDate();
185
186      /** Constructor.*/
187      public IsoOffsetDate() {
188         super("ISO_OFFSET_DATE", false);
189      }
190   }
191
192   /**
193    * Default swap to {@link DateTimeFormatter#ISO_OFFSET_DATE_TIME}.
194    * <p>
195    * Example: <js>"2011-12-03T10:15:30+01:00"</js>
196    */
197   public static class IsoOffsetDateTime extends TemporalSwap {
198
199      /** Default instance.*/
200      public static final TemporalSwap DEFAULT = new IsoOffsetDateTime();
201
202      /** Constructor.*/
203      public IsoOffsetDateTime() {
204         super("ISO_OFFSET_DATE_TIME", false);
205      }
206   }
207
208   /**
209    * Default swap to {@link DateTimeFormatter#ISO_OFFSET_TIME}.
210    * <p>
211    * Example: <js>"10:15:30+01:00"</js>
212    */
213   public static class IsoOffsetTime extends TemporalSwap {
214
215      /** Default instance.*/
216      public static final TemporalSwap DEFAULT = new IsoOffsetTime();
217
218      /** Constructor.*/
219      public IsoOffsetTime() {
220         super("ISO_OFFSET_TIME", false);
221      }
222   }
223
224   /**
225    * Default swap to {@link DateTimeFormatter#ISO_ORDINAL_DATE}.
226    * <p>
227    * Example: <js>"2012-337"</js>
228    */
229   public static class IsoOrdinalDate extends TemporalSwap {
230
231      /** Default instance.*/
232      public static final TemporalSwap DEFAULT = new IsoOrdinalDate();
233
234      /** Constructor.*/
235      public IsoOrdinalDate() {
236         super("ISO_ORDINAL_DATE", true);
237      }
238   }
239
240   /**
241    * Default swap to {@link DateTimeFormatter#ISO_TIME}.
242    * <p>
243    * Example: <js>"10:15:30+01:00"</js> or <js>"10:15:30"</js>
244    */
245   public static class IsoTime extends TemporalSwap {
246
247      /** Default instance.*/
248      public static final TemporalSwap DEFAULT = new IsoTime();
249
250      /** Constructor.*/
251      public IsoTime() {
252         super("ISO_TIME", true);
253      }
254   }
255
256   /**
257    * Default swap to {@link DateTimeFormatter#ISO_WEEK_DATE}.
258    * <p>
259    * Example: <js>"2012-W48-6"</js>
260    */
261   public static class IsoWeekDate extends TemporalSwap {
262
263      /** Default instance.*/
264      public static final TemporalSwap DEFAULT = new IsoWeekDate();
265
266      /** Constructor.*/
267      public IsoWeekDate() {
268         super("ISO_WEEK_DATE", true);
269      }
270   }
271
272   /**
273    * Default swap to {@link DateTimeFormatter#ISO_WEEK_DATE}.
274    * <p>
275    * Example: <js>"2011"</js>
276    */
277   public static class IsoYear extends TemporalSwap {
278
279      /** Default instance.*/
280      public static final TemporalSwap DEFAULT = new IsoYear();
281
282      /** Constructor.*/
283      public IsoYear() {
284         super("uuuu", true);
285      }
286   }
287
288   /**
289    * Default swap to {@link DateTimeFormatter#ISO_WEEK_DATE}.
290    * <p>
291    * Example: <js>"2011-12"</js>
292    */
293   public static class IsoYearMonth extends TemporalSwap {
294
295      /** Default instance.*/
296      public static final TemporalSwap DEFAULT = new IsoYearMonth();
297
298      /** Constructor.*/
299      public IsoYearMonth() {
300         super("uuuu-MM", true);
301      }
302   }
303
304   /**
305    * Default swap to {@link DateTimeFormatter#ISO_ZONED_DATE_TIME}.
306    * <p>
307    * Example: <js>"2011-12-03T10:15:30+01:00[Europe/Paris]"</js>
308    */
309   public static class IsoZonedDateTime extends TemporalSwap {
310
311      /** Default instance.*/
312      public static final TemporalSwap DEFAULT = new IsoZonedDateTime();
313
314      /** Constructor.*/
315      public IsoZonedDateTime() {
316         super("ISO_ZONED_DATE_TIME", false);
317      }
318   }
319
320   /**
321    * Default swap to {@link DateTimeFormatter#RFC_1123_DATE_TIME}.
322    * <p>
323    * Example: <js>"Tue, 3 Jun 2008 11:05:30 GMT"</js>
324    */
325   public static class Rfc1123DateTime extends TemporalSwap {
326
327      /** Default instance.*/
328      public static final TemporalSwap DEFAULT = new Rfc1123DateTime();
329
330      /** Constructor.*/
331      public Rfc1123DateTime() {
332         super("RFC_1123_DATE_TIME", false);
333      }
334   }
335
336   private static final ZoneId Z = ZoneId.of("Z");
337   private static final Map<Class<? extends Temporal>,Method> FROM_METHODS = new ConcurrentHashMap<>();
338
339   private static Method findParseMethod(Class<? extends Temporal> c) throws ExecutableException {
340      var m = FROM_METHODS.get(c);
341      if (m == null) {
342      // @formatter:off
343      m = info(c).getPublicMethod(
344         x -> x.isStatic()
345            && x.isNotDeprecated()
346            && x.hasName("from")
347            && x.hasReturnType(c)
348            && x.hasParameterTypes(TemporalAccessor.class)
349         )
350         .map(MethodInfo::inner)
351         .orElseThrow(() -> new ExecutableException("Parse method not found on temporal class ''{0}''", c.getSimpleName()));
352         // @formatter:on
353         FROM_METHODS.put(c, m);
354      }
355      return m;
356   }
357
358   private final DateTimeFormatter formatter;
359   private final boolean zoneOptional;
360
361   /**
362    * Constructor.
363    *
364    * @param pattern The timestamp format or name of predefined {@link DateTimeFormatter}.
365    * @param zoneOptional <jk>true</jk> if the time zone on the pattern is optional.
366    */
367   public TemporalSwap(String pattern, boolean zoneOptional) {
368      super(Temporal.class);
369      this.formatter = getDateTimeFormatter(pattern);
370      this.zoneOptional = zoneOptional;
371   }
372
373   @Override /* Overridden from ObjectSwap */
374   public String swap(BeanSession session, Temporal o) throws Exception {
375      if (o == null)
376         return null;
377      o = convertToSerializable(session, o);
378      return formatter.format(o);
379   }
380
381   @SuppressWarnings("unchecked")
382   @Override /* Overridden from ObjectSwap */
383   public Temporal unswap(BeanSession session, String f, ClassMeta<?> hint) throws Exception {
384      if (f == null)
385         return null;
386      if (hint == null)
387         hint = session.getClassMeta(Instant.class);
388      var tc = (Class<? extends Temporal>)hint.inner();
389
390      var offset = session.getTimeZoneId();
391
392      if (tc == Instant.class)
393         offset = Z;
394
395      var parseMethod = findParseMethod(tc);
396
397      var ta = defaulting(formatter.parse(f), offset);
398      return (Temporal)parseMethod.invoke(null, ta);
399   }
400
401   private final static TemporalAccessor defaulting(TemporalAccessor t, ZoneId zoneId) {
402      return new DefaultingTemporalAccessor(t, zoneId);
403   }
404
405   /**
406    * Converts the specified temporal object to a form suitable to be serialized using any pattern.
407    *
408    * @param session The current bean session.
409    * @param t The temporal object to convert.
410    * @return The converted temporal object.
411    */
412   protected Temporal convertToSerializable(BeanSession session, Temporal t) {
413
414      var zoneId = session.getTimeZoneId();
415      var tc = t.getClass();
416
417      // Instant is always serialized in GMT.
418      if (tc == Instant.class)
419         return ZonedDateTime.from(defaulting(t, Z));
420
421      // These can handle any pattern.
422      if (tc == ZonedDateTime.class || tc == OffsetDateTime.class)
423         return t;
424
425      // Pattern optionally includes a time zone, so zoned and local date-times are good.
426      if (zoneOptional()) {
427         if (tc == LocalDateTime.class)
428            return t;
429         if (tc == OffsetTime.class)
430            return ZonedDateTime.from(defaulting(t, zoneId));
431         return LocalDateTime.from(defaulting(t, zoneId));
432      }
433
434      return ZonedDateTime.from(defaulting(t, zoneId));
435   }
436
437   /**
438    * Returns <jk>true</jk> if the time zone on the pattern is optional.
439    *
440    * <p>
441    * If it's not optional, then local dates/times must be converted into zoned times using the session time zone.
442    * Otherwise, local date/times are fine.
443    *
444    * @return <jk>true</jk> if the time zone on the pattern is optional.
445    */
446   protected boolean zoneOptional() {
447      return zoneOptional;
448   }
449}