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.time.LocalDate; 027import java.time.LocalDateTime; 028import java.time.format.DateTimeFormatter; 029import java.util.ArrayList; 030import java.util.Collections; 031import java.util.Currency; 032import java.util.HashMap; 033import java.util.List; 034import java.util.Locale; 035import java.util.Map; 036import java.util.Objects; 037import java.util.logging.Level; 038 039import javax.money.CurrencyContextBuilder; 040import javax.money.CurrencyUnit; 041import javax.money.MonetaryCurrencies; 042import javax.money.convert.ConversionContext; 043import javax.money.convert.ConversionContextBuilder; 044import javax.money.convert.ConversionQuery; 045import javax.money.convert.ExchangeRate; 046import javax.money.convert.ExchangeRateProvider; 047import javax.money.convert.ProviderContext; 048import javax.money.convert.ProviderContextBuilder; 049import javax.money.convert.RateType; 050import javax.money.spi.Bootstrap; 051 052import org.javamoney.moneta.CurrencyUnitBuilder; 053import org.javamoney.moneta.ExchangeRateBuilder; 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 Fond").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 private static Map<String, CurrencyUnit> currenciesByName = new HashMap<>(); 088 089 static { 090 for (Currency currency : Currency.getAvailableCurrencies()) { 091 currenciesByName.put(currency.getDisplayName(Locale.ENGLISH), 092 MonetaryCurrencies.getCurrency(currency.getCurrencyCode())); 093 } 094 // Additional IMF differing codes: 095 // This mapping is required to fix data issues in the input stream, it has nothing to do with i18n 096 currenciesByName.put("U.K. Pound Sterling", MonetaryCurrencies.getCurrency("GBP")); 097 currenciesByName.put("U.S. Dollar", MonetaryCurrencies.getCurrency("USD")); 098 currenciesByName.put("Bahrain Dinar", MonetaryCurrencies.getCurrency("BHD")); 099 currenciesByName.put("Botswana Pula", MonetaryCurrencies.getCurrency("BWP")); 100 currenciesByName.put("Czech Koruna", MonetaryCurrencies.getCurrency("CZK")); 101 currenciesByName.put("Icelandic Krona", MonetaryCurrencies.getCurrency("ISK")); 102 currenciesByName.put("Korean Won", MonetaryCurrencies.getCurrency("KRW")); 103 currenciesByName.put("Rial Omani", MonetaryCurrencies.getCurrency("OMR")); 104 currenciesByName.put("Nuevo Sol", MonetaryCurrencies.getCurrency("PEN")); 105 currenciesByName.put("Qatar Riyal", MonetaryCurrencies.getCurrency("QAR")); 106 currenciesByName.put("Saudi Arabian Riyal", MonetaryCurrencies.getCurrency("SAR")); 107 currenciesByName.put("Sri Lanka Rupee", MonetaryCurrencies.getCurrency("LKR")); 108 currenciesByName.put("Trinidad And Tobago Dollar", MonetaryCurrencies.getCurrency("TTD")); 109 currenciesByName.put("U.A.E. Dirham", MonetaryCurrencies.getCurrency("AED")); 110 currenciesByName.put("Peso Uruguayo", MonetaryCurrencies.getCurrency("UYU")); 111 currenciesByName.put("Bolivar Fuerte", MonetaryCurrencies.getCurrency("VEF")); 112 } 113 114 public IMFRateProvider() throws MalformedURLException { 115 super(CONTEXT); 116 LoaderService loader = Bootstrap.getService(LoaderService.class); 117 loader.addLoaderListener(this, DATA_ID); 118 try { 119 loader.loadData(DATA_ID); 120 } catch (IOException e) { 121 LOGGER.log(Level.WARNING, "Error loading initial data from IMF provider...", e); 122 } 123 } 124 125 @Override 126 public void newDataLoaded(String data, InputStream is) { 127 try { 128 loadRatesTSV(is); 129 } catch (Exception e) { 130 LOGGER.log(Level.SEVERE, "Error", e); 131 } 132 } 133 134 @SuppressWarnings("unchecked") 135 private void loadRatesTSV(InputStream inputStream) throws IOException, ParseException { 136 Map<CurrencyUnit, List<ExchangeRate>> newCurrencyToSdr = new HashMap<>(); 137 Map<CurrencyUnit, List<ExchangeRate>> newSdrToCurrency = new HashMap<>(); 138 NumberFormat f = new DecimalFormat("#0.0000000000"); 139 f.setGroupingUsed(false); 140 BufferedReader pr = new BufferedReader(new InputStreamReader(inputStream)); 141 String line = pr.readLine(); 142 // int lineType = 0; 143 boolean currencyToSdr = true; 144 // SDRs per Currency unit (2) 145 // 146 // Currency January 31, 2013 January 30, 2013 January 29, 2013 147 // January 28, 2013 January 25, 2013 148 // Euro 0.8791080000 0.8789170000 0.8742470000 0.8752180000 149 // 0.8768020000 150 151 // Currency units per SDR(3) 152 // 153 // Currency January 31, 2013 January 30, 2013 January 29, 2013 154 // January 28, 2013 January 25, 2013 155 // Euro 1.137520 1.137760 1.143840 1.142570 1.140510 156 List<LocalDate> timestamps = null; 157 while (Objects.nonNull(line)) { 158 if (line.trim().isEmpty()) { 159 line = pr.readLine(); 160 continue; 161 } 162 if (line.startsWith("SDRs per Currency unit")) { 163 currencyToSdr = false; 164 line = pr.readLine(); 165 continue; 166 } else if (line.startsWith("Currency units per SDR")) { 167 currencyToSdr = true; 168 line = pr.readLine(); 169 continue; 170 } else if (line.startsWith("Currency")) { 171 timestamps = readTimestamps(line); 172 line = pr.readLine(); 173 continue; 174 } 175 String[] parts = line.split("\\t"); 176 CurrencyUnit currency = currenciesByName.get(parts[0]); 177 if (Objects.isNull(currency)) { 178 LOGGER.finest(() -> "Uninterpretable data from IMF data feed: " + parts[0]); 179 line = pr.readLine(); 180 continue; 181 } 182 Double[] values = parseValues(f, parts); 183 for (int i = 0; i < values.length; i++) { 184 if (Objects.isNull(values[i])) { 185 continue; 186 } 187 LocalDate fromTS = timestamps != null ? timestamps.get(i) : null; 188 if (fromTS == null) { 189 continue; 190 } 191 RateType rateType = RateType.HISTORIC; 192 if (fromTS.equals(LocalDate.now())) { 193 rateType = RateType.DEFERRED; 194 } 195 if (currencyToSdr) { // Currency -> SDR 196 ExchangeRate rate = new ExchangeRateBuilder( 197 ConversionContextBuilder.create(CONTEXT, rateType).set(fromTS).build()) 198 .setBase(currency).setTerm(SDR).setFactor(new DefaultNumberValue(1d / values[i])).build(); 199 List<ExchangeRate> rates = newCurrencyToSdr.computeIfAbsent(currency, c -> new ArrayList<>(5)); 200 rates.add(rate); 201 } else { // SDR -> Currency 202 ExchangeRate rate = new ExchangeRateBuilder( 203 ConversionContextBuilder.create(CONTEXT, rateType).set(fromTS).build()) 204 .setBase(SDR).setTerm(currency).setFactor(DefaultNumberValue.of(1d / values[i])).build(); 205 List<ExchangeRate> rates = newSdrToCurrency.computeIfAbsent(currency, (c) -> new ArrayList<>(5)); 206 rates.add(rate); 207 } 208 } 209 line = pr.readLine(); 210 } 211 // Cast is save, since contained DefaultExchangeRate is Comparable! 212 newSdrToCurrency.values().forEach((c) -> Collections.sort(List.class.cast(c))); 213 newCurrencyToSdr.values().forEach((c) -> Collections.sort(List.class.cast(c))); 214 this.sdrToCurrency = newSdrToCurrency; 215 this.currencyToSdr = newCurrencyToSdr; 216 this.sdrToCurrency.forEach((c, l) -> LOGGER.finest(() -> "SDR -> " + c.getCurrencyCode() + ": " + l)); 217 this.currencyToSdr.forEach((c, l) -> LOGGER.finest(() -> c.getCurrencyCode() + " -> SDR: " + l)); 218 } 219 220 private Double[] parseValues(NumberFormat f, String[] parts) throws ParseException { 221 Double[] result = new Double[parts.length - 1]; 222 for (int i = 1; i < parts.length; i++) { 223 if (parts[i].isEmpty()) { 224 continue; 225 } 226 result[i - 1] = f.parse(parts[i]).doubleValue(); 227 } 228 return result; 229 } 230 231 private List<LocalDate> readTimestamps(String line) throws ParseException { 232 // Currency May 01, 2013 April 30, 2013 April 29, 2013 April 26, 2013 233 // April 25, 2013 234 DateTimeFormatter sdf = DateTimeFormatter.ofPattern("MMMM dd, uuuu").withLocale(Locale.ENGLISH); 235 String[] parts = line.split("\\\t"); 236 List<LocalDate> dates = new ArrayList<>(parts.length); 237 for (int i = 1; i < parts.length; i++) { 238 dates.add(LocalDate.parse(parts[i], sdf)); 239 } 240 return dates; 241 } 242 243 @Override 244 public ExchangeRate getExchangeRate(ConversionQuery conversionQuery) { 245 if (!isAvailable(conversionQuery)) { 246 return null; 247 } 248 CurrencyUnit base = conversionQuery.getBaseCurrency(); 249 CurrencyUnit term = conversionQuery.getCurrency(); 250 LocalDate timestamp = conversionQuery.get(LocalDate.class); 251 if (timestamp == null) { 252 LocalDateTime dateTime = conversionQuery.get(LocalDateTime.class); 253 if (dateTime != null) { 254 timestamp = dateTime.toLocalDate(); 255 } 256 } 257 ExchangeRate rate1 = lookupRate(currencyToSdr.get(base), timestamp); 258 ExchangeRate rate2 = lookupRate(sdrToCurrency.get(term), timestamp); 259 if (base.equals(SDR)) { 260 return rate2; 261 } else if (term.equals(SDR)) { 262 return rate1; 263 } 264 if (Objects.isNull(rate1) || Objects.isNull(rate2)) { 265 return null; 266 } 267 ExchangeRateBuilder builder = 268 new ExchangeRateBuilder(ConversionContext.of(CONTEXT.getProviderName(), RateType.HISTORIC)); 269 builder.setBase(base); 270 builder.setTerm(term); 271 builder.setFactor(multiply(rate1.getFactor(), rate2.getFactor())); 272 builder.setRateChain(rate1, rate2); 273 return builder.build(); 274 } 275 276 private ExchangeRate lookupRate(List<ExchangeRate> list, LocalDate localDate) { 277 if (Objects.isNull(list)) { 278 return null; 279 } 280 ExchangeRate found = null; 281 for (ExchangeRate rate : list) { 282 if (Objects.isNull(localDate)) { 283 localDate = LocalDate.now(); 284 } 285 if (isValid(rate.getContext(), localDate)) { 286 return rate; 287 } 288 if (Objects.isNull(found)) { 289 found = rate; 290 } 291 } 292 return found; 293 } 294 295 private boolean isValid(ConversionContext conversionContext, LocalDate timestamp) { 296 LocalDate validAt = conversionContext.get(LocalDate.class); 297 return !(Objects.nonNull(validAt)) && validAt.equals(timestamp); 298 } 299 300}