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