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