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.InputStream; 019import java.math.BigDecimal; 020import java.math.MathContext; 021import java.net.MalformedURLException; 022import java.text.ParseException; 023import java.text.SimpleDateFormat; 024import java.util.*; 025import java.util.concurrent.ConcurrentHashMap; 026import java.util.logging.Level; 027 028import javax.money.CurrencyUnit; 029import javax.money.MonetaryCurrencies; 030import javax.money.convert.*; 031import javax.money.spi.Bootstrap; 032import javax.xml.parsers.SAXParser; 033import javax.xml.parsers.SAXParserFactory; 034 035import org.javamoney.moneta.ExchangeRateBuilder; 036import org.javamoney.moneta.spi.AbstractRateProvider; 037import org.javamoney.moneta.spi.DefaultNumberValue; 038import org.javamoney.moneta.spi.LoaderService; 039import org.javamoney.moneta.spi.LoaderService.LoaderListener; 040import org.xml.sax.Attributes; 041import org.xml.sax.SAXException; 042import org.xml.sax.helpers.DefaultHandler; 043 044/** 045 * This class implements an {@link javax.money.convert.ExchangeRateProvider} that loads data from 046 * the European Central Bank data feed (XML). It loads the current exchange 047 * rates, as well as historic rates for the past 90 days. The provider loads all data up to 1999 into its 048 * historic data cache. 049 * 050 * @author Anatole Tresch 051 * @author Werner Keil 052 */ 053public class ECBHistoricRateProvider extends AbstractRateProvider implements LoaderListener{ 054 055 /** 056 * The data id used for the LoaderService. 057 */ 058 private static final String DATA_ID = ECBHistoricRateProvider.class.getSimpleName(); 059 private static final String BASE_CURRENCY_CODE = "EUR"; 060 /** 061 * Base currency of the loaded rates is always EUR. 062 */ 063 public static final CurrencyUnit BASE_CURRENCY = MonetaryCurrencies.getCurrency(BASE_CURRENCY_CODE); 064 065 /** 066 * Historic exchange rates, rate timestamp as UTC long. 067 */ 068 private final Map<Long,Map<String,ExchangeRate>> historicRates = new ConcurrentHashMap<>(); 069 /** 070 * Parser factory. 071 */ 072 private SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); 073 /** 074 * The {@link ConversionContext} of this provider. 075 */ 076 private static final ProviderContext CONTEXT = 077 ProviderContextBuilder.of("ECB-HIST", RateType.HISTORIC, RateType.DEFERRED) 078 .set("providerDescription", "European Central Bank").set("days", 1500).build(); 079 080 /** 081 * Constructor, also loads initial data. 082 * 083 * @throws MalformedURLException 084 */ 085 public ECBHistoricRateProvider() throws MalformedURLException{ 086 super(CONTEXT); 087 saxParserFactory.setNamespaceAware(false); 088 saxParserFactory.setValidating(false); 089 LoaderService loader = Bootstrap.getService(LoaderService.class); 090 loader.addLoaderListener(this, DATA_ID); 091 loader.loadDataAsync(DATA_ID); 092 } 093 094 @Override 095 public void newDataLoaded(String data, InputStream is){ 096 final int oldSize = this.historicRates.size(); 097 try{ 098 SAXParser parser = saxParserFactory.newSAXParser(); 099 parser.parse(is, new RateReadingHandler()); 100 } 101 catch(Exception e){ 102 LOGGER.log(Level.FINEST, "Error during data load.", e); 103 } 104 int newSize = this.historicRates.size(); 105 LOGGER.info("Loaded " + DATA_ID + " exchange rates for days:" + (newSize - oldSize)); 106 } 107 108 /* 109 * (non-Javadoc) 110 * 111 * @see javax.money.convert.spi.ExchangeRateProviderSpi#getExchangeRateType 112 * () 113 */ 114 @Override 115 public ProviderContext getProviderContext(){ 116 return CONTEXT; 117 } 118 119 public ExchangeRate getExchangeRate(ConversionQuery query){ 120 if(Objects.isNull(query.getTimestampMillis())){ 121 return null; 122 } 123 ExchangeRateBuilder builder = new ExchangeRateBuilder( 124 ConversionContextBuilder.create(CONTEXT, RateType.HISTORIC) 125 .setTimestampMillis(query.getTimestampMillis()).build()); 126 builder.setBase(query.getBaseCurrency()); 127 builder.setTerm(query.getCurrency()); 128 ExchangeRate sourceRate; 129 ExchangeRate target; 130 if(historicRates.isEmpty()){ 131 return null; 132 } 133 final Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC")); 134 cal.setTimeInMillis(query.getTimestampMillis()); 135 cal.set(Calendar.HOUR, 0); 136 cal.set(Calendar.MINUTE, 0); 137 cal.set(Calendar.SECOND, 0); 138 cal.set(Calendar.MILLISECOND, 0); 139 Long targetTS = cal.getTimeInMillis(); 140 Map<String,ExchangeRate> targets = this.historicRates.get(targetTS); 141 if(Objects.isNull(targets)){ 142 return null; 143 } 144 sourceRate = targets.get(query.getBaseCurrency().getCurrencyCode()); 145 target = targets.get(query.getCurrency().getCurrencyCode()); 146 if(BASE_CURRENCY_CODE.equals(query.getBaseCurrency().getCurrencyCode()) && 147 BASE_CURRENCY_CODE.equals(query.getCurrency().getCurrencyCode())){ 148 builder.setFactor(DefaultNumberValue.ONE); 149 return builder.build(); 150 }else if(BASE_CURRENCY_CODE.equals(query.getCurrency().getCurrencyCode())){ 151 if(Objects.isNull(sourceRate)){ 152 return null; 153 } 154 return reverse(sourceRate); 155 }else if(BASE_CURRENCY_CODE.equals(query.getBaseCurrency().getCurrencyCode())){ 156 return target; 157 }else{ 158 // Get Conversion base as derived rate: base -> EUR -> term 159 ExchangeRate rate1 = getExchangeRate( 160 query.toBuilder().setTermCurrency(MonetaryCurrencies.getCurrency(BASE_CURRENCY_CODE)).build()); 161 ExchangeRate rate2 = getExchangeRate( 162 query.toBuilder().setBaseCurrency(MonetaryCurrencies.getCurrency(BASE_CURRENCY_CODE)) 163 .setTermCurrency(query.getCurrency()).build()); 164 if(Objects.nonNull(rate1) || Objects.nonNull(rate2)){ 165 builder.setFactor(multiply(rate1.getFactor(), rate2.getFactor())); 166 builder.setRateChain(rate1, rate2); 167 return builder.build(); 168 } 169 return null; 170 } 171 } 172 173 private static ExchangeRate reverse(ExchangeRate rate){ 174 if(Objects.isNull(rate)){ 175 throw new IllegalArgumentException("Rate null is not reversable."); 176 } 177 return new ExchangeRateBuilder(rate).setRate(rate).setBase(rate.getCurrency()).setTerm(rate.getBaseCurrency()) 178 .setFactor(divide(DefaultNumberValue.ONE, rate.getFactor(), MathContext.DECIMAL64)).build(); 179 } 180 181 /** 182 * SAX Event Handler that reads the quotes. 183 * <p> 184 * Format: <gesmes:Envelope 185 * xmlns:gesmes="http://www.gesmes.org/xml/2002-08-01" 186 * xmlns="http://www.ecb.int/vocabulary/2002-08-01/eurofxref"> 187 * <gesmes:subject>Reference rates</gesmes:subject> <gesmes:Sender> 188 * <gesmes:name>European Central Bank</gesmes:name> </gesmes:Sender> <Cube> 189 * <Cube time="2013-02-21">...</Cube> <Cube time="2013-02-20">...</Cube> 190 * <Cube time="2013-02-19"> <Cube currency="USD" rate="1.3349"/> <Cube 191 * currency="JPY" rate="124.81"/> <Cube currency="BGN" rate="1.9558"/> <Cube 192 * currency="CZK" rate="25.434"/> <Cube currency="DKK" rate="7.4599"/> <Cube 193 * currency="GBP" rate="0.8631"/> <Cube currency="HUF" rate="290.79"/> <Cube 194 * currency="LTL" rate="3.4528"/> ... 195 * 196 * @author Anatole Tresch 197 */ 198 private class RateReadingHandler extends DefaultHandler{ 199 200 /** 201 * Date parser. 202 */ 203 private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); 204 /** 205 * Current timestamp for the given section. 206 */ 207 private Long timestamp; 208 209 /** Flag, if current or historic data is loaded. */ 210 // private boolean loadCurrent; 211 212 /** 213 * Creates a new parser. 214 */ 215 public RateReadingHandler(){ 216 dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); 217 } 218 219 /* 220 * (non-Javadoc) 221 * 222 * @see 223 * org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, 224 * java.lang.String, java.lang.String, org.xml.sax.Attributes) 225 */ 226 @Override 227 public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException{ 228 try{ 229 if("Cube".equals(qName)){ 230 if(Objects.nonNull(attributes.getValue("time"))){ 231 Date date = dateFormat.parse(attributes.getValue("time")); 232 timestamp = date.getTime(); 233 }else if(Objects.nonNull(attributes.getValue("currency"))){ 234 // read data <Cube currency="USD" rate="1.3349"/> 235 CurrencyUnit tgtCurrency = MonetaryCurrencies.getCurrency(attributes.getValue("currency")); 236 addRate(tgtCurrency, timestamp, 237 BigDecimal.valueOf(Double.parseDouble(attributes.getValue("rate")))); 238 } 239 } 240 super.startElement(uri, localName, qName, attributes); 241 } 242 catch(ParseException e){ 243 throw new SAXException("Failed to read.", e); 244 } 245 } 246 247 } 248 249 /** 250 * Method to add a currency exchange rate. 251 * 252 * @param term the term (target) currency, mapped from EUR. 253 * @param timestamp The target day. 254 * @param rate The rate. 255 */ 256 void addRate(CurrencyUnit term, Long timestamp, Number rate){ 257 RateType rateType = RateType.HISTORIC; 258 ExchangeRateBuilder builder; 259 if(Objects.nonNull(timestamp)){ 260 if(timestamp > System.currentTimeMillis()){ 261 rateType = RateType.DEFERRED; 262 } 263 builder = new ExchangeRateBuilder( 264 ConversionContextBuilder.create(CONTEXT, rateType).setTimestampMillis(timestamp).build()); 265 }else{ 266 builder = new ExchangeRateBuilder(ConversionContextBuilder.create(CONTEXT, rateType).build()); 267 } 268 builder.setBase(BASE_CURRENCY); 269 builder.setTerm(term); 270 builder.setFactor(DefaultNumberValue.of(rate)); 271 ExchangeRate exchangeRate = builder.build(); 272 Map<String,ExchangeRate> rateMap = this.historicRates.get(timestamp); 273 if(Objects.isNull(rateMap)){ 274 synchronized(this.historicRates){ 275 rateMap = Optional.ofNullable(this.historicRates.get(timestamp)).orElse(new ConcurrentHashMap<>()); 276 this.historicRates.putIfAbsent(timestamp, rateMap); 277 278 } 279 } 280 rateMap.put(term.getCurrencyCode(), exchangeRate); 281 } 282}