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}