001/*
002 * jPOS Project [http://jpos.org]
003 * Copyright (C) 2000-2026 jPOS Software SRL
004 *
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU Affero General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU Affero General Public License for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License
016 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jpos.iso;
020
021import java.io.IOException;
022import java.io.InputStream;
023import java.math.BigDecimal;
024import java.util.*;
025import java.util.stream.Collectors;
026
027/**
028 * ISO Currency Conversion package
029 *
030 * @author vsalaman@gmail.com
031 * @author Jonathan.O'Connor@xcom.de
032 * @version $Id$
033 * @see <a href="http://www.evertype.com/standards/iso4217/iso4217-en.html">ISO 4217 currency codes</a>
034 * @see <a href="http://www.iso.org/iso/en/prods-services/popstds/currencycodeslist.html">ISO currency codes list</a>
035 */
036public class ISOCurrency
037{
038    private static final Map<String, Currency> currencies = new HashMap<String, Currency>();
039
040    // Avoid creation of instances.
041    private ISOCurrency()
042    {
043    }
044
045    static
046    {
047        addJavaCurrencies();
048        loadPropertiesFromClasspath("org/jpos/iso/ISOCurrency.properties");
049        loadPropertiesFromClasspath("META-INF/org/jpos/config/ISOCurrency.properties");
050    }
051
052    private static void addJavaCurrencies()
053    {
054        List<java.util.Currency> currencies = java.util.Currency.getAvailableCurrencies()
055                .stream()
056                .sorted(Comparator.comparing(java.util.Currency::getCurrencyCode))
057                .collect(Collectors.toList());
058        for (java.util.Currency sc : currencies)
059        {
060            try
061            {
062                addCurrency(sc.getCurrencyCode().toUpperCase(),
063                            ISOUtil.zeropad(Integer.toString(sc.getNumericCode()), 3),
064                            sc.getDefaultFractionDigits());
065            }
066            catch (ISOException ignored)
067            {
068            }
069        }
070    }
071
072    /**
073     * Loads currency properties from the classpath.
074     * @param base base path for properties resource
075     */
076    @SuppressWarnings({"EmptyCatchBlock"})
077    public static void loadPropertiesFromClasspath(String base)
078    {
079        InputStream in=loadResourceAsStream(base);
080        try
081        {
082            if(in!=null)
083            {
084                addBundle(new PropertyResourceBundle(in));
085            }
086        }
087        catch (IOException e)
088        {
089        }
090        finally
091        {
092            if(in!=null)
093            {
094                try
095                {
096                    in.close();
097                }
098                catch (IOException e)
099                {
100                }
101            }
102        }
103    }
104
105    /**
106     * Converts from an ISO Amount (12 digit string) to a double taking in
107     * consideration the number of decimal digits according to currency
108     *
109     * @param isoamount - The ISO amount to be converted (eg. ISOField 4)
110     * @param currency  - The ISO currency to be converted (eg. ISOField 49)
111     * @return result - A double representing the converted field
112     * @throws IllegalArgumentException if we fail to convert the amount
113     * @deprecated You should never use doubles
114     */
115    @Deprecated
116    public static double convertFromIsoMsg(String isoamount, String currency) throws IllegalArgumentException
117    {
118        Currency c = findCurrency(currency);
119        return c.parseAmountFromISOMsg(isoamount);
120    }
121    /**
122     * Converts a BigDecimal amount to an ISO 8583 amount string.
123     * @param amount   the monetary amount
124     * @param currency the ISO 4217 currency code
125     * @return the formatted ISO 87 string
126     */
127    
128    public static String toISO87String (BigDecimal amount, String currency)
129    {
130        try {
131            Currency c = findCurrency(currency);
132            return ISOUtil.zeropad(amount.movePointRight(c.getDecimals()).setScale(0).toPlainString(), 12);
133        }
134        catch (ISOException e) {
135            throw new IllegalArgumentException("Failed to convert amount",e);
136        }
137    }
138    /**
139     * Parses an ISO 8583 amount string to a BigDecimal.
140     * @param isoamount the ISO 87 amount string
141     * @param currency  the ISO 4217 currency code
142     * @return the parsed BigDecimal amount
143     */
144    
145    public static BigDecimal parseFromISO87String (String isoamount, String currency) {
146        int decimals = findCurrency(currency).getDecimals();
147        return new BigDecimal(isoamount).movePointLeft(decimals);
148    }
149
150    /**
151     * Adds a resource bundle for currency definitions.
152     * @param bundleName the bundle name to add
153     */
154    
155    public static void addBundle(String bundleName)
156    {
157        ResourceBundle r = ResourceBundle.getBundle(bundleName);
158        addBundle(r);
159    }
160
161    /**
162     * Converts an amount to an ISO Amount taking in consideration
163     * the number of decimal digits according to currency
164     *
165     * @param amount   - The amount to be converted
166     * @param currency - The ISO currency to be converted (eg. ISOField 49)
167     * @return result - An iso amount representing the converted field
168     * @throws IllegalArgumentException if we fail to convert the amount
169     */
170    public static String convertToIsoMsg(double amount, String currency) throws IllegalArgumentException
171    {
172        return findCurrency(currency).formatAmountForISOMsg(amount);
173    }
174
175    /**
176     * Decomposes a composed currency string into its components.
177     * @param incurr the composed currency string
178     * @return an Object array with the decomposed parts
179     * @throws IllegalArgumentException if the currency is invalid
180     */
181    
182    public static Object[] decomposeComposedCurrency(String incurr) throws IllegalArgumentException
183    {
184        final String[] strings = incurr.split(" ");
185        if (strings.length != 2)
186        {
187            throw new IllegalArgumentException("Invalid parameter: " + incurr);
188        }
189        return new Object[]{strings[0], Double.valueOf(strings[1])};
190    }
191
192    /**
193     * Returns the ISO numeric code for the given alpha currency code.
194     * @param alphacode the 3-letter alpha currency code
195     * @return the ISO numeric code as a string
196     * @throws IllegalArgumentException if the code is unknown
197     */
198    
199    public static String getIsoCodeFromAlphaCode(String alphacode) throws IllegalArgumentException
200    {
201        try
202        {
203            Currency c = findCurrency(alphacode);
204            return ISOUtil.zeropad(Integer.toString(c.getIsoCode()), 3);
205        }
206        catch (ISOException e)
207        {
208            throw new IllegalArgumentException("Failed getIsoCodeFromAlphaCode/ zeropad failed?", e);
209        }
210    }
211
212    /**
213     * Returns the Currency for the given numeric ISO 4217 code.
214     * @param code the numeric currency code
215     * @return the corresponding Currency
216     * @throws ISOException if the code is unknown
217     */
218    
219    public static Currency getCurrency(int code) throws ISOException
220    {
221        final String isoCode = ISOUtil.zeropad(Integer.toString(code), 3);
222        return findCurrency(isoCode);
223    }
224
225    /**
226     * Returns the Currency for the given string code.
227     * @param code the currency code (numeric or alpha)
228     * @return the corresponding Currency
229     * @throws ISOException if the code is unknown
230     */
231    
232    public static Currency getCurrency(String code) throws ISOException
233    {
234        final String isoCode = ISOUtil.zeropad(code, 3);
235        return findCurrency(isoCode);
236    }
237
238    private static InputStream loadResourceAsStream(String name)
239    {
240        InputStream in = null;
241
242        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
243        if (contextClassLoader != null)
244        {
245            in = contextClassLoader.getResourceAsStream(name);
246        }
247        if (in == null)
248        {
249            in = ISOCurrency.class.getClassLoader().getResourceAsStream(name);
250        }
251        return in;
252    }
253
254    /**
255     * Should be called like this: put("ALL", "008", 2);
256     * Note: the second parameter is zero padded to three digits
257     *
258     * @param alphaCode   An alphabetic code such as USD
259     * @param isoCode     An ISO code such as 840
260     * @param numDecimals the number of implied decimals
261     */
262    private static void addCurrency(String alphaCode, String isoCode, int numDecimals)
263    {
264        // to allow a clean replacement from a more specific resource bundle we
265        // require clearing instead of overriding.
266        if(currencies.containsKey(alphaCode) || currencies.containsKey(isoCode))
267        {
268            currencies.remove(alphaCode);
269            currencies.remove(isoCode);
270        }
271        Currency ccy = new Currency(alphaCode, Integer.parseInt(isoCode), numDecimals);
272        currencies.put(alphaCode, ccy);
273        currencies.put(isoCode, ccy);
274    }
275
276    private static Currency findCurrency(String currency)
277    {
278        final Currency c = currencies.get(currency.toUpperCase());
279        if (c == null)
280        {
281            throw new IllegalArgumentException("Currency with key '" + currency + "' was not found");
282        }
283        return c;
284    }
285
286    private static void addBundle(ResourceBundle r)
287    {
288        Enumeration en = r.getKeys();
289        while (en.hasMoreElements())
290        {
291            String alphaCode = (String) en.nextElement();
292            String[] tmp = r.getString(alphaCode).split(" ");
293            String isoCode = tmp[0];
294            int numDecimals = Integer.parseInt(tmp[1]);
295            addCurrency(alphaCode, isoCode, numDecimals);
296        }
297    }
298}