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.*; 022import java.util.stream.*; 023 024/** 025 * Supported wire formats for {@link Currency} values. 026 * 027 * <p> 028 * Used by {@link MarshallingContext.Builder#currencyFormat(CurrencyFormat)}, 029 * {@link Marshalled#currencyFormat()}, 030 * {@link MarshalledProp#currencyFormat()}, and 031 * {@link MarshalledConfig#currencyFormat()} to control how {@link Currency} 032 * values are written to text-based wire formats. 033 * 034 * <p> 035 * The default is {@link #ISO_CODE} which preserves the historical wire output ({@link Currency#toString()} / 036 * {@link Currency#getCurrencyCode()}). 037 * 038 * <h5 class='topic'>Locale composability</h5> 039 * 040 * <p> 041 * {@link #SYMBOL} and {@link #DISPLAY_NAME} are locale-sensitive — they consult the supplied {@link Locale} 042 * (typically the {@link MarshallingContext#getLocale()} resolved through the {@link MarshallingSession}). 043 * {@link #ISO_CODE} ignores the locale. 044 * 045 * <h5 class='topic'>Round-trip caveat</h5> 046 * 047 * <p> 048 * <b>Round-trip is strictly guaranteed only for {@link #ISO_CODE}.</b> {@code SYMBOL} parsing of a token 049 * like {@code "$"} could resolve to any of USD / CAD / AUD / MXN / HKD / SGD / NZD depending on the 050 * locale, and {@code DISPLAY_NAME} parsing is even less reliable ({@code "Dollar"} matches at least eight 051 * ISO codes). For machine-to-machine wires, prefer {@link #ISO_CODE}. 052 * 053 * <h5 class='topic'>Precedence (highest to lowest)</h5> 054 * <ol> 055 * <li>{@link MarshalledProp#currencyFormat() @MarshalledProp(currencyFormat=…)} on the bean property. 056 * <li>{@link Marshalled#currencyFormat() @Marshalled(currencyFormat=…)} on the bean class. 057 * <li>{@link MarshalledConfig#currencyFormat() @MarshalledConfig(currencyFormat=…)} on 058 * <code><ja>@Rest</ja></code>-annotated classes / methods. 059 * <li>Programmatic {@link MarshallingContext.Builder#currencyFormat(CurrencyFormat)}. 060 * <li>Environment variable <c>MarshallingContext.currencyFormat</c>. 061 * <li>The default constant ({@link #ISO_CODE}). 062 * </ol> 063 * 064 * <h5 class='topic'>Parser leniency</h5> 065 * 066 * <p> 067 * Parsers SHALL try {@link Currency#getInstance(String)} first regardless of the parser-side 068 * {@code CurrencyFormat} setting — this handles the {@link #ISO_CODE} shape unambiguously. For 069 * {@link #SYMBOL} / {@link #DISPLAY_NAME}, the parser falls back to a best-effort iterate-and-match scan 070 * against {@link Currency#getAvailableCurrencies()}. 071 * 072 * <h5 class='topic'>Binary serializers</h5> 073 * 074 * <p> 075 * Binary serializers (BSON / CBOR / MsgPack / Prototext / Parquet) emit a UTF-8 string regardless of this 076 * setting — there is no native {@code Currency} wire type in any supported binary format. The choice of 077 * {@code ISO_CODE} vs {@code SYMBOL} vs {@code DISPLAY_NAME} is purely about the textual content of that 078 * string. 079 */ 080public enum CurrencyFormat { 081 082 /** Sentinel meaning "no value configured" — falls through to the next-higher precedence level. */ 083 NOT_SET, 084 085 /** 086 * ISO 4217 currency code (the default). 087 * 088 * <h5 class='figure'>Example:</h5> 089 * <p class='bcode'> 090 * USD 091 * EUR 092 * JPY 093 * </p> 094 * 095 * <p> 096 * Locale-independent. Round-trips through {@link Currency#getInstance(String)}. 097 */ 098 ISO_CODE, 099 100 /** 101 * Currency symbol per {@link Currency#getSymbol(Locale)}. 102 * 103 * <h5 class='figure'>Example (locale {@code en_US}):</h5> 104 * <p class='bcode'> 105 * $ 106 * € 107 * ¥ 108 * </p> 109 * 110 * <p> 111 * <b>Locale-sensitive.</b> The symbol is resolved against the supplied locale. Round-trip is 112 * best-effort — {@code "$"} is shared by USD / CAD / AUD / MXN / HKD / SGD / NZD; the parser prefers 113 * the locale's default currency on ambiguity and throws when no match is found. 114 */ 115 SYMBOL, 116 117 /** 118 * Localized display name per {@link Currency#getDisplayName(Locale)}. 119 * 120 * <h5 class='figure'>Example (locale {@code en_US}):</h5> 121 * <p class='bcode'> 122 * US Dollar 123 * Euro 124 * Japanese Yen 125 * </p> 126 * 127 * <p> 128 * <b>Locale-sensitive and even less round-trip-safe than {@link #SYMBOL}.</b> Use only for 129 * user-facing display contexts. 130 */ 131 DISPLAY_NAME; 132 133 /** 134 * Formats the specified {@link Currency} using this format. 135 * 136 * <p> 137 * {@link #NOT_SET} falls through to {@link #ISO_CODE}. {@code null} locale is treated as 138 * {@link Locale#ROOT}. 139 * 140 * @param value The value to format. Can be <jk>null</jk> (returns <jk>null</jk>). 141 * @param format The configured format. Can be <jk>null</jk> (treated as {@link #ISO_CODE}). 142 * @param locale The locale used by {@link #SYMBOL} / {@link #DISPLAY_NAME}. Can be <jk>null</jk> 143 * (treated as {@link Locale#ROOT}). 144 * @return The formatted wire representation, or <jk>null</jk> if {@code value} is <jk>null</jk>. 145 */ 146 public static String format(Currency value, CurrencyFormat format, Locale locale) { 147 if (value == null) 148 return null; 149 var fmt = format == null ? ISO_CODE : format; 150 var loc = locale == null ? Locale.ROOT : locale; 151 return switch (fmt) { 152 case NOT_SET, ISO_CODE -> value.getCurrencyCode(); 153 case SYMBOL -> value.getSymbol(loc); 154 case DISPLAY_NAME -> value.getDisplayName(loc); 155 }; 156 } 157 158 /** 159 * Parses the specified wire value into a {@link Currency}. 160 * 161 * <p> 162 * Lenient parsing — always tries {@link Currency#getInstance(String)} first (handles {@link #ISO_CODE} 163 * unambiguously). For {@link #SYMBOL} / {@link #DISPLAY_NAME} the parser falls back to a best-effort 164 * iterate-and-match scan against {@link Currency#getAvailableCurrencies()}; when multiple currencies 165 * match a symbol, prefers the locale's default currency. 166 * 167 * @param value The wire value. Can be <jk>null</jk> or blank (returns <jk>null</jk>). 168 * @param format The configured format hint. Can be <jk>null</jk> (treated as {@link #ISO_CODE}). 169 * @param locale The locale used to disambiguate {@link #SYMBOL} / {@link #DISPLAY_NAME} matches. 170 * Can be <jk>null</jk> (treated as {@link Locale#ROOT}). 171 * @return The parsed {@link Currency}, or <jk>null</jk> if {@code value} is <jk>null</jk> or blank. 172 * @throws IllegalArgumentException If the value cannot be resolved to a unique currency. 173 */ 174 public static Currency parse(String value, CurrencyFormat format, Locale locale) { 175 if (value == null) 176 return null; 177 var s = value.trim(); 178 if (s.isEmpty()) 179 return null; 180 var loc = locale == null ? Locale.ROOT : locale; 181 var fmt = format == null ? ISO_CODE : format; 182 // Try ISO code first — works unambiguously for the ISO_CODE shape and many SYMBOL / DISPLAY_NAME inputs. 183 try { 184 return Currency.getInstance(s); 185 } catch (@SuppressWarnings("unused") IllegalArgumentException ignored) { 186 // Fall through to symbol / display-name scan. 187 } 188 return switch (fmt) { 189 case NOT_SET, ISO_CODE -> throw iaex("Invalid currency code '%s'", value); 190 case SYMBOL -> findBySymbol(s, loc, value); 191 case DISPLAY_NAME -> findByDisplayName(s, loc, value); 192 }; 193 } 194 195 private static Currency findBySymbol(String token, Locale loc, String original) { 196 var matches = Currency.getAvailableCurrencies().stream().filter(c -> eq(token, c.getSymbol(loc))).collect(Collectors.toCollection(ArrayList::new)); 197 return resolveAmbiguousMatches(matches, loc, original, "symbol"); 198 } 199 200 private static Currency findByDisplayName(String token, Locale loc, String original) { 201 var matches = Currency.getAvailableCurrencies().stream().filter(c -> eq(token, c.getDisplayName(loc))).collect(Collectors.toCollection(ArrayList::new)); 202 return resolveAmbiguousMatches(matches, loc, original, "display name"); 203 } 204 205 private static Currency resolveAmbiguousMatches(List<Currency> matches, Locale loc, String original, String kind) { 206 if (matches.isEmpty()) 207 throw iaex("Could not resolve currency %s '%s' in locale %s", kind, original, loc); 208 if (matches.size() == 1) 209 return matches.get(0); 210 // Prefer the locale's default currency when the locale has one. 211 try { 212 var preferred = Currency.getInstance(loc); 213 if (matches.contains(preferred)) 214 return preferred; 215 } catch (@SuppressWarnings("unused") IllegalArgumentException ignored) { 216 // Locale has no associated currency (e.g. Locale.ROOT, Locale.ENGLISH, Locale.JAPAN may or may not). 217 } 218 throw iaex("Ambiguous currency %s '%s' in locale %s: matches %s", kind, original, loc, matches); 219 } 220 221 /** 222 * Returns <jk>true</jk> if this format emits a numeric wire value. 223 * 224 * <p> 225 * Always <jk>false</jk> — every constant emits a textual representation. 226 * 227 * @return <jk>false</jk>. 228 */ 229 @SuppressWarnings({ 230 "java:S3400", // Same rationale — must remain an instance method, not a constant, to match the cross-Format API contract. 231 "static-method" // Kept as an instance method for polymorphic-by-convention symmetry with the other Format classes (BigNumberFormat, FloatFormat, DurationFormat, etc.) where isNumeric() depends on the enum constant. 232 }) 233 public boolean isNumeric() { 234 return false; 235 } 236}