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