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 "http://www.evertype.com/standards/iso4217/iso4217-en.html"
034 *      "http://www.iso.org/iso/en/prods-services/popstds/currencycodeslist.html"
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    @SuppressWarnings({"EmptyCatchBlock"})
073    public static void loadPropertiesFromClasspath(String base)
074    {
075        InputStream in=loadResourceAsStream(base);
076        try
077        {
078            if(in!=null)
079            {
080                addBundle(new PropertyResourceBundle(in));
081            }
082        }
083        catch (IOException e)
084        {
085        }
086        finally
087        {
088            if(in!=null)
089            {
090                try
091                {
092                    in.close();
093                }
094                catch (IOException e)
095                {
096                }
097            }
098        }
099    }
100
101    /**
102     * Converts from an ISO Amount (12 digit string) to a double taking in
103     * consideration the number of decimal digits according to currency
104     *
105     * @param isoamount - The ISO amount to be converted (eg. ISOField 4)
106     * @param currency  - The ISO currency to be converted (eg. ISOField 49)
107     * @return result - A double representing the converted field
108     * @throws IllegalArgumentException if we fail to convert the amount
109     * @deprecated You should never use doubles
110     */
111    @Deprecated
112    public static double convertFromIsoMsg(String isoamount, String currency) throws IllegalArgumentException
113    {
114        Currency c = findCurrency(currency);
115        return c.parseAmountFromISOMsg(isoamount);
116    }
117    public static String toISO87String (BigDecimal amount, String currency)
118    {
119        try {
120            Currency c = findCurrency(currency);
121            return ISOUtil.zeropad(amount.movePointRight(c.getDecimals()).setScale(0).toPlainString(), 12);
122        }
123        catch (ISOException e) {
124            throw new IllegalArgumentException("Failed to convert amount",e);
125        }
126    }
127    public static BigDecimal parseFromISO87String (String isoamount, String currency) {
128        int decimals = findCurrency(currency).getDecimals();
129        return new BigDecimal(isoamount).movePointLeft(decimals);
130    }
131
132    public static void addBundle(String bundleName)
133    {
134        ResourceBundle r = ResourceBundle.getBundle(bundleName);
135        addBundle(r);
136    }
137
138    /**
139     * Converts an amount to an ISO Amount taking in consideration
140     * the number of decimal digits according to currency
141     *
142     * @param amount   - The amount to be converted
143     * @param currency - The ISO currency to be converted (eg. ISOField 49)
144     * @return result - An iso amount representing the converted field
145     * @throws IllegalArgumentException if we fail to convert the amount
146     */
147    public static String convertToIsoMsg(double amount, String currency) throws IllegalArgumentException
148    {
149        return findCurrency(currency).formatAmountForISOMsg(amount);
150    }
151
152    public static Object[] decomposeComposedCurrency(String incurr) throws IllegalArgumentException
153    {
154        final String[] strings = incurr.split(" ");
155        if (strings.length != 2)
156        {
157            throw new IllegalArgumentException("Invalid parameter: " + incurr);
158        }
159        return new Object[]{strings[0], Double.valueOf(strings[1])};
160    }
161
162    public static String getIsoCodeFromAlphaCode(String alphacode) throws IllegalArgumentException
163    {
164        try
165        {
166            Currency c = findCurrency(alphacode);
167            return ISOUtil.zeropad(Integer.toString(c.getIsoCode()), 3);
168        }
169        catch (ISOException e)
170        {
171            throw new IllegalArgumentException("Failed getIsoCodeFromAlphaCode/ zeropad failed?", e);
172        }
173    }
174
175    public static Currency getCurrency(int code) throws ISOException
176    {
177        final String isoCode = ISOUtil.zeropad(Integer.toString(code), 3);
178        return findCurrency(isoCode);
179    }
180
181    public static Currency getCurrency(String code) throws ISOException
182    {
183        final String isoCode = ISOUtil.zeropad(code, 3);
184        return findCurrency(isoCode);
185    }
186
187    private static InputStream loadResourceAsStream(String name)
188    {
189        InputStream in = null;
190
191        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
192        if (contextClassLoader != null)
193        {
194            in = contextClassLoader.getResourceAsStream(name);
195        }
196        if (in == null)
197        {
198            in = ISOCurrency.class.getClassLoader().getResourceAsStream(name);
199        }
200        return in;
201    }
202
203    /**
204     * Should be called like this: put("ALL", "008", 2);
205     * Note: the second parameter is zero padded to three digits
206     *
207     * @param alphaCode   An alphabetic code such as USD
208     * @param isoCode     An ISO code such as 840
209     * @param numDecimals the number of implied decimals
210     */
211    private static void addCurrency(String alphaCode, String isoCode, int numDecimals)
212    {
213        // to allow a clean replacement from a more specific resource bundle we
214        // require clearing instead of overriding.
215        if(currencies.containsKey(alphaCode) || currencies.containsKey(isoCode))
216        {
217            currencies.remove(alphaCode);
218            currencies.remove(isoCode);
219        }
220        Currency ccy = new Currency(alphaCode, Integer.parseInt(isoCode), numDecimals);
221        currencies.put(alphaCode, ccy);
222        currencies.put(isoCode, ccy);
223    }
224
225    private static Currency findCurrency(String currency)
226    {
227        final Currency c = currencies.get(currency.toUpperCase());
228        if (c == null)
229        {
230            throw new IllegalArgumentException("Currency with key '" + currency + "' was not found");
231        }
232        return c;
233    }
234
235    private static void addBundle(ResourceBundle r)
236    {
237        Enumeration en = r.getKeys();
238        while (en.hasMoreElements())
239        {
240            String alphaCode = (String) en.nextElement();
241            String[] tmp = r.getString(alphaCode).split(" ");
242            String isoCode = tmp[0];
243            int numDecimals = Integer.parseInt(tmp[1]);
244            addCurrency(alphaCode, isoCode, numDecimals);
245        }
246    }
247}