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.utils.Shorts.*;
020import static org.apache.juneau.commons.utils.ThrowableUtils.*;
021
022import java.time.*;
023import java.time.format.*;
024import java.util.*;
025
026import javax.xml.datatype.*;
027
028import org.apache.juneau.marshall.swaps.*;
029
030/**
031 * Supported wire formats for {@link Calendar} values.
032 *
033 * <p>
034 * Used by {@link MarshallingContext.Builder#calendarFormat(CalendarFormat)},
035 * {@link Marshalled#calendarFormat()},
036 * {@link MarshalledProp#calendarFormat()}, and
037 * {@link MarshalledConfig#calendarFormat()} to control how
038 * {@link Calendar} values (including {@link GregorianCalendar}) are written to and read from the wire.
039 *
040 * <p>
041 * The default is {@link #ISO_OFFSET_DATE_TIME} which preserves the historical wire output produced
042 * before this enum existed.
043 *
044 * <p>
045 * Numeric formats ({@link #MILLIS}) emit native numeric wire types on binary serializers
046 * (BSON / CBOR / MsgPack) and bare numbers on text formats. Text formats emit a quoted ISO 8601 string for
047 * every other constant.
048 *
049 * <p>
050 * {@link XMLGregorianCalendar} fields always emit {@code toXMLFormat()} regardless of the configured
051 * {@code CalendarFormat}. The {@link #XML_FORMAT} constant exists so that regular {@code Calendar} /
052 * {@code GregorianCalendar} fields can opt into the same wire form for symmetry with their
053 * {@code XMLGregorianCalendar} siblings.
054 */
055public enum CalendarFormat {
056
057   /** Sentinel meaning "no value configured" — falls through to the next-higher precedence level. */
058   NOT_SET,
059
060   /** {@link DateTimeFormatter#BASIC_ISO_DATE}: <js>"20111203"</js>. */
061   BASIC_ISO_DATE,
062
063   /** {@link DateTimeFormatter#ISO_DATE}: <js>"2011-12-03+01:00"</js> or <js>"2011-12-03"</js>. */
064   ISO_DATE,
065
066   /** {@link DateTimeFormatter#ISO_DATE_TIME}: <js>"2011-12-03T10:15:30+01:00[Europe/Paris]"</js>. */
067   ISO_DATE_TIME,
068
069   /** {@link DateTimeFormatter#ISO_INSTANT}: <js>"2011-12-03T10:15:30Z"</js>. */
070   ISO_INSTANT,
071
072   /** {@link DateTimeFormatter#ISO_LOCAL_DATE}: <js>"2011-12-03"</js>. */
073   ISO_LOCAL_DATE,
074
075   /** {@link DateTimeFormatter#ISO_LOCAL_DATE_TIME}: <js>"2011-12-03T10:15:30"</js>. */
076   ISO_LOCAL_DATE_TIME,
077
078   /** {@link DateTimeFormatter#ISO_LOCAL_TIME}: <js>"10:15:30"</js>. */
079   ISO_LOCAL_TIME,
080
081   /** {@link DateTimeFormatter#ISO_OFFSET_DATE}: <js>"2011-12-03+01:00"</js>. */
082   ISO_OFFSET_DATE,
083
084   /** {@link DateTimeFormatter#ISO_OFFSET_DATE_TIME} (the default): <js>"2011-12-03T10:15:30+01:00"</js>. */
085   ISO_OFFSET_DATE_TIME,
086
087   /** {@link DateTimeFormatter#ISO_OFFSET_TIME}: <js>"10:15:30+01:00"</js>. */
088   ISO_OFFSET_TIME,
089
090   /** {@link DateTimeFormatter#ISO_ORDINAL_DATE}: <js>"2012-337"</js>. */
091   ISO_ORDINAL_DATE,
092
093   /** {@link DateTimeFormatter#ISO_TIME}: <js>"10:15:30+01:00"</js> or <js>"10:15:30"</js>. */
094   ISO_TIME,
095
096   /** {@link DateTimeFormatter#ISO_WEEK_DATE}: <js>"2012-W48-6"</js>. */
097   ISO_WEEK_DATE,
098
099   /** {@link DateTimeFormatter#ISO_ZONED_DATE_TIME}: <js>"2011-12-03T10:15:30+01:00[Europe/Paris]"</js>. */
100   ISO_ZONED_DATE_TIME,
101
102   /** {@link DateTimeFormatter#RFC_1123_DATE_TIME}: <js>"Tue, 3 Jun 2008 11:05:30 GMT"</js>. */
103   RFC_1123_DATE_TIME,
104
105   /**
106    * Epoch milliseconds as a numeric value.
107    *
108    * <p>
109    * Emitted as a native int64 by binary serializers (BSON / CBOR / MsgPack) and as a bare integer
110    * literal by text serializers.
111    */
112   MILLIS,
113
114   /**
115    * {@link XMLGregorianCalendar#toXMLFormat() XML Schema dateTime} form.
116    *
117    * <p>
118    * Lets regular {@link Calendar} / {@link GregorianCalendar} fields opt into the same wire form
119    * that {@link XMLGregorianCalendar} fields always use, for symmetry across mixed-type beans.
120    */
121   XML_FORMAT;
122
123   private static final ZoneId Z = ZoneId.of("Z");
124
125   private static final DatatypeFactory DATATYPE_FACTORY;
126   static {
127      try {
128         DATATYPE_FACTORY = DatatypeFactory.newInstance();
129      } catch (DatatypeConfigurationException e) {
130         throw toRex(e);
131      }
132   }
133
134   /**
135    * Formats the specified value using this format.
136    *
137    * @param value The value to format. Can be <jk>null</jk> (returns <jk>null</jk>).
138    * @param zoneId The default zone id used for time-only / unzoned values.
139    * @return The formatted string, or <jk>null</jk> if {@code value} is <jk>null</jk>.
140    */
141   public String format(Calendar value, ZoneId zoneId) {
142      if (value == null)
143         return null;
144      if (this == MILLIS)
145         return Long.toString(value.getTimeInMillis());
146      if (this == XML_FORMAT) {
147         var gc = value instanceof GregorianCalendar value2 ? value2 : GregorianCalendar.from(value.toInstant().atZone(value.getTimeZone().toZoneId()));
148         return DATATYPE_FACTORY.newXMLGregorianCalendar(gc).toXMLFormat();
149      }
150      var zone = zoneId == null ? value.getTimeZone().toZoneId() : zoneId;
151      var zdt = value instanceof GregorianCalendar value2 ? value2.toZonedDateTime() : value.toInstant().atZone(zone);
152      return formatter().format(zdt);
153   }
154
155   /**
156    * Parses the specified wire value using this format.
157    *
158    * @param value The wire value. Can be <jk>null</jk> or blank (returns <jk>null</jk>).
159    * @param zoneId The default zone id used for time-only / unzoned values.
160    * @return The parsed {@link Calendar}, or <jk>null</jk> if {@code value} is <jk>null</jk> or blank.
161    */
162   public Calendar parse(String value, ZoneId zoneId) {
163      if (value == null)
164         return null;
165      var s = value.trim();
166      if (s.isEmpty())
167         return null;
168      var zone = zoneId == null ? ZoneId.systemDefault() : zoneId;
169      if (this == MILLIS)
170         return GregorianCalendar.from(Instant.ofEpochMilli(Long.parseLong(s)).atZone(zone));
171      if (this == XML_FORMAT)
172         return DATATYPE_FACTORY.newXMLGregorianCalendar(s).toGregorianCalendar();
173      var ta = new DefaultingTemporalAccessor(formatter().parse(s), zone);
174      return GregorianCalendar.from(ZonedDateTime.from(ta));
175   }
176
177   /**
178    * Returns <jk>true</jk> if this format is numeric on the wire.
179    *
180    * @return <jk>true</jk> if this format emits a numeric wire value.
181    */
182   public boolean isNumeric() {
183      return this == MILLIS;
184   }
185
186   private DateTimeFormatter formatter() {
187      return switch (this) {
188         case NOT_SET, ISO_OFFSET_DATE_TIME -> DateTimeFormatter.ISO_OFFSET_DATE_TIME;
189         case BASIC_ISO_DATE -> DateTimeFormatter.BASIC_ISO_DATE;
190         case ISO_DATE -> DateTimeFormatter.ISO_DATE;
191         case ISO_DATE_TIME -> DateTimeFormatter.ISO_DATE_TIME;
192         case ISO_INSTANT -> DateTimeFormatter.ISO_INSTANT;
193         case ISO_LOCAL_DATE -> DateTimeFormatter.ISO_LOCAL_DATE;
194         case ISO_LOCAL_DATE_TIME -> DateTimeFormatter.ISO_LOCAL_DATE_TIME;
195         case ISO_LOCAL_TIME -> DateTimeFormatter.ISO_LOCAL_TIME;
196         case ISO_OFFSET_DATE -> DateTimeFormatter.ISO_OFFSET_DATE;
197         case ISO_OFFSET_TIME -> DateTimeFormatter.ISO_OFFSET_TIME;
198         case ISO_ORDINAL_DATE -> DateTimeFormatter.ISO_ORDINAL_DATE;
199         case ISO_TIME -> DateTimeFormatter.ISO_TIME;
200         case ISO_WEEK_DATE -> DateTimeFormatter.ISO_WEEK_DATE;
201         case ISO_ZONED_DATE_TIME -> DateTimeFormatter.ISO_ZONED_DATE_TIME;
202         case RFC_1123_DATE_TIME -> DateTimeFormatter.RFC_1123_DATE_TIME;
203         case XML_FORMAT, MILLIS -> throw isex("Format %s does not use a DateTimeFormatter", this);
204      };
205   }
206
207   /**
208    * Returns the {@link ZoneId} used by the {@link #ISO_INSTANT} variant.
209    *
210    * @return The fixed {@code "Z"} (UTC) zone id used by the {@link #ISO_INSTANT} variant.
211    */
212   public static ZoneId zuluZone() {
213      return Z;
214   }
215}