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