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.internal.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.logging.Level; 036 037import javax.money.CurrencyContextBuilder; 038import javax.money.CurrencyUnit; 039import javax.money.Monetary; 040import javax.money.convert.ConversionContext; 041import javax.money.convert.ConversionContextBuilder; 042import javax.money.convert.ConversionQuery; 043import javax.money.convert.ExchangeRate; 044import javax.money.convert.ExchangeRateProvider; 045import javax.money.convert.ProviderContext; 046import javax.money.convert.ProviderContextBuilder; 047import javax.money.convert.RateType; 048import javax.money.spi.Bootstrap; 049 050import org.javamoney.moneta.CurrencyUnitBuilder; 051import org.javamoney.moneta.ExchangeRateBuilder; 052import org.javamoney.moneta.spi.AbstractRateProvider; 053import org.javamoney.moneta.spi.DefaultNumberValue; 054import org.javamoney.moneta.spi.LoaderService; 055import org.javamoney.moneta.spi.LoaderService.LoaderListener; 056 057/** 058 * Implements a {@link ExchangeRateProvider} that loads the IMF conversion data. 059 * In most cases this provider will provide chained rates, since IMF always is 060 * converting from/to the IMF <i>SDR</i> currency unit. 061 * 062 * @author Anatole Tresch 063 * @author Werner Keil 064 */ 065public class IMFRateProvider extends AbstractRateProvider implements LoaderListener { 066 067 /** 068 * The data id used for the LoaderService. 069 */ 070 private static final String DATA_ID = IMFRateProvider.class.getSimpleName(); 071 /** 072 * The {@link ConversionContext} of this provider. 073 */ 074 private static final ProviderContext CONTEXT = ProviderContextBuilder.of("IMF", RateType.DEFERRED) 075 .set("providerDescription", "International Monetary Fond").set("days", 1).build(); 076 077 private static final CurrencyUnit SDR = 078 CurrencyUnitBuilder.of("SDR", CurrencyContextBuilder.of(IMFRateProvider.class.getSimpleName()).build()) 079 .setDefaultFractionDigits(3).build(true); 080 081 private Map<CurrencyUnit, List<ExchangeRate>> currencyToSdr = new HashMap<>(); 082 083 private Map<CurrencyUnit, List<ExchangeRate>> sdrToCurrency = new HashMap<>(); 084 085 private static final Map<String, CurrencyUnit> currenciesByName = new HashMap<>(); 086 087 static { 088 for (Currency currency : Currency.getAvailableCurrencies()) { 089 currenciesByName.put(currency.getDisplayName(Locale.ENGLISH), 090 Monetary.getCurrency(currency.getCurrencyCode())); 091 } 092 // Additional IMF differing codes: 093 // This mapping is required to fix data issues in the input stream, it has nothing to do with i18n 094 currenciesByName.put("U.K. Pound Sterling", Monetary.getCurrency("GBP")); 095 currenciesByName.put("U.S. Dollar", Monetary.getCurrency("USD")); 096 currenciesByName.put("Bahrain Dinar", Monetary.getCurrency("BHD")); 097 currenciesByName.put("Botswana Pula", Monetary.getCurrency("BWP")); 098 currenciesByName.put("Czech Koruna", Monetary.getCurrency("CZK")); 099 currenciesByName.put("Icelandic Krona", Monetary.getCurrency("ISK")); 100 currenciesByName.put("Korean Won", Monetary.getCurrency("KRW")); 101 currenciesByName.put("Rial Omani", Monetary.getCurrency("OMR")); 102 currenciesByName.put("Nuevo Sol", Monetary.getCurrency("PEN")); 103 currenciesByName.put("Qatar Riyal", Monetary.getCurrency("QAR")); 104 currenciesByName.put("Saudi Arabian Riyal", Monetary.getCurrency("SAR")); 105 currenciesByName.put("Sri Lanka Rupee", Monetary.getCurrency("LKR")); 106 currenciesByName.put("Trinidad And Tobago Dollar", Monetary.getCurrency("TTD")); 107 currenciesByName.put("U.A.E. Dirham", Monetary.getCurrency("AED")); 108 currenciesByName.put("Peso Uruguayo", Monetary.getCurrency("UYU")); 109 currenciesByName.put("Bolivar Fuerte", Monetary.getCurrency("VEF")); 110 } 111 112 public IMFRateProvider() { 113 super(CONTEXT); 114 LoaderService loader = Bootstrap.getService(LoaderService.class); 115 loader.addLoaderListener(this, DATA_ID); 116 try { 117 loader.loadData(DATA_ID); 118 } catch (IOException e) { 119 LOGGER.log(Level.WARNING, "Error loading initial data from IMF provider...", e); 120 } 121 } 122 123 @Override 124 public void newDataLoaded(String data, InputStream is) { 125 try { 126 loadRatesTSV(is); 127 } catch (Exception e) { 128 LOGGER.log(Level.SEVERE, "Error", e); 129 } 130 } 131 132 @SuppressWarnings("unchecked") 133 private void loadRatesTSV(InputStream inputStream) throws IOException, ParseException { 134 Map<CurrencyUnit, List<ExchangeRate>> newCurrencyToSdr = new HashMap<>(); 135 Map<CurrencyUnit, List<ExchangeRate>> newSdrToCurrency = new HashMap<>(); 136 NumberFormat f = new DecimalFormat("#0.0000000000"); 137 f.setGroupingUsed(false); 138 BufferedReader pr = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); 139 String line = pr.readLine(); 140 // int lineType = 0; 141 boolean currencyToSdr = true; 142 // SDRs per Currency unit (2) 143 // 144 // Currency January 31, 2013 January 30, 2013 January 29, 2013 145 // January 28, 2013 January 25, 2013 146 // Euro 0.8791080000 0.8789170000 0.8742470000 0.8752180000 147 // 0.8768020000 148 149 // Currency units per SDR(3) 150 // 151 // Currency January 31, 2013 January 30, 2013 January 29, 2013 152 // January 28, 2013 January 25, 2013 153 // Euro 1.137520 1.137760 1.143840 1.142570 1.140510 154 List<LocalDate> timestamps = null; 155 while (line!=null) { 156 if (line.trim().isEmpty()) { 157 line = pr.readLine(); 158 continue; 159 } 160 if (line.startsWith("SDRs per Currency unit")) { 161 currencyToSdr = false; 162 line = pr.readLine(); 163 continue; 164 } else if (line.startsWith("Currency units per SDR")) { 165 currencyToSdr = true; 166 line = pr.readLine(); 167 continue; 168 } else if (line.startsWith("Currency")) { 169 timestamps = readTimestamps(line); 170 line = pr.readLine(); 171 continue; 172 } 173 String[] parts = line.split("\\t"); 174 CurrencyUnit currency = currenciesByName.get(parts[0]); 175 if (currency==null) { 176 LOGGER.finest("Uninterpretable data from IMF data feed: " + parts[0]); 177 line = pr.readLine(); 178 continue; 179 } 180 Double[] values = parseValues(f, parts); 181 for (int i = 0; i < values.length; i++) { 182 if (values[i]==null) { 183 continue; 184 } 185 LocalDate fromTS = timestamps != null ? timestamps.get(i) : null; 186 if (fromTS == null) { 187 continue; 188 } 189 RateType rateType = RateType.HISTORIC; 190 if (fromTS.equals(LocalDate.now())) { 191 rateType = RateType.DEFERRED; 192 } 193 if (currencyToSdr) { // Currency -> SDR 194 ExchangeRate rate = new ExchangeRateBuilder( 195 ConversionContextBuilder.create(CONTEXT, rateType).set(fromTS).build()) 196 .setBase(currency).setTerm(SDR).setFactor(new DefaultNumberValue(1d / values[i])).build(); 197 List<ExchangeRate> rates = newCurrencyToSdr.get(currency); 198 if(rates==null){ 199 rates = new ArrayList<>(5); 200 newCurrencyToSdr.put(currency,rates); 201 } 202 rates.add(rate); 203 } else { // SDR -> Currency 204 ExchangeRate rate = new ExchangeRateBuilder( 205 ConversionContextBuilder.create(CONTEXT, rateType).set(fromTS) 206 .set("LocalTime",fromTS.toString()).build()) 207 .setBase(SDR).setTerm(currency) 208 .setFactor(DefaultNumberValue.of(1d / values[i])).build(); 209 List<ExchangeRate> rates = newSdrToCurrency.get(currency); 210 if(rates==null){ 211 rates = new ArrayList<>(5); 212 newSdrToCurrency.put(currency,rates); 213 } 214 rates.add(rate); 215 } 216 } 217 line = pr.readLine(); 218 } 219 // Cast is save, since contained DefaultExchangeRate is Comparable! 220 for(List<ExchangeRate> list:newSdrToCurrency.values()){ 221 Collections.sort(List.class.cast(list)); 222 } 223 for(List<ExchangeRate> list:newCurrencyToSdr.values()){ 224 Collections.sort(List.class.cast(list)); 225 } 226 this.sdrToCurrency = newSdrToCurrency; 227 this.currencyToSdr = newCurrencyToSdr; 228 for(Map.Entry<CurrencyUnit, List<ExchangeRate>> entry: this.sdrToCurrency.entrySet()){ 229 LOGGER.finest("SDR -> " + entry.getKey().getCurrencyCode() + ": " + entry.getValue()); 230 } 231 for(Map.Entry<CurrencyUnit, List<ExchangeRate>> entry: this.currencyToSdr.entrySet()){ 232 LOGGER.finest(entry.getKey().getCurrencyCode() + " -> SDR: " + entry.getValue()); 233 } 234 } 235 236 private Double[] parseValues(NumberFormat f, String[] parts) throws ParseException { 237 Double[] result = new Double[parts.length - 1]; 238 for (int i = 1; i < parts.length; i++) { 239 if (parts[i].isEmpty()) { 240 continue; 241 } 242 result[i - 1] = f.parse(parts[i]).doubleValue(); 243 } 244 return result; 245 } 246 247 private List<LocalDate> readTimestamps(String line) throws ParseException { 248 // Currency May 01, 2013 April 30, 2013 April 29, 2013 April 26, 2013 249 // April 25, 2013 250 SimpleDateFormat sdf = new SimpleDateFormat("MMMM dd, yyyy", Locale.ENGLISH); 251 String[] parts = line.split("\\\t"); 252 List<LocalDate> dates = new ArrayList<>(parts.length); 253 for (int i = 1; i < parts.length; i++) { 254 Calendar date = GregorianCalendar.getInstance(); 255 date.setTime(sdf.parse(parts[i])); 256 dates.add(LocalDate.from(date)); 257 } 258 return dates; 259 } 260 261 @Override 262 public ExchangeRate getExchangeRate(ConversionQuery conversionQuery) { 263 if (!isAvailable(conversionQuery)) { 264 return null; 265 } 266 CurrencyUnit base = conversionQuery.getBaseCurrency(); 267 CurrencyUnit term = conversionQuery.getCurrency(); 268 Calendar timestamp = conversionQuery.get(Calendar.class); 269 if (timestamp == null) { 270 timestamp = conversionQuery.get(GregorianCalendar.class); 271 } 272 ExchangeRate rate1; 273 ExchangeRate rate2; 274 LocalDate localDate; 275 if (timestamp == null) { 276 localDate = LocalDate.yesterday(); 277 rate1 = lookupRate(currencyToSdr.get(base), localDate); 278 rate2 = lookupRate(sdrToCurrency.get(term), localDate); 279 if(rate1==null || rate2==null){ 280 localDate = LocalDate.beforeDays(2); 281 } 282 rate1 = lookupRate(currencyToSdr.get(base), localDate); 283 rate2 = lookupRate(sdrToCurrency.get(term), localDate); 284 if(rate1==null || rate2==null){ 285 localDate = LocalDate.beforeDays(3); 286 rate1 = lookupRate(currencyToSdr.get(base), localDate); 287 rate2 = lookupRate(sdrToCurrency.get(term), localDate); 288 } 289 } 290 else{ 291 localDate = LocalDate.from(timestamp); 292 rate1 = lookupRate(currencyToSdr.get(base), localDate); 293 rate2 = lookupRate(sdrToCurrency.get(term), localDate); 294 } 295 if(rate1==null || rate2==null){ 296 return null; 297 } 298 if (base.equals(SDR)) { 299 return rate2; 300 } else if (term.equals(SDR)) { 301 return rate1; 302 } 303 ExchangeRateBuilder builder = 304 new ExchangeRateBuilder(ConversionContext.of(CONTEXT.getProviderName(), RateType.HISTORIC)); 305 builder.setBase(base); 306 builder.setTerm(term); 307 builder.setFactor(multiply(rate1.getFactor(), rate2.getFactor())); 308 builder.setRateChain(rate1, rate2); 309 return builder.build(); 310 } 311 312 private ExchangeRate lookupRate(List<ExchangeRate> list, LocalDate localDate) { 313 if (list==null) { 314 return null; 315 } 316 for (ExchangeRate rate : list) { 317 if (localDate==null) { 318 localDate = LocalDate.now(); 319 } 320 if (rate!=null) { 321 return rate; 322 } 323 } 324 return null; 325 } 326 327}