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