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