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}