001/* 002 * Copyright (c) 2012, 2014, Credit Suisse (Anatole Tresch), Werner Keil. Licensed under the Apache 003 * License, Version 2.0 (the "License"); you may not use this file except in compliance with the 004 * License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 005 * Unless required by applicable law or agreed to in writing, software distributed under the License 006 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 007 * or implied. See the License for the specific language governing permissions and limitations under 008 * the License. Contributors: Anatole Tresch - initial implementation. 009 */ 010package org.javamoney.moneta.convert.internal; 011 012import org.javamoney.moneta.spi.AbstractRateProvider; 013import org.javamoney.moneta.spi.DefaultNumberValue; 014import org.javamoney.moneta.spi.LoaderService; 015import org.javamoney.moneta.spi.LoaderService.LoaderListener; 016import org.xml.sax.Attributes; 017import org.xml.sax.SAXException; 018import org.xml.sax.helpers.DefaultHandler; 019 020import javax.money.CurrencyUnit; 021import javax.money.MonetaryCurrencies; 022import javax.money.convert.ConversionContext; 023import javax.money.convert.ExchangeRate; 024import javax.money.convert.ProviderContext; 025import javax.money.convert.RateType; 026import javax.money.spi.Bootstrap; 027import javax.xml.parsers.ParserConfigurationException; 028import javax.xml.parsers.SAXParser; 029import javax.xml.parsers.SAXParserFactory; 030import java.io.IOException; 031import java.io.InputStream; 032import java.math.BigDecimal; 033import java.net.MalformedURLException; 034import java.text.ParseException; 035import java.text.SimpleDateFormat; 036import java.util.Date; 037import java.util.Map; 038import java.util.TimeZone; 039import java.util.concurrent.ConcurrentHashMap; 040import java.util.logging.Level; 041 042/** 043 * This class implements an {@link javax.money.convert.ExchangeRateProvider} that loads data from 044 * the European Central Bank data feed (XML). It loads the current exchange 045 * rates, as well as historic rates for the past 90 days. The provider loads all data up to 1999 into its 046 * historic data cache. 047 * 048 * @author Anatole Tresch 049 * @author Werner Keil 050 */ 051public class ECBCurrentRateProvider extends AbstractRateProvider implements LoaderListener{ 052 053 private static final String BASE_CURRENCY_CODE = "EUR"; 054 /** 055 * Base currency of the loaded rates is always EUR. 056 */ 057 public static final CurrencyUnit BASE_CURRENCY = MonetaryCurrencies.getCurrency(BASE_CURRENCY_CODE); 058 /** 059 * The data id used for the LoaderService. 060 */ 061 private static final String DATA_ID = ECBCurrentRateProvider.class.getSimpleName(); 062 063 /** 064 * Current exchange rates. 065 */ 066 private Map<String,ExchangeRate> currentRates = new ConcurrentHashMap<String,ExchangeRate>(); 067 /** 068 * Parser factory. 069 */ 070 private SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); 071 /** 072 * The {@link ConversionContext} of this provider. 073 */ 074 private static final ProviderContext CONTEXT = new ProviderContext.Builder("ECB").setRateTypes(RateType.DEFERRED) 075 .set("European Central Bank", "providerDescription").set(1, "days").create(); 076 077 /** 078 * Constructor, also loads initial data. 079 * 080 * @throws MalformedURLException 081 */ 082 public ECBCurrentRateProvider() throws MalformedURLException{ 083 super(CONTEXT); 084 saxParserFactory.setNamespaceAware(false); 085 saxParserFactory.setValidating(false); 086 LoaderService loader = Bootstrap.getService(LoaderService.class); 087 loader.addLoaderListener(this, DATA_ID); 088 try{ 089 loader.loadData(DATA_ID); 090 } 091 catch(IOException e){ 092 LOGGER.log(Level.SEVERE, "Error loading ECB data.", e); 093 } 094 } 095 096 /** 097 * (Re)load the given data feed. 098 * 099 * @throws IOException 100 * @throws SAXException 101 * @throws ParserConfigurationException 102 */ 103 @Override 104 public void newDataLoaded(String data, InputStream is){ 105 try{ 106 SAXParser parser = saxParserFactory.newSAXParser(); 107 parser.parse(is, new RateReadingHandler()); 108 LOGGER.info("Loaded current " + DATA_ID + " exchange rates."); 109 } 110 catch(Exception e){ 111 LOGGER.log(Level.SEVERE, "Error reading resource for ECB currencies: ", e); 112 } 113 } 114 115 protected ExchangeRate getExchangeRateInternal(CurrencyUnit base, CurrencyUnit term, ConversionContext context){ 116 if(context.getTimestamp() != null){ 117 return null; 118 } 119 ExchangeRate.Builder builder = new ExchangeRate.Builder( 120 ConversionContext.of(CONTEXT.getProviderName(), RateType.DEFERRED, context.getTimestamp())); 121 builder.setBase(base); 122 builder.setTerm(term); 123 ExchangeRate sourceRate = null; 124 ExchangeRate target = null; 125 if(currentRates.isEmpty()){ 126 return null; 127 } 128 sourceRate = currentRates.get(base.getCurrencyCode()); 129 target = currentRates.get(term.getCurrencyCode()); 130 if(BASE_CURRENCY_CODE.equals(base.getCurrencyCode()) && BASE_CURRENCY_CODE.equals(term.getCurrencyCode())){ 131 builder.setFactor(DefaultNumberValue.ONE); 132 return builder.create(); 133 }else if(BASE_CURRENCY_CODE.equals(term.getCurrencyCode())){ 134 if(sourceRate == null){ 135 return null; 136 } 137 return getReversed(sourceRate); 138 }else if(BASE_CURRENCY_CODE.equals(base.getCurrencyCode())){ 139 return target; 140 }else{ 141 // Get Conversion base as derived rate: base -> EUR -> term 142 ExchangeRate rate1 = 143 getExchangeRateInternal(base, MonetaryCurrencies.getCurrency(BASE_CURRENCY_CODE), context); 144 ExchangeRate rate2 = 145 getExchangeRateInternal(MonetaryCurrencies.getCurrency(BASE_CURRENCY_CODE), term, context); 146 if(rate1 != null && rate2 != null){ 147 builder.setFactor(multiply(rate1.getFactor(), rate2.getFactor())); 148 builder.setRateChain(rate1, rate2); 149 return builder.create(); 150 } 151 } 152 return null; 153 } 154 155 /** 156 * SAX Event Handler that reads the quotes. 157 * <p/> 158 * Format: <gesmes:Envelope 159 * xmlns:gesmes="http://www.gesmes.org/xml/2002-08-01" 160 * xmlns="http://www.ecb.int/vocabulary/2002-08-01/eurofxref"> 161 * <gesmes:subject>Reference rates</gesmes:subject> <gesmes:Sender> 162 * <gesmes:name>European Central Bank</gesmes:name> </gesmes:Sender> <Cube> 163 * <Cube time="2013-02-21">...</Cube> <Cube time="2013-02-20">...</Cube> 164 * <Cube time="2013-02-19"> <Cube currency="USD" rate="1.3349"/> <Cube 165 * currency="JPY" rate="124.81"/> <Cube currency="BGN" rate="1.9558"/> <Cube 166 * currency="CZK" rate="25.434"/> <Cube currency="DKK" rate="7.4599"/> <Cube 167 * currency="GBP" rate="0.8631"/> <Cube currency="HUF" rate="290.79"/> <Cube 168 * currency="LTL" rate="3.4528"/> ... 169 * 170 * @author Anatole Tresch 171 */ 172 private class RateReadingHandler extends DefaultHandler{ 173 174 /** 175 * Date parser. 176 */ 177 private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); 178 /** 179 * Current timestamp for the given section. 180 */ 181 private Long timestamp; 182 183 /** 184 * Creates a new parser. 185 */ 186 public RateReadingHandler(){ 187 dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); 188 } 189 190 /* 191 * (non-Javadoc) 192 * 193 * @see 194 * org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, 195 * java.lang.String, java.lang.String, org.xml.sax.Attributes) 196 */ 197 @Override 198 public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException{ 199 try{ 200 if("Cube".equals(qName)){ 201 if(attributes.getValue("time") != null){ 202 Date date = dateFormat.parse(attributes.getValue("time")); 203 timestamp = Long.valueOf(date.getTime()); 204 }else if(attributes.getValue("currency") != null){ 205 // read data <Cube currency="USD" rate="1.3349"/> 206 CurrencyUnit tgtCurrency = MonetaryCurrencies.getCurrency(attributes.getValue("currency")); 207 addRate(tgtCurrency, timestamp, 208 BigDecimal.valueOf(Double.parseDouble(attributes.getValue("rate")))); 209 } 210 } 211 super.startElement(uri, localName, qName, attributes); 212 } 213 catch(ParseException e){ 214 throw new SAXException("Failed to read.", e); 215 } 216 } 217 218 } 219 220 /** 221 * Method to add a currency exchange rate. 222 * 223 * @param term the term (target) currency, mapped from EUR. 224 * @param timestamp The target day. 225 * @param factor The conversion factor. 226 * @param loadCurrent Flag, if current or historic data is loaded. 227 */ 228 void addRate(CurrencyUnit term, Long timestamp, Number factor){ 229 ExchangeRate.Builder builder = 230 new ExchangeRate.Builder(ConversionContext.of(CONTEXT.getProviderName(), RateType.DEFERRED, timestamp)); 231 builder.setBase(BASE_CURRENCY); 232 builder.setTerm(term); 233 builder.setFactor(new DefaultNumberValue(factor)); 234 this.currentRates.put(term.getCurrencyCode(), builder.create()); 235 } 236 237}