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}