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 } catch (Exception e) { 110 LOGGER.log(Level.SEVERE, "Error", e); 111 } 112 } 113 114 private void loadRatesTSV(InputStream inputStream) throws IOException, ParseException { 115 Map<CurrencyUnit, List<ExchangeRate>> newCurrencyToSdr = new HashMap<>(); 116 Map<CurrencyUnit, List<ExchangeRate>> newSdrToCurrency = new HashMap<>(); 117 NumberFormat f = new DecimalFormat("#0.0000000000"); 118 f.setGroupingUsed(false); 119 BufferedReader pr = new BufferedReader(new InputStreamReader(inputStream)); 120 String line = pr.readLine(); 121 // int lineType = 0; 122 boolean currencyToSdr = true; 123 // SDRs per Currency unit (2) 124 // 125 // Currency January 31, 2013 January 30, 2013 January 29, 2013 126 // January 28, 2013 January 25, 2013 127 // Euro 0.8791080000 0.8789170000 0.8742470000 0.8752180000 128 // 0.8768020000 129 130 // Currency units per SDR(3) 131 // 132 // Currency January 31, 2013 January 30, 2013 January 29, 2013 133 // January 28, 2013 January 25, 2013 134 // Euro 1.137520 1.137760 1.143840 1.142570 1.140510 135 List<Long> timestamps = null; 136 while (Objects.nonNull(line)) { 137 if (line.trim().isEmpty()) { 138 line = pr.readLine(); 139 continue; 140 } 141 if (line.startsWith("SDRs per Currency unit")) { 142 currencyToSdr = false; 143 line = pr.readLine(); 144 continue; 145 } else if (line.startsWith("Currency units per SDR")) { 146 currencyToSdr = true; 147 line = pr.readLine(); 148 continue; 149 } else if (line.startsWith("Currency")) { 150 timestamps = readTimestamps(line); 151 line = pr.readLine(); 152 continue; 153 } 154 String[] parts = line.split("\\t"); 155 CurrencyUnit currency = currenciesByName.get(parts[0]); 156 if (Objects.isNull(currency)) { 157 LOGGER.warning("Unknown currency from, IMF data feed: " + parts[0]); 158 line = pr.readLine(); 159 continue; 160 } 161 Double[] values = parseValues(f, parts); 162 for (int i = 0; i < values.length; i++) { 163 if (Objects.isNull(values[i])) { 164 continue; 165 } 166 Long fromTS = timestamps != null ? timestamps.get(i) : null; 167 if (fromTS == null) { 168 continue; 169 } 170 Long toTS = fromTS + 3600L * 1000L * 24L; // One day 171 RateType rateType = RateType.HISTORIC; 172 if (toTS > System.currentTimeMillis()) { 173 rateType = RateType.DEFERRED; 174 } 175 if (currencyToSdr) { // Currency -> SDR 176 List<ExchangeRate> rates = this.currencyToSdr.get(currency); 177 if (Objects.isNull(rates)) { 178 rates = new ArrayList<>(5); 179 newCurrencyToSdr.put(currency, rates); 180 } 181 ExchangeRate rate = new ExchangeRateBuilder( 182 ConversionContextBuilder.create(CONTEXT, rateType).setTimestampMillis(toTS).build()) 183 .setBase(currency).setTerm(SDR).setFactor(new DefaultNumberValue(values[i])).build(); 184 rates.add(rate); 185 } else { // SDR -> Currency 186 List<ExchangeRate> rates = this.sdrToCurrency.get(currency); 187 if (Objects.isNull(rates)) { 188 rates = new ArrayList<>(5); 189 newSdrToCurrency.put(currency, rates); 190 } 191 ExchangeRate rate = new ExchangeRateBuilder( 192 ConversionContextBuilder.create(CONTEXT, rateType).setTimestampMillis(fromTS).build()) 193 .setBase(SDR).setTerm(currency).setFactor(DefaultNumberValue.of(values[i])).build(); 194 rates.add(rate); 195 } 196 } 197 line = pr.readLine(); 198 } 199 // Cast is save, since contained DefaultExchangeRate is Comparable! 200 newSdrToCurrency.values().forEach((c) -> Collections.sort(List.class.cast(c))); 201 newCurrencyToSdr.values().forEach((c) -> Collections.sort(List.class.cast(c))); 202 this.sdrToCurrency = newSdrToCurrency; 203 this.currencyToSdr = newCurrencyToSdr; 204 } 205 206 private Double[] parseValues(NumberFormat f, String[] parts) throws ParseException { 207 Double[] result = new Double[parts.length - 1]; 208 for (int i = 1; i < parts.length; i++) { 209 if (parts[i].isEmpty()) { 210 continue; 211 } 212 result[i - 1] = f.parse(parts[i]).doubleValue(); 213 } 214 return result; 215 } 216 217 private List<Long> readTimestamps(String line) throws ParseException { 218 // Currency May 01, 2013 April 30, 2013 April 29, 2013 April 26, 2013 219 // April 25, 2013 220 SimpleDateFormat sdf = new SimpleDateFormat("MMM DD, yyyy", Locale.ENGLISH); 221 String[] parts = line.split("\\\t"); 222 List<Long> dates = new ArrayList<>(parts.length); 223 for (int i = 1; i < parts.length; i++) { 224 dates.add(sdf.parse(parts[i]).getTime()); 225 } 226 return dates; 227 } 228 229 public ExchangeRate getExchangeRate(ConversionQuery conversionQuery) { 230 if (!isAvailable(conversionQuery)) { 231 return null; 232 } 233 CurrencyUnit base = conversionQuery.getBaseCurrency(); 234 CurrencyUnit term = conversionQuery.getCurrency(); 235 Long timestamp = conversionQuery.getTimestampMillis(); 236 ExchangeRate rate1 = lookupRate(currencyToSdr.get(base), timestamp); 237 ExchangeRate rate2 = lookupRate(sdrToCurrency.get(term), timestamp); 238 if (base.equals(SDR)) { 239 return rate2; 240 } else if (term.equals(SDR)) { 241 return rate1; 242 } 243 if (Objects.isNull(rate1) || Objects.isNull(rate2)) { 244 return null; 245 } 246 ExchangeRateBuilder builder = 247 new ExchangeRateBuilder(ConversionContext.of(CONTEXT.getProvider(), RateType.HISTORIC)); 248 builder.setBase(base); 249 builder.setTerm(term); 250 builder.setFactor(multiply(rate1.getFactor(), rate2.getFactor())); 251 builder.setRateChain(rate1, rate2); 252 return builder.build(); 253 } 254 255 private ExchangeRate lookupRate(List<ExchangeRate> list, Long timestamp) { 256 if (Objects.isNull(list)) { 257 return null; 258 } 259 ExchangeRate found = null; 260 for (ExchangeRate rate : list) { 261 if (Objects.isNull(timestamp)) { 262 timestamp = System.currentTimeMillis(); 263 } 264 if (isValid(rate.getConversionContext(), timestamp)) { 265 return rate; 266 } 267 if (Objects.isNull(found)) { 268 found = rate; 269 } 270 } 271 return found; 272 } 273 274 private boolean isValid(ConversionContext conversionContext, Long timestamp) { 275 Long validFrom = conversionContext.getLong("validFrom"); 276 Long validTo = conversionContext.getLong("validTo"); 277 return !(Objects.nonNull(validFrom) && validFrom > timestamp) && 278 !(Objects.nonNull(validTo) && validTo < timestamp); 279 } 280 281}