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