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}