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.time.LocalDate;
027import java.time.LocalDateTime;
028import java.time.format.DateTimeFormatter;
029import java.util.ArrayList;
030import java.util.Collections;
031import java.util.Currency;
032import java.util.HashMap;
033import java.util.List;
034import java.util.Locale;
035import java.util.Map;
036import java.util.Objects;
037import java.util.logging.Level;
038
039import javax.money.CurrencyContextBuilder;
040import javax.money.CurrencyUnit;
041import javax.money.MonetaryCurrencies;
042import javax.money.convert.ConversionContext;
043import javax.money.convert.ConversionContextBuilder;
044import javax.money.convert.ConversionQuery;
045import javax.money.convert.ExchangeRate;
046import javax.money.convert.ExchangeRateProvider;
047import javax.money.convert.ProviderContext;
048import javax.money.convert.ProviderContextBuilder;
049import javax.money.convert.RateType;
050import javax.money.spi.Bootstrap;
051
052import org.javamoney.moneta.CurrencyUnitBuilder;
053import org.javamoney.moneta.ExchangeRateBuilder;
054import org.javamoney.moneta.spi.AbstractRateProvider;
055import org.javamoney.moneta.spi.DefaultNumberValue;
056import org.javamoney.moneta.spi.LoaderService;
057import org.javamoney.moneta.spi.LoaderService.LoaderListener;
058
059/**
060 * Implements a {@link ExchangeRateProvider} that loads the IMF conversion data.
061 * In most cases this provider will provide chained rates, since IMF always is
062 * converting from/to the IMF <i>SDR</i> currency unit.
063 *
064 * @author Anatole Tresch
065 * @author Werner Keil
066 */
067public class IMFRateProvider extends AbstractRateProvider implements LoaderListener {
068
069    /**
070     * The data id used for the LoaderService.
071     */
072    private static final String DATA_ID = IMFRateProvider.class.getSimpleName();
073    /**
074     * The {@link ConversionContext} of this provider.
075     */
076    private static final ProviderContext CONTEXT = ProviderContextBuilder.of("IMF", RateType.DEFERRED)
077            .set("providerDescription", "International Monetary Fond").set("days", 1).build();
078
079    private static final CurrencyUnit SDR =
080            CurrencyUnitBuilder.of("SDR", CurrencyContextBuilder.of(IMFRateProvider.class.getSimpleName()).build())
081                    .setDefaultFractionDigits(3).build(true);
082
083    private Map<CurrencyUnit, List<ExchangeRate>> currencyToSdr = new HashMap<>();
084
085    private Map<CurrencyUnit, List<ExchangeRate>> sdrToCurrency = new HashMap<>();
086
087    private static Map<String, CurrencyUnit> currenciesByName = new HashMap<>();
088
089    static {
090        for (Currency currency : Currency.getAvailableCurrencies()) {
091            currenciesByName.put(currency.getDisplayName(Locale.ENGLISH),
092                    MonetaryCurrencies.getCurrency(currency.getCurrencyCode()));
093        }
094        // Additional IMF differing codes:
095        // This mapping is required to fix data issues in the input stream, it has nothing to do with i18n
096        currenciesByName.put("U.K. Pound Sterling", MonetaryCurrencies.getCurrency("GBP"));
097        currenciesByName.put("U.S. Dollar", MonetaryCurrencies.getCurrency("USD"));
098        currenciesByName.put("Bahrain Dinar", MonetaryCurrencies.getCurrency("BHD"));
099        currenciesByName.put("Botswana Pula", MonetaryCurrencies.getCurrency("BWP"));
100        currenciesByName.put("Czech Koruna", MonetaryCurrencies.getCurrency("CZK"));
101        currenciesByName.put("Icelandic Krona", MonetaryCurrencies.getCurrency("ISK"));
102        currenciesByName.put("Korean Won", MonetaryCurrencies.getCurrency("KRW"));
103        currenciesByName.put("Rial Omani", MonetaryCurrencies.getCurrency("OMR"));
104        currenciesByName.put("Nuevo Sol", MonetaryCurrencies.getCurrency("PEN"));
105        currenciesByName.put("Qatar Riyal", MonetaryCurrencies.getCurrency("QAR"));
106        currenciesByName.put("Saudi Arabian Riyal", MonetaryCurrencies.getCurrency("SAR"));
107        currenciesByName.put("Sri Lanka Rupee", MonetaryCurrencies.getCurrency("LKR"));
108        currenciesByName.put("Trinidad And Tobago Dollar", MonetaryCurrencies.getCurrency("TTD"));
109        currenciesByName.put("U.A.E. Dirham", MonetaryCurrencies.getCurrency("AED"));
110        currenciesByName.put("Peso Uruguayo", MonetaryCurrencies.getCurrency("UYU"));
111        currenciesByName.put("Bolivar Fuerte", MonetaryCurrencies.getCurrency("VEF"));
112    }
113
114    public IMFRateProvider() throws MalformedURLException {
115        super(CONTEXT);
116        LoaderService loader = Bootstrap.getService(LoaderService.class);
117        loader.addLoaderListener(this, DATA_ID);
118        try {
119            loader.loadData(DATA_ID);
120        } catch (IOException e) {
121            LOGGER.log(Level.WARNING, "Error loading initial data from IMF provider...", e);
122        }
123    }
124
125    @Override
126    public void newDataLoaded(String data, InputStream is) {
127        try {
128            loadRatesTSV(is);
129        } catch (Exception e) {
130            LOGGER.log(Level.SEVERE, "Error", e);
131        }
132    }
133
134    @SuppressWarnings("unchecked")
135    private void loadRatesTSV(InputStream inputStream) throws IOException, ParseException {
136        Map<CurrencyUnit, List<ExchangeRate>> newCurrencyToSdr = new HashMap<>();
137        Map<CurrencyUnit, List<ExchangeRate>> newSdrToCurrency = new HashMap<>();
138        NumberFormat f = new DecimalFormat("#0.0000000000");
139        f.setGroupingUsed(false);
140        BufferedReader pr = new BufferedReader(new InputStreamReader(inputStream));
141        String line = pr.readLine();
142        // int lineType = 0;
143        boolean currencyToSdr = true;
144        // SDRs per Currency unit (2)
145        //
146        // Currency January 31, 2013 January 30, 2013 January 29, 2013
147        // January 28, 2013 January 25, 2013
148        // Euro 0.8791080000 0.8789170000 0.8742470000 0.8752180000
149        // 0.8768020000
150
151        // Currency units per SDR(3)
152        //
153        // Currency January 31, 2013 January 30, 2013 January 29, 2013
154        // January 28, 2013 January 25, 2013
155        // Euro 1.137520 1.137760 1.143840 1.142570 1.140510
156        List<LocalDate> timestamps = null;
157        while (Objects.nonNull(line)) {
158            if (line.trim().isEmpty()) {
159                line = pr.readLine();
160                continue;
161            }
162            if (line.startsWith("SDRs per Currency unit")) {
163                currencyToSdr = false;
164                line = pr.readLine();
165                continue;
166            } else if (line.startsWith("Currency units per SDR")) {
167                currencyToSdr = true;
168                line = pr.readLine();
169                continue;
170            } else if (line.startsWith("Currency")) {
171                timestamps = readTimestamps(line);
172                line = pr.readLine();
173                continue;
174            }
175            String[] parts = line.split("\\t");
176            CurrencyUnit currency = currenciesByName.get(parts[0]);
177            if (Objects.isNull(currency)) {
178                LOGGER.finest(() -> "Uninterpretable data from IMF data feed: " + parts[0]);
179                line = pr.readLine();
180                continue;
181            }
182            Double[] values = parseValues(f, parts);
183            for (int i = 0; i < values.length; i++) {
184                if (Objects.isNull(values[i])) {
185                    continue;
186                }
187                LocalDate fromTS = timestamps != null ? timestamps.get(i) : null;
188                if (fromTS == null) {
189                    continue;
190                }
191                RateType rateType = RateType.HISTORIC;
192                if (fromTS.equals(LocalDate.now())) {
193                    rateType = RateType.DEFERRED;
194                }
195                if (currencyToSdr) { // Currency -> SDR
196                    ExchangeRate rate = new ExchangeRateBuilder(
197                            ConversionContextBuilder.create(CONTEXT, rateType).set(fromTS).build())
198                            .setBase(currency).setTerm(SDR).setFactor(new DefaultNumberValue(1d / values[i])).build();
199                    List<ExchangeRate> rates = newCurrencyToSdr.computeIfAbsent(currency, c -> new ArrayList<>(5));
200                    rates.add(rate);
201                } else { // SDR -> Currency
202                    ExchangeRate rate = new ExchangeRateBuilder(
203                            ConversionContextBuilder.create(CONTEXT, rateType).set(fromTS).build())
204                            .setBase(SDR).setTerm(currency).setFactor(DefaultNumberValue.of(1d / values[i])).build();
205                    List<ExchangeRate> rates = newSdrToCurrency.computeIfAbsent(currency, (c) -> new ArrayList<>(5));
206                    rates.add(rate);
207                }
208            }
209            line = pr.readLine();
210        }
211        // Cast is save, since contained DefaultExchangeRate is Comparable!
212        newSdrToCurrency.values().forEach((c) -> Collections.sort(List.class.cast(c)));
213        newCurrencyToSdr.values().forEach((c) -> Collections.sort(List.class.cast(c)));
214        this.sdrToCurrency = newSdrToCurrency;
215        this.currencyToSdr = newCurrencyToSdr;
216        this.sdrToCurrency.forEach((c, l) -> LOGGER.finest(() -> "SDR -> " + c.getCurrencyCode() + ": " + l));
217        this.currencyToSdr.forEach((c, l) -> LOGGER.finest(() -> c.getCurrencyCode() + " -> SDR: " + l));
218    }
219
220    private Double[] parseValues(NumberFormat f, String[] parts) throws ParseException {
221        Double[] result = new Double[parts.length - 1];
222        for (int i = 1; i < parts.length; i++) {
223            if (parts[i].isEmpty()) {
224                continue;
225            }
226            result[i - 1] = f.parse(parts[i]).doubleValue();
227        }
228        return result;
229    }
230
231    private List<LocalDate> readTimestamps(String line) throws ParseException {
232        // Currency May 01, 2013 April 30, 2013 April 29, 2013 April 26, 2013
233        // April 25, 2013
234        DateTimeFormatter sdf = DateTimeFormatter.ofPattern("MMMM dd, uuuu").withLocale(Locale.ENGLISH);
235        String[] parts = line.split("\\\t");
236        List<LocalDate> dates = new ArrayList<>(parts.length);
237        for (int i = 1; i < parts.length; i++) {
238            dates.add(LocalDate.parse(parts[i], sdf));
239        }
240        return dates;
241    }
242
243    @Override
244    public ExchangeRate getExchangeRate(ConversionQuery conversionQuery) {
245        if (!isAvailable(conversionQuery)) {
246            return null;
247        }
248        CurrencyUnit base = conversionQuery.getBaseCurrency();
249        CurrencyUnit term = conversionQuery.getCurrency();
250        LocalDate timestamp = conversionQuery.get(LocalDate.class);
251        if (timestamp == null) {
252            LocalDateTime dateTime = conversionQuery.get(LocalDateTime.class);
253            if (dateTime != null) {
254                timestamp = dateTime.toLocalDate();
255            }
256        }
257        ExchangeRate rate1 = lookupRate(currencyToSdr.get(base), timestamp);
258        ExchangeRate rate2 = lookupRate(sdrToCurrency.get(term), timestamp);
259        if (base.equals(SDR)) {
260            return rate2;
261        } else if (term.equals(SDR)) {
262            return rate1;
263        }
264        if (Objects.isNull(rate1) || Objects.isNull(rate2)) {
265            return null;
266        }
267        ExchangeRateBuilder builder =
268                new ExchangeRateBuilder(ConversionContext.of(CONTEXT.getProviderName(), RateType.HISTORIC));
269        builder.setBase(base);
270        builder.setTerm(term);
271        builder.setFactor(multiply(rate1.getFactor(), rate2.getFactor()));
272        builder.setRateChain(rate1, rate2);
273        return builder.build();
274    }
275
276    private ExchangeRate lookupRate(List<ExchangeRate> list, LocalDate localDate) {
277        if (Objects.isNull(list)) {
278            return null;
279        }
280        ExchangeRate found = null;
281        for (ExchangeRate rate : list) {
282            if (Objects.isNull(localDate)) {
283                localDate = LocalDate.now();
284            }
285            if (isValid(rate.getContext(), localDate)) {
286                return rate;
287            }
288            if (Objects.isNull(found)) {
289                found = rate;
290            }
291        }
292        return found;
293    }
294
295    private boolean isValid(ConversionContext conversionContext, LocalDate timestamp) {
296        LocalDate validAt = conversionContext.get(LocalDate.class);
297        return !(Objects.nonNull(validAt)) && validAt.equals(timestamp);
298    }
299
300}