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