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;
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.concurrent.CountDownLatch;
036import java.util.concurrent.TimeUnit;
037import java.util.logging.Level;
038
039import javax.money.CurrencyContextBuilder;
040import javax.money.CurrencyUnit;
041import javax.money.Monetary;
042import javax.money.MonetaryException;
043import javax.money.convert.ConversionContext;
044import javax.money.convert.ConversionContextBuilder;
045import javax.money.convert.ConversionQuery;
046import javax.money.convert.ExchangeRate;
047import javax.money.convert.ExchangeRateProvider;
048import javax.money.convert.ProviderContext;
049import javax.money.convert.ProviderContextBuilder;
050import javax.money.convert.RateType;
051import javax.money.spi.Bootstrap;
052
053import org.javamoney.moneta.CurrencyUnitBuilder;
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 Fund").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    protected volatile String loadState;
088
089    protected volatile CountDownLatch loadLock = new CountDownLatch(1);
090
091    private static final Map<String, CurrencyUnit> currenciesByName = new HashMap<>();
092
093    static {
094        for (Currency currency : Currency.getAvailableCurrencies()) {
095            currenciesByName.put(currency.getDisplayName(Locale.ENGLISH).toLowerCase(Locale.ENGLISH),
096                    Monetary.getCurrency(currency.getCurrencyCode()));
097        }
098        // Additional IMF differing codes:
099        // This mapping is required to fix data issues in the input stream, it has nothing to do with i18n
100        currenciesByName.put("U.K. pound".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("GBP"));
101        currenciesByName.put("U.S. dollar".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("USD"));
102        currenciesByName.put("Bahrain dinar".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("BHD"));
103        currenciesByName.put("Botswana pula".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("BWP"));
104        currenciesByName.put("Czech koruna".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("CZK"));
105        currenciesByName.put("Icelandic krona".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("ISK"));
106        currenciesByName.put("Korean won".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("KRW"));
107        currenciesByName.put("Omani rial".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("OMR"));
108        currenciesByName.put("Peruvian sol".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("PEN"));
109        currenciesByName.put("Qatari riyal".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("QAR"));
110        currenciesByName.put("Saudi Arabian riyal".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("SAR"));
111        currenciesByName.put("Sri Lankan rupee".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("LKR"));
112        currenciesByName.put("Trinidadian dollar".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("TTD"));
113        currenciesByName.put("U.A.E. dirham".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("AED"));
114        currenciesByName.put("Uruguayan peso".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("UYU"));
115        currenciesByName.put("Bolivar Fuerte".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("VEF"));
116    }
117
118    public IMFRateProvider() {
119        super(CONTEXT);
120        LoaderService loader = Bootstrap.getService(LoaderService.class);
121        loader.addLoaderListener(this, DATA_ID);
122        try {
123            loader.loadData(DATA_ID);
124        } catch (IOException e) {
125            LOGGER.log(Level.WARNING, "Error loading initial data from IMF provider...", e);
126        }
127    }
128
129    @Override
130    public void newDataLoaded(String data, InputStream is) {
131        try {
132            int oldSize = this.sdrToCurrency.size();
133            loadRatesTSV(is);
134            int newSize = this.sdrToCurrency.size();
135            loadState = "Loaded " + DATA_ID + " exchange rates for days:" + (newSize - oldSize);
136            LOGGER.info(loadState);
137            loadLock.countDown();
138        } catch (Exception e) {
139            loadState = "Last Error during data load: " + e.getMessage();
140            throw new IllegalArgumentException("Failed to load IMF data provided.", e);
141        }
142    }
143
144    @SuppressWarnings("unchecked")
145    private void loadRatesTSV(InputStream inputStream) throws IOException, ParseException {
146        Map<CurrencyUnit, List<ExchangeRate>> newCurrencyToSdr = new HashMap<>();
147        Map<CurrencyUnit, List<ExchangeRate>> newSdrToCurrency = new HashMap<>();
148        NumberFormat f = new DecimalFormat("#0.0000000000");
149        f.setGroupingUsed(false);
150        BufferedReader pr = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
151        String line = pr.readLine();
152        if(line.contains("Request Rejected")){
153            throw new IOException("Request has been rejected by IMF server.");
154        }
155        // int lineType = 0;
156        boolean currencyToSdr = true;
157        // SDRs per Currency unit (2)
158        //
159        // Currency January 31, 2013 January 30, 2013 January 29, 2013
160        // January 28, 2013 January 25, 2013
161        // Euro 0.8791080000 0.8789170000 0.8742470000 0.8752180000
162        // 0.8768020000
163
164        // Currency units per SDR(3)
165        //
166        // Currency January 31, 2013 January 30, 2013 January 29, 2013
167        // January 28, 2013 January 25, 2013
168        // Euro 1.137520 1.137760 1.143840 1.142570 1.140510
169        List<LocalDate> timestamps = null;
170        while (line!=null) {
171            if (line.trim().isEmpty()) {
172                line = pr.readLine();
173                continue;
174            }
175            if (line.startsWith("SDRs per Currency unit")) {
176                currencyToSdr = false;
177                line = pr.readLine();
178                continue;
179            } else if (line.startsWith("Currency units per SDR")) {
180                currencyToSdr = true;
181                line = pr.readLine();
182                continue;
183            } else if (line.startsWith("Currency")) {
184                timestamps = readTimestamps(line);
185                line = pr.readLine();
186                continue;
187            }
188            String[] parts = line.split("\\t");
189            CurrencyUnit currency = currenciesByName.get(parts[0].toLowerCase(Locale.ENGLISH));
190            if (currency==null) {
191                LOGGER.finest("Uninterpretable data from IMF data feed: " + parts[0]);
192                line = pr.readLine();
193                continue;
194            }
195            Double[] values = parseValues(parts);
196            for (int i = 0; i < values.length; i++) {
197                if (values[i]==null) {
198                    continue;
199                }
200                LocalDate fromTS = timestamps != null ? timestamps.get(i) : null;
201                if (fromTS == null) {
202                    continue;
203                }
204                RateType rateType = RateType.HISTORIC;
205                if (fromTS.equals(LocalDate.now())) {
206                    rateType = RateType.DEFERRED;
207                }
208                if (currencyToSdr) { // Currency -> SDR
209                    ExchangeRate rate = new ExchangeRateBuilder(
210                            ConversionContextBuilder.create(CONTEXT, rateType).set(fromTS).build())
211                            .setBase(currency).setTerm(SDR).setFactor(new DefaultNumberValue(1d / values[i])).build();
212                    List<ExchangeRate> rates = newCurrencyToSdr.get(currency);
213                    if(rates==null){
214                        rates = new ArrayList<>(5);
215                        newCurrencyToSdr.put(currency,rates);
216                    }
217                    rates.add(rate);
218                } else { // SDR -> Currency
219                    ExchangeRate rate = new ExchangeRateBuilder(
220                            ConversionContextBuilder.create(CONTEXT, rateType).set(fromTS)
221                                    .set("LocalTime",fromTS.toString()).build())
222                                    .setBase(SDR).setTerm(currency)
223                                    .setFactor(DefaultNumberValue.of(1d / values[i])).build();
224                    List<ExchangeRate> rates = newSdrToCurrency.get(currency);
225                    if(rates==null){
226                        rates = new ArrayList<>(5);
227                        newSdrToCurrency.put(currency,rates);
228                    }
229                    rates.add(rate);
230                }
231            }
232            line = pr.readLine();
233        }
234        // Cast is save, since contained DefaultExchangeRate is Comparable!
235        for(List<ExchangeRate> list:newSdrToCurrency.values()){
236            Collections.sort(List.class.cast(list));
237        }
238        for(List<ExchangeRate> list:newCurrencyToSdr.values()){
239            Collections.sort(List.class.cast(list));
240        }
241        this.sdrToCurrency = newSdrToCurrency;
242        this.currencyToSdr = newCurrencyToSdr;
243        for(Map.Entry<CurrencyUnit, List<ExchangeRate>> entry: this.sdrToCurrency.entrySet()){
244            LOGGER.finest("SDR -> " + entry.getKey().getCurrencyCode() + ": " + entry.getValue());
245        }
246        for(Map.Entry<CurrencyUnit, List<ExchangeRate>> entry: this.currencyToSdr.entrySet()){
247            LOGGER.finest(entry.getKey().getCurrencyCode() + " -> SDR: " + entry.getValue());
248        }
249    }
250
251    private Double[] parseValues(String[] parts) throws ParseException {
252
253                ArrayList<Double> result = new ArrayList<>();
254                int index = 0;
255                for (String part : parts) {
256                        if(index == 0) {
257                                index++;
258                                continue;
259                        }
260                        if (part.isEmpty() || "NA".equals(part)) {
261                                index++;
262                                result.add(null);
263                                continue;
264                        }
265                        index++;
266                        result.add(Double.valueOf(part.trim().replace(",", "")));
267                }
268                return result.toArray(new Double[parts.length - 1]);
269        }
270
271    private List<LocalDate> readTimestamps(String line) throws ParseException {
272        // Currency May 01, 2013 April 30, 2013 April 29, 2013 April 26, 2013
273        // April 25, 2013
274        SimpleDateFormat sdf = new SimpleDateFormat("MMMM dd, yyyy", Locale.ENGLISH);
275        @SuppressWarnings("Annotator") String[] parts = line.split("\\\t");
276        List<LocalDate> dates = new ArrayList<>(parts.length);
277        for (int i = 1; i < parts.length; i++) {
278            Calendar date = GregorianCalendar.getInstance();
279            date.setTime(sdf.parse(parts[i]));
280            dates.add(LocalDate.from(date));
281        }
282        return dates;
283    }
284
285    @Override
286    public ExchangeRate getExchangeRate(ConversionQuery conversionQuery) {
287        try {
288            if (loadLock.await(30, TimeUnit.SECONDS)) {
289                if (currencyToSdr.isEmpty()) {
290                    return null;
291                }
292                if (!isAvailable(conversionQuery)) {
293                    return null;
294                }
295                CurrencyUnit base = conversionQuery.getBaseCurrency();
296                CurrencyUnit term = conversionQuery.getCurrency();
297                Calendar timestamp = conversionQuery.get(Calendar.class);
298                if (timestamp == null) {
299                    timestamp = conversionQuery.get(GregorianCalendar.class);
300                }
301                ExchangeRate rate1;
302                ExchangeRate rate2;
303                LocalDate localDate;
304                if (timestamp == null) {
305                    localDate = LocalDate.yesterday();
306                    rate1 = lookupRate(currencyToSdr.get(base), localDate);
307                    rate2 = lookupRate(sdrToCurrency.get(term), localDate);
308                    if(rate1==null || rate2==null){
309                        localDate = LocalDate.beforeDays(2);
310                    }
311                    rate1 = lookupRate(currencyToSdr.get(base), localDate);
312                    rate2 = lookupRate(sdrToCurrency.get(term), localDate);
313                    if(rate1==null || rate2==null){
314                        localDate = LocalDate.beforeDays(3);
315                        rate1 = lookupRate(currencyToSdr.get(base), localDate);
316                        rate2 = lookupRate(sdrToCurrency.get(term), localDate);
317                    }
318                }
319                else{
320                    localDate = LocalDate.from(timestamp);
321                    rate1 = lookupRate(currencyToSdr.get(base), localDate);
322                    rate2 = lookupRate(sdrToCurrency.get(term), localDate);
323                }
324                if(rate1==null || rate2==null){
325                    return null;
326                }
327                if (base.equals(SDR)) {
328                    return rate2;
329                } else if (term.equals(SDR)) {
330                    return rate1;
331                }
332                ExchangeRateBuilder builder =
333                        new ExchangeRateBuilder(ConversionContext.of(CONTEXT.getProviderName(), RateType.HISTORIC));
334                builder.setBase(base);
335                builder.setTerm(term);
336                builder.setFactor(multiply(rate1.getFactor(), rate2.getFactor()));
337                builder.setRateChain(rate1, rate2);
338                return builder.build();
339            }else{
340                // Lets wait for a successful load only once, then answer requests as data is present.
341                loadLock.countDown();
342                throw new MonetaryException("Failed to load currency conversion data: " + loadState);
343            }
344        }
345        catch(InterruptedException e){
346            throw new MonetaryException("Failed to load currency conversion data: Load task has been interrupted.", e);
347        }
348    }
349
350    private ExchangeRate lookupRate(List<ExchangeRate> list, LocalDate localDate) {
351        if (list==null) {
352            return null;
353        }
354        for (ExchangeRate rate : list) {
355            if (localDate==null) {
356                localDate = LocalDate.now();
357            }
358            if (rate!=null) {
359                return rate;
360            }
361        }
362        return null;
363    }
364
365}