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