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