001/**
002 * Copyright (c) 2012, 2014, Credit Suisse (Anatole Tresch), Werner Keil and others by the @author tag.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005 * use this file except in compliance with the License. You may obtain a copy of
006 * the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013 * License for the specific language governing permissions and limitations under
014 * the License.
015 */
016package org.javamoney.moneta.convert.internal;
017
018import java.io.BufferedReader;
019import java.io.IOException;
020import java.io.InputStream;
021import java.io.InputStreamReader;
022import java.net.MalformedURLException;
023import java.text.DecimalFormat;
024import java.text.NumberFormat;
025import java.text.ParseException;
026import java.text.SimpleDateFormat;
027import java.util.*;
028import java.util.logging.Level;
029
030import javax.money.CurrencyContextBuilder;
031import javax.money.CurrencyUnit;
032import javax.money.MonetaryCurrencies;
033import javax.money.convert.*;
034import javax.money.spi.Bootstrap;
035
036import org.javamoney.moneta.CurrencyUnitBuilder;
037import org.javamoney.moneta.ExchangeRateBuilder;
038import org.javamoney.moneta.spi.AbstractRateProvider;
039import org.javamoney.moneta.spi.DefaultNumberValue;
040import org.javamoney.moneta.spi.LoaderService;
041import org.javamoney.moneta.spi.LoaderService.LoaderListener;
042
043/**
044 * Implements a {@link ExchangeRateProvider} that loads the IMF conversion data.
045 * In most cases this provider will provide chained rates, since IMF always is
046 * converting from/to the IMF <i>SDR</i> currency unit.
047 *
048 * @author Anatole Tresch
049 * @author Werner Keil
050 */
051public class IMFRateProvider extends AbstractRateProvider implements LoaderListener {
052
053    /**
054     * The data id used for the LoaderService.
055     */
056    private static final String DATA_ID = IMFRateProvider.class.getSimpleName();
057    /**
058     * The {@link ConversionContext} of this provider.
059     */
060    private static final ProviderContext CONTEXT = ProviderContextBuilder.of("IMF", RateType.DEFERRED)
061            .set("providerDescription", "International Monetary Fond").set("days", 1).build();
062
063    private static final CurrencyUnit SDR =
064            CurrencyUnitBuilder.of("SDR", CurrencyContextBuilder.of(IMFRateProvider.class.getSimpleName()).build())
065                    .setDefaultFractionDigits(3).build(true);
066
067    private Map<CurrencyUnit, List<ExchangeRate>> currencyToSdr = new HashMap<>();
068
069    private Map<CurrencyUnit, List<ExchangeRate>> sdrToCurrency = new HashMap<>();
070
071    private static Map<String, CurrencyUnit> currenciesByName = new HashMap<>();
072
073    static {
074        for (Currency currency : Currency.getAvailableCurrencies()) {
075            currenciesByName.put(currency.getDisplayName(Locale.ENGLISH),
076                    MonetaryCurrencies.getCurrency(currency.getCurrencyCode()));
077        }
078        // Additional IMF differing codes:
079        // This mapping is required to fix data issues in the input stream, it has nothing to do with i18n
080        currenciesByName.put("U.K. Pound Sterling", MonetaryCurrencies.getCurrency("GBP"));
081        currenciesByName.put("U.S. Dollar", MonetaryCurrencies.getCurrency("USD"));
082        currenciesByName.put("Bahrain Dinar", MonetaryCurrencies.getCurrency("BHD"));
083        currenciesByName.put("Botswana Pula", MonetaryCurrencies.getCurrency("BWP"));
084        currenciesByName.put("Czech Koruna", MonetaryCurrencies.getCurrency("CZK"));
085        currenciesByName.put("Icelandic Krona", MonetaryCurrencies.getCurrency("ISK"));
086        currenciesByName.put("Korean Won", MonetaryCurrencies.getCurrency("KRW"));
087        currenciesByName.put("Rial Omani", MonetaryCurrencies.getCurrency("OMR"));
088        currenciesByName.put("Nuevo Sol", MonetaryCurrencies.getCurrency("PEN"));
089        currenciesByName.put("Qatar Riyal", MonetaryCurrencies.getCurrency("QAR"));
090        currenciesByName.put("Saudi Arabian Riyal", MonetaryCurrencies.getCurrency("SAR"));
091        currenciesByName.put("Sri Lanka Rupee", MonetaryCurrencies.getCurrency("LKR"));
092        currenciesByName.put("Trinidad And Tobago Dollar", MonetaryCurrencies.getCurrency("TTD"));
093        currenciesByName.put("U.A.E. Dirham", MonetaryCurrencies.getCurrency("AED"));
094        currenciesByName.put("Peso Uruguayo", MonetaryCurrencies.getCurrency("UYU"));
095        currenciesByName.put("Bolivar Fuerte", MonetaryCurrencies.getCurrency("VEF"));
096    }
097
098    public IMFRateProvider() throws MalformedURLException {
099        super(CONTEXT);
100        LoaderService loader = Bootstrap.getService(LoaderService.class);
101        loader.addLoaderListener(this, DATA_ID);
102        loader.loadDataAsync(DATA_ID);
103    }
104
105    @Override
106    public void newDataLoaded(String data, InputStream is) {
107        try {
108            loadRatesTSV(is);
109        } catch (Exception e) {
110            LOGGER.log(Level.SEVERE, "Error", e);
111        }
112    }
113
114    private void loadRatesTSV(InputStream inputStream) throws IOException, ParseException {
115        Map<CurrencyUnit, List<ExchangeRate>> newCurrencyToSdr = new HashMap<>();
116        Map<CurrencyUnit, List<ExchangeRate>> newSdrToCurrency = new HashMap<>();
117        NumberFormat f = new DecimalFormat("#0.0000000000");
118        f.setGroupingUsed(false);
119        BufferedReader pr = new BufferedReader(new InputStreamReader(inputStream));
120        String line = pr.readLine();
121        // int lineType = 0;
122        boolean currencyToSdr = true;
123        // SDRs per Currency unit (2)
124        //
125        // Currency January 31, 2013 January 30, 2013 January 29, 2013
126        // January 28, 2013 January 25, 2013
127        // Euro 0.8791080000 0.8789170000 0.8742470000 0.8752180000
128        // 0.8768020000
129
130        // Currency units per SDR(3)
131        //
132        // Currency January 31, 2013 January 30, 2013 January 29, 2013
133        // January 28, 2013 January 25, 2013
134        // Euro 1.137520 1.137760 1.143840 1.142570 1.140510
135        List<Long> timestamps = null;
136        while (Objects.nonNull(line)) {
137            if (line.trim().isEmpty()) {
138                line = pr.readLine();
139                continue;
140            }
141            if (line.startsWith("SDRs per Currency unit")) {
142                currencyToSdr = false;
143                line = pr.readLine();
144                continue;
145            } else if (line.startsWith("Currency units per SDR")) {
146                currencyToSdr = true;
147                line = pr.readLine();
148                continue;
149            } else if (line.startsWith("Currency")) {
150                timestamps = readTimestamps(line);
151                line = pr.readLine();
152                continue;
153            }
154            String[] parts = line.split("\\t");
155            CurrencyUnit currency = currenciesByName.get(parts[0]);
156            if (Objects.isNull(currency)) {
157                LOGGER.warning("Unknown currency from, IMF data feed: " + parts[0]);
158                line = pr.readLine();
159                continue;
160            }
161            Double[] values = parseValues(f, parts);
162            for (int i = 0; i < values.length; i++) {
163                if (Objects.isNull(values[i])) {
164                    continue;
165                }
166                Long fromTS = timestamps != null ? timestamps.get(i) : null;
167                if (fromTS == null) {
168                    continue;
169                }
170                Long toTS = fromTS + 3600L * 1000L * 24L; // One day
171                RateType rateType = RateType.HISTORIC;
172                if (toTS > System.currentTimeMillis()) {
173                    rateType = RateType.DEFERRED;
174                }
175                if (currencyToSdr) { // Currency -> SDR
176                    List<ExchangeRate> rates = this.currencyToSdr.get(currency);
177                    if (Objects.isNull(rates)) {
178                        rates = new ArrayList<>(5);
179                        newCurrencyToSdr.put(currency, rates);
180                    }
181                    ExchangeRate rate = new ExchangeRateBuilder(
182                            ConversionContextBuilder.create(CONTEXT, rateType).setTimestampMillis(toTS).build())
183                            .setBase(currency).setTerm(SDR).setFactor(new DefaultNumberValue(values[i])).build();
184                    rates.add(rate);
185                } else { // SDR -> Currency
186                    List<ExchangeRate> rates = this.sdrToCurrency.get(currency);
187                    if (Objects.isNull(rates)) {
188                        rates = new ArrayList<>(5);
189                        newSdrToCurrency.put(currency, rates);
190                    }
191                    ExchangeRate rate = new ExchangeRateBuilder(
192                            ConversionContextBuilder.create(CONTEXT, rateType).setTimestampMillis(fromTS).build())
193                            .setBase(SDR).setTerm(currency).setFactor(DefaultNumberValue.of(values[i])).build();
194                    rates.add(rate);
195                }
196            }
197            line = pr.readLine();
198        }
199        // Cast is save, since contained DefaultExchangeRate is Comparable!
200        newSdrToCurrency.values().forEach((c) -> Collections.sort(List.class.cast(c)));
201        newCurrencyToSdr.values().forEach((c) -> Collections.sort(List.class.cast(c)));
202        this.sdrToCurrency = newSdrToCurrency;
203        this.currencyToSdr = newCurrencyToSdr;
204    }
205
206    private Double[] parseValues(NumberFormat f, String[] parts) throws ParseException {
207        Double[] result = new Double[parts.length - 1];
208        for (int i = 1; i < parts.length; i++) {
209            if (parts[i].isEmpty()) {
210                continue;
211            }
212            result[i - 1] = f.parse(parts[i]).doubleValue();
213        }
214        return result;
215    }
216
217    private List<Long> readTimestamps(String line) throws ParseException {
218        // Currency May 01, 2013 April 30, 2013 April 29, 2013 April 26, 2013
219        // April 25, 2013
220        SimpleDateFormat sdf = new SimpleDateFormat("MMM DD, yyyy", Locale.ENGLISH);
221        String[] parts = line.split("\\\t");
222        List<Long> dates = new ArrayList<>(parts.length);
223        for (int i = 1; i < parts.length; i++) {
224            dates.add(sdf.parse(parts[i]).getTime());
225        }
226        return dates;
227    }
228
229    public ExchangeRate getExchangeRate(ConversionQuery conversionQuery) {
230        if (!isAvailable(conversionQuery)) {
231            return null;
232        }
233        CurrencyUnit base = conversionQuery.getBaseCurrency();
234        CurrencyUnit term = conversionQuery.getCurrency();
235        Long timestamp = conversionQuery.getTimestampMillis();
236        ExchangeRate rate1 = lookupRate(currencyToSdr.get(base), timestamp);
237        ExchangeRate rate2 = lookupRate(sdrToCurrency.get(term), timestamp);
238        if (base.equals(SDR)) {
239            return rate2;
240        } else if (term.equals(SDR)) {
241            return rate1;
242        }
243        if (Objects.isNull(rate1) || Objects.isNull(rate2)) {
244            return null;
245        }
246        ExchangeRateBuilder builder =
247                new ExchangeRateBuilder(ConversionContext.of(CONTEXT.getProvider(), RateType.HISTORIC));
248        builder.setBase(base);
249        builder.setTerm(term);
250        builder.setFactor(multiply(rate1.getFactor(), rate2.getFactor()));
251        builder.setRateChain(rate1, rate2);
252        return builder.build();
253    }
254
255    private ExchangeRate lookupRate(List<ExchangeRate> list, Long timestamp) {
256        if (Objects.isNull(list)) {
257            return null;
258        }
259        ExchangeRate found = null;
260        for (ExchangeRate rate : list) {
261            if (Objects.isNull(timestamp)) {
262                timestamp = System.currentTimeMillis();
263            }
264            if (isValid(rate.getConversionContext(), timestamp)) {
265                return rate;
266            }
267            if (Objects.isNull(found)) {
268                found = rate;
269            }
270        }
271        return found;
272    }
273
274    private boolean isValid(ConversionContext conversionContext, Long timestamp) {
275        Long validFrom = conversionContext.getLong("validFrom");
276        Long validTo = conversionContext.getLong("validTo");
277        return !(Objects.nonNull(validFrom) && validFrom > timestamp) &&
278                !(Objects.nonNull(validTo) && validTo < timestamp);
279    }
280
281}