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}