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