All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.javamoney.moneta.internal.convert.IMFRateProvider Maven / Gradle / Ivy

Go to download

JSR 354 provides an API for representing, transporting, and performing comprehensive calculations with Money and Currency. This module implements JSR 354.

There is a newer version: 1.4.1
Show newest version
/*
 * Copyright (c) 2012, 2014, Credit Suisse (Anatole Tresch), Werner Keil and others by the @author tag.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package org.javamoney.moneta.internal.convert;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Currency;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;

import javax.money.CurrencyContextBuilder;
import javax.money.CurrencyUnit;
import javax.money.Monetary;
import javax.money.MonetaryException;
import javax.money.convert.ConversionContext;
import javax.money.convert.ConversionContextBuilder;
import javax.money.convert.ConversionQuery;
import javax.money.convert.ExchangeRate;
import javax.money.convert.ExchangeRateProvider;
import javax.money.convert.ProviderContext;
import javax.money.convert.ProviderContextBuilder;
import javax.money.convert.RateType;
import javax.money.spi.Bootstrap;

import org.javamoney.moneta.CurrencyUnitBuilder;
import org.javamoney.moneta.convert.ExchangeRateBuilder;
import org.javamoney.moneta.spi.AbstractRateProvider;
import org.javamoney.moneta.spi.DefaultNumberValue;
import org.javamoney.moneta.spi.LoaderService;
import org.javamoney.moneta.spi.LoaderService.LoaderListener;

/**
 * Implements a {@link ExchangeRateProvider} that loads the IMF conversion data.
 * In most cases this provider will provide chained rates, since IMF always is
 * converting from/to the IMF SDR currency unit.
 *
 * @author Anatole Tresch
 * @author Werner Keil
 */
public class IMFRateProvider extends AbstractRateProvider implements LoaderListener {

    /**
     * The data id used for the LoaderService.
     */
    private static final String DATA_ID = IMFRateProvider.class.getSimpleName();
    /**
     * The {@link ConversionContext} of this provider.
     */
    private static final ProviderContext CONTEXT = ProviderContextBuilder.of("IMF", RateType.DEFERRED)
            .set("providerDescription", "International Monetary Fond").set("days", 1).build();

    private static final CurrencyUnit SDR =
            CurrencyUnitBuilder.of("SDR", CurrencyContextBuilder.of(IMFRateProvider.class.getSimpleName()).build())
                    .setDefaultFractionDigits(3).build(true);

    private Map> currencyToSdr = new HashMap<>();

    private Map> sdrToCurrency = new HashMap<>();

    protected volatile String loadState;

    protected volatile CountDownLatch loadLock = new CountDownLatch(1);

    private static final Map currenciesByName = new HashMap<>();

    static {
        for (Currency currency : Currency.getAvailableCurrencies()) {
            currenciesByName.put(currency.getDisplayName(Locale.ENGLISH).toLowerCase(Locale.ENGLISH),
                    Monetary.getCurrency(currency.getCurrencyCode()));
        }
        // Additional IMF differing codes:
        // This mapping is required to fix data issues in the input stream, it has nothing to do with i18n
        currenciesByName.put("U.K. pound".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("GBP"));
        currenciesByName.put("U.S. dollar".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("USD"));
        currenciesByName.put("Bahrain dinar".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("BHD"));
        currenciesByName.put("Botswana pula".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("BWP"));
        currenciesByName.put("Czech koruna".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("CZK"));
        currenciesByName.put("Icelandic krona".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("ISK"));
        currenciesByName.put("Korean won".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("KRW"));
        currenciesByName.put("Omani rial".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("OMR"));
        currenciesByName.put("Peruvian sol".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("PEN"));
        currenciesByName.put("Qatari riyal".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("QAR"));
        currenciesByName.put("Saudi Arabian riyal".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("SAR"));
        currenciesByName.put("Sri Lankan rupee".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("LKR"));
        currenciesByName.put("Trinidadian dollar".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("TTD"));
        currenciesByName.put("U.A.E. dirham".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("AED"));
        currenciesByName.put("Uruguayan peso".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("UYU"));
        currenciesByName.put("Bolivar Fuerte".toLowerCase(Locale.ENGLISH), Monetary.getCurrency("VEF"));
    }

    public IMFRateProvider() {
        super(CONTEXT);
        LoaderService loader = Bootstrap.getService(LoaderService.class);
        loader.addLoaderListener(this, DATA_ID);
        try {
            loader.loadData(DATA_ID);
        } catch (IOException e) {
            LOGGER.log(Level.WARNING, "Error loading initial data from IMF provider...", e);
        }
    }

    @Override
    public void newDataLoaded(String data, InputStream is) {
        try {
            int oldSize = this.sdrToCurrency.size();
            loadRatesTSV(is);
            int newSize = this.sdrToCurrency.size();
            loadState = "Loaded " + DATA_ID + " exchange rates for days:" + (newSize - oldSize);
            LOGGER.info(loadState);
            loadLock.countDown();
        } catch (Exception e) {
            loadState = "Last Error during data load: " + e.getMessage();
            throw new IllegalArgumentException("Failed to load IMF data provided.", e);
        }
    }

    @SuppressWarnings("unchecked")
    private void loadRatesTSV(InputStream inputStream) throws IOException, ParseException {
        Map> newCurrencyToSdr = new HashMap<>();
        Map> newSdrToCurrency = new HashMap<>();
        NumberFormat f = new DecimalFormat("#0.0000000000");
        f.setGroupingUsed(false);
        BufferedReader pr = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
        String line = pr.readLine();
        if(line.contains("Request Rejected")){
            throw new IOException("Request has been rejected by IMF server.");
        }
        // int lineType = 0;
        boolean currencyToSdr = true;
        // SDRs per Currency unit (2)
        //
        // Currency January 31, 2013 January 30, 2013 January 29, 2013
        // January 28, 2013 January 25, 2013
        // Euro 0.8791080000 0.8789170000 0.8742470000 0.8752180000
        // 0.8768020000

        // Currency units per SDR(3)
        //
        // Currency January 31, 2013 January 30, 2013 January 29, 2013
        // January 28, 2013 January 25, 2013
        // Euro 1.137520 1.137760 1.143840 1.142570 1.140510
        List timestamps = null;
        while (line!=null) {
            if (line.trim().isEmpty()) {
                line = pr.readLine();
                continue;
            }
            if (line.startsWith("SDRs per Currency unit")) {
                currencyToSdr = false;
                line = pr.readLine();
                continue;
            } else if (line.startsWith("Currency units per SDR")) {
                currencyToSdr = true;
                line = pr.readLine();
                continue;
            } else if (line.startsWith("Currency")) {
                timestamps = readTimestamps(line);
                line = pr.readLine();
                continue;
            }
            String[] parts = line.split("\\t");
            CurrencyUnit currency = currenciesByName.get(parts[0].toLowerCase(Locale.ENGLISH));
            if (currency==null) {
                LOGGER.finest("Uninterpretable data from IMF data feed: " + parts[0]);
                line = pr.readLine();
                continue;
            }
            Double[] values = parseValues(parts);
            for (int i = 0; i < values.length; i++) {
                if (values[i]==null) {
                    continue;
                }
                LocalDate fromTS = timestamps != null ? timestamps.get(i) : null;
                if (fromTS == null) {
                    continue;
                }
                RateType rateType = RateType.HISTORIC;
                if (fromTS.equals(LocalDate.now())) {
                    rateType = RateType.DEFERRED;
                }
                if (currencyToSdr) { // Currency -> SDR
                    ExchangeRate rate = new ExchangeRateBuilder(
                            ConversionContextBuilder.create(CONTEXT, rateType).set(fromTS).build())
                            .setBase(currency).setTerm(SDR).setFactor(new DefaultNumberValue(1d / values[i])).build();
                    List rates = newCurrencyToSdr.get(currency);
                    if(rates==null){
                        rates = new ArrayList<>(5);
                        newCurrencyToSdr.put(currency,rates);
                    }
                    rates.add(rate);
                } else { // SDR -> Currency
                    ExchangeRate rate = new ExchangeRateBuilder(
                            ConversionContextBuilder.create(CONTEXT, rateType).set(fromTS)
                                    .set("LocalTime",fromTS.toString()).build())
                                    .setBase(SDR).setTerm(currency)
                                    .setFactor(DefaultNumberValue.of(1d / values[i])).build();
                    List rates = newSdrToCurrency.get(currency);
                    if(rates==null){
                        rates = new ArrayList<>(5);
                        newSdrToCurrency.put(currency,rates);
                    }
                    rates.add(rate);
                }
            }
            line = pr.readLine();
        }
        // Cast is save, since contained DefaultExchangeRate is Comparable!
        for(List list:newSdrToCurrency.values()){
            Collections.sort(List.class.cast(list));
        }
        for(List list:newCurrencyToSdr.values()){
            Collections.sort(List.class.cast(list));
        }
        this.sdrToCurrency = newSdrToCurrency;
        this.currencyToSdr = newCurrencyToSdr;
        for(Map.Entry> entry: this.sdrToCurrency.entrySet()){
            LOGGER.finest("SDR -> " + entry.getKey().getCurrencyCode() + ": " + entry.getValue());
        }
        for(Map.Entry> entry: this.currencyToSdr.entrySet()){
            LOGGER.finest(entry.getKey().getCurrencyCode() + " -> SDR: " + entry.getValue());
        }
    }

    private Double[] parseValues(String[] parts) throws ParseException {

		ArrayList result = new ArrayList<>();
		int index = 0;
		for (String part : parts) {
			if(index == 0) {
				index++;
				continue;
			}
			if (part.isEmpty() || "NA".equals(part)) {
				index++;
				result.add(null);
				continue;
			}
			index++;
			result.add(Double.valueOf(part.trim().replace(",", "")));
		}
		return result.toArray(new Double[parts.length - 1]);
	}

    private List readTimestamps(String line) throws ParseException {
        // Currency May 01, 2013 April 30, 2013 April 29, 2013 April 26, 2013
        // April 25, 2013
        SimpleDateFormat sdf = new SimpleDateFormat("MMMM dd, yyyy", Locale.ENGLISH);
        @SuppressWarnings("Annotator") String[] parts = line.split("\\\t");
        List dates = new ArrayList<>(parts.length);
        for (int i = 1; i < parts.length; i++) {
            Calendar date = GregorianCalendar.getInstance();
            date.setTime(sdf.parse(parts[i]));
            dates.add(LocalDate.from(date));
        }
        return dates;
    }

    @Override
    public ExchangeRate getExchangeRate(ConversionQuery conversionQuery) {
        try {
            if (loadLock.await(30, TimeUnit.SECONDS)) {
                if (currencyToSdr.isEmpty()) {
                    return null;
                }
                if (!isAvailable(conversionQuery)) {
                    return null;
                }
                CurrencyUnit base = conversionQuery.getBaseCurrency();
                CurrencyUnit term = conversionQuery.getCurrency();
                Calendar timestamp = conversionQuery.get(Calendar.class);
                if (timestamp == null) {
                    timestamp = conversionQuery.get(GregorianCalendar.class);
                }
                ExchangeRate rate1;
                ExchangeRate rate2;
                LocalDate localDate;
                if (timestamp == null) {
                    localDate = LocalDate.yesterday();
                    rate1 = lookupRate(currencyToSdr.get(base), localDate);
                    rate2 = lookupRate(sdrToCurrency.get(term), localDate);
                    if(rate1==null || rate2==null){
                        localDate = LocalDate.beforeDays(2);
                    }
                    rate1 = lookupRate(currencyToSdr.get(base), localDate);
                    rate2 = lookupRate(sdrToCurrency.get(term), localDate);
                    if(rate1==null || rate2==null){
                        localDate = LocalDate.beforeDays(3);
                        rate1 = lookupRate(currencyToSdr.get(base), localDate);
                        rate2 = lookupRate(sdrToCurrency.get(term), localDate);
                    }
                }
                else{
                    localDate = LocalDate.from(timestamp);
                    rate1 = lookupRate(currencyToSdr.get(base), localDate);
                    rate2 = lookupRate(sdrToCurrency.get(term), localDate);
                }
                if(rate1==null || rate2==null){
                    return null;
                }
                if (base.equals(SDR)) {
                    return rate2;
                } else if (term.equals(SDR)) {
                    return rate1;
                }
                ExchangeRateBuilder builder =
                        new ExchangeRateBuilder(ConversionContext.of(CONTEXT.getProviderName(), RateType.HISTORIC));
                builder.setBase(base);
                builder.setTerm(term);
                builder.setFactor(multiply(rate1.getFactor(), rate2.getFactor()));
                builder.setRateChain(rate1, rate2);
                return builder.build();
            }else{
                // Lets wait for a successful load only once, then answer requests as data is present.
                loadLock.countDown();
                throw new MonetaryException("Failed to load currency conversion data: " + loadState);
            }
        }
        catch(InterruptedException e){
            throw new MonetaryException("Failed to load currency conversion data: Load task has been interrupted.", e);
        }
    }

    private ExchangeRate lookupRate(List list, LocalDate localDate) {
        if (list==null) {
            return null;
        }
        for (ExchangeRate rate : list) {
            if (localDate==null) {
                localDate = LocalDate.now();
            }
            if (rate!=null) {
                return rate;
            }
        }
        return null;
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy