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 java.time.*;
020import java.util.*;
021
022/**
023 * Supported wire formats for {@link TimeZone} and {@link ZoneId} values.
024 *
025 * <p>
026 * Used by {@link MarshallingContext.Builder#timeZoneFormat(TimeZoneFormat)},
027 * {@link Marshalled#timeZoneFormat()},
028 * {@link MarshalledProp#timeZoneFormat()}, and
029 * {@link MarshalledConfig#timeZoneFormat()} to control how time-zone
030 * identifiers are written to and read from the wire.
031 *
032 * <p>
033 * The default is {@link #ID} which preserves the historical wire output produced before this enum existed
034 * ({@code TimeZone.getID()} / {@code ZoneId.getId()}).
035 */
036public enum TimeZoneFormat {
037
038   /** Sentinel meaning "no value configured" — falls through to the next-higher precedence level. */
039   NOT_SET,
040
041   /** The IANA zone id (the default): <js>"America/Los_Angeles"</js>, <js>"UTC"</js>, <js>"GMT-08:00"</js>. */
042   ID,
043
044   /** Normalized to a {@link ZoneOffset} where possible: <js>"-08:00"</js>; falls back to {@link #ID} for region zones. */
045   OFFSET,
046
047   /**
048    * Default long display name in {@link Locale#ROOT} (formatting only; not always round-trippable):
049    * <js>"Pacific Standard Time"</js>.
050    *
051    * <p>
052    * Parsing accepts any value that {@link ZoneId#of(String)} understands; localized display names are
053    * <b>not</b> reverse-parsed.
054    */
055   NAME_LONG,
056
057   /**
058    * Default short display name in {@link Locale#ROOT} (formatting only; not always round-trippable):
059    * <js>"PST"</js>.
060    *
061    * <p>
062    * Parsing accepts any value that {@link ZoneId#of(String)} understands; localized display names are
063    * <b>not</b> reverse-parsed.
064    */
065   NAME_SHORT;
066
067   /**
068    * Formats the specified {@link TimeZone} using this format.
069    *
070    * @param value The value to format. Can be <jk>null</jk> (returns <jk>null</jk>).
071    * @return The formatted string, or <jk>null</jk> if {@code value} is <jk>null</jk>.
072    */
073   public String format(TimeZone value) {
074      if (value == null)
075         return null;
076      return switch (this) {
077         case NAME_LONG -> value.getDisplayName(false, TimeZone.LONG, Locale.ROOT);
078         case NAME_SHORT -> value.getDisplayName(false, TimeZone.SHORT, Locale.ROOT);
079         case OFFSET -> formatOffset(value.toZoneId());
080         case NOT_SET, ID -> value.getID();
081      };
082   }
083
084   /**
085    * Formats the specified {@link ZoneId} using this format.
086    *
087    * @param value The value to format. Can be <jk>null</jk> (returns <jk>null</jk>).
088    * @return The formatted string, or <jk>null</jk> if {@code value} is <jk>null</jk>.
089    */
090   public String format(ZoneId value) {
091      if (value == null)
092         return null;
093      return switch (this) {
094         case NAME_LONG -> TimeZone.getTimeZone(value).getDisplayName(false, TimeZone.LONG, Locale.ROOT);
095         case NAME_SHORT -> TimeZone.getTimeZone(value).getDisplayName(false, TimeZone.SHORT, Locale.ROOT);
096         case OFFSET -> formatOffset(value);
097         case NOT_SET, ID -> value.getId();
098      };
099   }
100
101   /**
102    * Parses the specified wire value as a {@link TimeZone}.
103    *
104    * @param value The wire value. Can be <jk>null</jk> or blank (returns <jk>null</jk>).
105    * @return The parsed {@link TimeZone}, or <jk>null</jk> if {@code value} is <jk>null</jk> or blank.
106    */
107   public static TimeZone parseTimeZone(String value) {
108      if (value == null)
109         return null;
110      var s = value.trim();
111      if (s.isEmpty())
112         return null;
113      return TimeZone.getTimeZone(s);
114   }
115
116   /**
117    * Parses the specified wire value as a {@link ZoneId}.
118    *
119    * @param value The wire value. Can be <jk>null</jk> or blank (returns <jk>null</jk>).
120    * @return The parsed {@link ZoneId}, or <jk>null</jk> if {@code value} is <jk>null</jk> or blank.
121    */
122   public static ZoneId parseZoneId(String value) {
123      if (value == null)
124         return null;
125      var s = value.trim();
126      if (s.isEmpty())
127         return null;
128      return ZoneId.of(s, ZoneId.SHORT_IDS);
129   }
130
131   private static String formatOffset(ZoneId zoneId) {
132      try {
133         return ZoneOffset.from(zoneId.getRules().getOffset(Instant.now())).getId();
134      } catch (@SuppressWarnings("unused") Exception e) {  // NOSONAR: ZoneOffset.from throws DateTimeException; fall back to ID
135         return zoneId.getId();
136      }
137   }
138}