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