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}