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