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