001/* 002 * Copyright (c) 2012, 2013, Credit Suisse (Anatole Tresch), Werner Keil. 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 * 016 * Contributors: Anatole Tresch - initial implementation. 017 */ 018package org.javamoney.moneta.convert.internal; 019 020import org.javamoney.moneta.spi.AbstractRateProvider; 021import org.javamoney.moneta.spi.DefaultNumberValue; 022import org.javamoney.moneta.spi.LoaderService; 023import org.javamoney.moneta.spi.LoaderService.LoaderListener; 024import org.xml.sax.Attributes; 025import org.xml.sax.SAXException; 026import org.xml.sax.helpers.DefaultHandler; 027 028import javax.money.CurrencyUnit; 029import javax.money.MonetaryCurrencies; 030import javax.money.convert.ConversionContext; 031import javax.money.convert.ExchangeRate; 032import javax.money.convert.ProviderContext; 033import javax.money.convert.RateType; 034import javax.money.spi.Bootstrap; 035import javax.xml.parsers.SAXParser; 036import javax.xml.parsers.SAXParserFactory; 037import java.io.InputStream; 038import java.math.BigDecimal; 039import java.math.MathContext; 040import java.net.MalformedURLException; 041import java.text.ParseException; 042import java.text.SimpleDateFormat; 043import java.util.*; 044import java.util.concurrent.ConcurrentHashMap; 045import java.util.logging.Level; 046 047/** 048 * This class implements an {@link javax.money.convert.ExchangeRateProvider} that loads data from 049 * the European Central Bank data feed (XML). It loads the current exchange 050 * rates, as well as historic rates for the past 90 days. The provider loads all data up to 1999 into its 051 * historic data cache. 052 * 053 * @author Anatole Tresch 054 * @author Werner Keil 055 */ 056public class ECBHistoricRateProvider extends AbstractRateProvider implements LoaderListener{ 057 058 /** 059 * The data id used for the LoaderService. 060 */ 061 private static final String DATA_ID = ECBHistoricRateProvider.class.getSimpleName(); 062 private static final String BASE_CURRENCY_CODE = "EUR"; 063 /** 064 * Base currency of the loaded rates is always EUR. 065 */ 066 public static final CurrencyUnit BASE_CURRENCY = MonetaryCurrencies.getCurrency(BASE_CURRENCY_CODE); 067 068 /** 069 * Historic exchange rates, rate timestamp as UTC long. 070 */ 071 private final Map<Long,Map<String,ExchangeRate>> historicRates = new ConcurrentHashMap<Long,Map<String,ExchangeRate>>(); 072 /** 073 * Parser factory. 074 */ 075 private SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); 076 /** 077 * The {@link ConversionContext} of this provider. 078 */ 079 private static final ProviderContext CONTEXT = 080 new ProviderContext.Builder("ECB-HIST").setRateTypes(RateType.HISTORIC, RateType.DEFERRED) 081 .set("European Central Bank", "providerDescription").set(1500, "days").create(); 082 083 /** 084 * Constructor, also loads initial data. 085 * 086 * @throws MalformedURLException 087 */ 088 public ECBHistoricRateProvider() throws MalformedURLException{ 089 super(CONTEXT); 090 saxParserFactory.setNamespaceAware(false); 091 saxParserFactory.setValidating(false); 092 LoaderService loader = Bootstrap.getService(LoaderService.class); 093 loader.addLoaderListener(this, DATA_ID); 094 loader.loadDataAsync(DATA_ID); 095 } 096 097 @Override 098 public void newDataLoaded(String data, InputStream is){ 099 final int oldSize = this.historicRates.size(); 100 try{ 101 SAXParser parser = saxParserFactory.newSAXParser(); 102 parser.parse(is, new RateReadingHandler()); 103 } 104 catch(Exception e){ 105 LOGGER.log(Level.FINEST, "Error during data load.", e); 106 } 107 int newSize = this.historicRates.size(); 108 LOGGER.info("Loaded " + DATA_ID + " exchange rates for days:" + (newSize - oldSize)); 109 } 110 111 /* 112 * (non-Javadoc) 113 * 114 * @see javax.money.convert.spi.ExchangeRateProviderSpi#getExchangeRateType 115 * () 116 */ 117 @Override 118 public ProviderContext getProviderContext(){ 119 return CONTEXT; 120 } 121 122 protected ExchangeRate getExchangeRateInternal(CurrencyUnit base, CurrencyUnit term, ConversionContext context){ 123 if(context.getTimestamp() == null){ 124 return null; 125 } 126 ExchangeRate.Builder builder = new ExchangeRate.Builder( 127 ConversionContext.of(CONTEXT.getProviderName(), RateType.HISTORIC, context.getTimestamp())); 128 builder.setBase(base); 129 builder.setTerm(term); 130 ExchangeRate sourceRate = null; 131 ExchangeRate target = null; 132 if(historicRates.isEmpty()){ 133 return null; 134 } 135 final Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC")); 136 cal.setTimeInMillis(context.getTimestamp()); 137 cal.set(Calendar.HOUR, 0); 138 cal.set(Calendar.MINUTE, 0); 139 cal.set(Calendar.SECOND, 0); 140 cal.set(Calendar.MILLISECOND, 0); 141 Long targetTS = Long.valueOf(cal.getTimeInMillis()); 142 Map<String,ExchangeRate> targets = this.historicRates.get(targetTS); 143 if(targets == null){ 144 return null; 145 } 146 sourceRate = targets.get(base.getCurrencyCode()); 147 target = targets.get(term.getCurrencyCode()); 148 if(BASE_CURRENCY_CODE.equals(base.getCurrencyCode()) && BASE_CURRENCY_CODE.equals(term.getCurrencyCode())){ 149 builder.setFactor(DefaultNumberValue.ONE); 150 return builder.create(); 151 }else if(BASE_CURRENCY_CODE.equals(term.getCurrencyCode())){ 152 if(sourceRate == null){ 153 return null; 154 } 155 return reverse(sourceRate); 156 }else if(BASE_CURRENCY_CODE.equals(base.getCurrencyCode())){ 157 return target; 158 }else{ 159 // Get Conversion base as derived rate: base -> EUR -> term 160 ExchangeRate rate1 = 161 getExchangeRateInternal(base, MonetaryCurrencies.getCurrency(BASE_CURRENCY_CODE), context); 162 ExchangeRate rate2 = 163 getExchangeRateInternal(MonetaryCurrencies.getCurrency(BASE_CURRENCY_CODE), term, context); 164 if(rate1 != null || rate2 != null){ 165 builder.setFactor(multiply(rate1.getFactor(), rate2.getFactor())); 166 builder.setRateChain(rate1, rate2); 167 return builder.create(); 168 } 169 return null; 170 } 171 } 172 173 private static ExchangeRate reverse(ExchangeRate rate){ 174 if(rate == null){ 175 throw new IllegalArgumentException("Rate null is not reversable."); 176 } 177 return rate.toBuilder().setRate(rate).setBase(rate.getTerm()).setTerm(rate.getBase()) 178 .setFactor(divide(DefaultNumberValue.ONE, rate.getFactor(), MathContext.DECIMAL64)).create(); 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(attributes.getValue("time") != null){ 231 Date date = dateFormat.parse(attributes.getValue("time")); 232 timestamp = Long.valueOf(date.getTime()); 233 }else if(attributes.getValue("currency") != null){ 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 ExchangeRate.Builder builder = null; 259 if(timestamp != null){ 260 if(timestamp > System.currentTimeMillis()){ 261 rateType = RateType.DEFERRED; 262 } 263 builder = new ExchangeRate.Builder(ConversionContext.of(CONTEXT.getProviderName(), rateType, timestamp)); 264 }else{ 265 builder = new ExchangeRate.Builder(ConversionContext.of(CONTEXT.getProviderName(), rateType)); 266 } 267 builder.setBase(BASE_CURRENCY); 268 builder.setTerm(term); 269 builder.setFactor(DefaultNumberValue.of(rate)); 270 ExchangeRate exchangeRate = builder.create(); 271 Map<String,ExchangeRate> rateMap = this.historicRates.get(timestamp); 272 if(rateMap == null){ 273 synchronized(this.historicRates){ 274 rateMap = this.historicRates.get(timestamp); 275 if(rateMap == null){ 276 rateMap = new ConcurrentHashMap<>(); 277 this.historicRates.put(timestamp, rateMap); 278 } 279 } 280 } 281 rateMap.put(term.getCurrencyCode(), exchangeRate); 282 } 283 284}