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