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.*;
020
021import java.util.*;
022
023/**
024 * Supported wire formats for {@link Enum} values.
025 *
026 * <p>
027 * Used by {@link MarshallingContext.Builder#enumFormat(EnumFormat)},
028 * {@link Marshalled#enumFormat()},
029 * {@link MarshalledProp#enumFormat()}, and
030 * {@link MarshalledConfig#enumFormat()} to control how
031 * {@link Enum} values are written to and read from the wire.
032 *
033 * <p>
034 * The default is {@link #TO_STRING} which preserves the historical wire output produced by
035 * {@code Object.toString()} (the previous default before this enum existed). New APIs SHOULD prefer
036 * {@link #NAME} for stability against {@code toString()} overrides.
037 *
038 * <p>
039 * Numeric formats ({@link #ORDINAL}) emit native numeric wire types on binary serializers
040 * (BSON / CBOR / MsgPack) and a bare integer literal on text formats. All other constants emit a
041 * UTF-8 string.
042 *
043 * <p>
044 * Parsers SHALL accept any wire shape that the corresponding serializer can produce, regardless of
045 * the parser-side {@code EnumFormat} setting. The parser-side setting is consulted only for genuinely
046 * ambiguous input.
047 */
048@SuppressWarnings({
049   "java:S1192" // Duplicated literals (argument/property names) read more clearly inline than as constants
050})
051public enum EnumFormat {
052
053   /** Sentinel meaning "no value configured" — falls through to the next-higher precedence level. */
054   NOT_SET,
055
056   /** Calls {@link Object#toString()} on the enum value (the default). */
057   TO_STRING,
058
059   /** Calls {@link Enum#name()} on the enum value. */
060   NAME,
061
062   /** Lower-case hyphenated form: {@code MY_ENUM_VALUE} → {@code "my-enum-value"}. */
063   LOWER_HYPHEN,
064
065   /** Upper-case hyphenated form: {@code MY_ENUM_VALUE} → {@code "MY-ENUM-VALUE"}. */
066   UPPER_HYPHEN,
067
068   /** Lower-case underscored form: {@code MY_ENUM_VALUE} → {@code "my_enum_value"}. */
069   LOWER_UNDERSCORE,
070
071   /** Lower-case form using {@link Locale#ROOT}: {@code MY_ENUM_VALUE} → {@code "my_enum_value"}. */
072   LOWER,
073
074   /** Upper-case form using {@link Locale#ROOT}: equivalent to {@link Enum#name()} for canonical UPPER_SNAKE enum names. */
075   UPPER,
076
077   /** {@link Enum#ordinal()} as a numeric value. */
078   ORDINAL;
079
080   /**
081    * Formats the specified enum value using this format.
082    *
083    * @param value The enum value.  Can be <jk>null</jk> (returns <jk>null</jk>).
084    * @return The formatted wire representation, or <jk>null</jk> if {@code value} is <jk>null</jk>.
085    */
086   public String format(Enum<?> value) {
087      if (value == null)
088         return null;
089      return switch (this) {
090         case NOT_SET, TO_STRING -> value.toString();
091         case NAME, UPPER -> value.name();
092         case LOWER_HYPHEN -> value.name().toLowerCase(Locale.ROOT).replace('_', '-');
093         case UPPER_HYPHEN -> value.name().replace('_', '-');
094         case LOWER_UNDERSCORE, LOWER -> value.name().toLowerCase(Locale.ROOT);
095         case ORDINAL -> Integer.toString(value.ordinal());
096      };
097   }
098
099   /**
100    * Parses the specified wire value into an enum constant of the specified class.
101    *
102    * <p>
103    * Parsing is format-agnostic: every constant accepts any of the wire shapes produced by
104    * {@link #format(Enum)} regardless of the configured constant.  Lookup order:
105    * <ol>
106    *    <li>Numeric input → resolved as {@link #ORDINAL}.
107    *    <li>Exact match against {@link Enum#name()}.
108    *    <li>Exact match against {@link Object#toString()}.
109    *    <li>Reconstruct UPPER_SNAKE form (replace {@code -} with {@code _}, upper-case) and re-try {@code name()}.
110    *    <li>Case-insensitive match against {@code name()} and {@code toString()}.
111    * </ol>
112    *
113    * @param <E> The enum class type.
114    * @param value The wire value.  Can be <jk>null</jk> or blank (returns <jk>null</jk>).
115    * @param enumClass The enum class.  Must not be <jk>null</jk>.
116    * @return The parsed enum constant, or <jk>null</jk> if {@code value} is <jk>null</jk> or blank.
117    * @throws IllegalArgumentException If the value does not match any enum constant.
118    */
119   @SuppressWarnings({
120      "java:S3776" // Cognitive complexity acceptable for enum format parsing dispatch
121   })
122   public static <E extends Enum<E>> E parse(String value, Class<E> enumClass) {
123      reqnn("enumClass", enumClass);
124      if (value == null)
125         return null;
126      var s = value.trim();
127      if (s.isEmpty())
128         return null;
129      // Numeric input: try ORDINAL first.
130      if (isAllDigits(s)) {
131         try {
132            var ord = Integer.parseInt(s);
133            var consts = enumClass.getEnumConstants();
134            if (ord >= 0 && ord < consts.length)
135               return consts[ord];
136         } catch (@SuppressWarnings("unused") NumberFormatException e) {
137            // Fall through to string-based matching.
138         }
139      }
140      // Exact name match.
141      for (var e : enumClass.getEnumConstants())
142         if (e.name().equals(s))
143            return e;
144      // Exact toString() match.
145      for (var e : enumClass.getEnumConstants())
146         if (s.equals(e.toString()))
147            return e;
148      // Reconstruct UPPER_SNAKE form (handles LOWER_HYPHEN / UPPER_HYPHEN / LOWER / LOWER_UNDERSCORE).
149      var snake = s.replace('-', '_').toUpperCase(Locale.ROOT);
150      for (var e : enumClass.getEnumConstants())
151         if (e.name().equals(snake))
152            return e;
153      // Case-insensitive fallback.
154      for (var e : enumClass.getEnumConstants())
155         if (eqic(e.name(), s) || eqic(s, e.toString()))
156            return e;
157      throw iaex("Could not resolve enum value '%s' on class '%s'", value, enumClass.getName());
158   }
159
160   /**
161    * Returns <jk>true</jk> if this format is numeric on the wire.
162    *
163    * @return <jk>true</jk> if this format emits a numeric wire value.
164    */
165   public boolean isNumeric() {
166      return this == ORDINAL;
167   }
168
169   private static boolean isAllDigits(String s) {
170      if (s.isEmpty())
171         return false;
172      var i = s.charAt(0) == '-' || s.charAt(0) == '+' ? 1 : 0;
173      if (i == s.length())
174         return false;
175      for (; i < s.length(); i++) {
176         var c = s.charAt(i);
177         if (c < '0' || c > '9')
178            return false;
179      }
180      return true;
181   }
182}