package org.javamoney.moneta.internal.convert.frb; import java.io.InputStream; import java.math.MathContext; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.money.CurrencyUnit; import javax.money.Monetary; import javax.money.MonetaryException; import javax.money.convert.ConversionContext; import javax.money.convert.ConversionQuery; import javax.money.convert.CurrencyConversionException; import javax.money.convert.ExchangeRate; import javax.money.convert.ProviderContext; import javax.money.convert.ProviderContextBuilder; import javax.money.convert.RateType; import javax.money.spi.Bootstrap; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; 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; /** * <p> * This class implements an {@link javax.money.convert.ExchangeRateProvider} * that loads data from the Federal Reserve Bank of the United States RSS feed. * The Fed publishes rates on its RSS feed for the prior week, Monday through Friday. * This provider loads all available rates, purging from its cache any older rates * with each re-load. * </p> */ public class USFederalReserveRateProvider extends AbstractRateProvider implements LoaderListener { private static final Logger LOG = Logger.getLogger(USFederalReserveRateProvider.class.getName()); private static final String DATA_ID = USFederalReserveRateProvider.class.getSimpleName(); protected static final String BASE_CURRENCY_CODE = "USD"; /** * Base currency of the loaded rates is always USD. */ public static final CurrencyUnit BASE_CURRENCY = Monetary.getCurrency(BASE_CURRENCY_CODE); /** * Historic exchange rates, rate timestamp as UTC long. */ private final Map<LocalDate, Map<String, ExchangeRate>> rates = new ConcurrentHashMap<>(); /** * Parser factory. */ private final SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); /** * The {@link ConversionContext} of this provider. */ private static final ProviderContext CONTEXT = ProviderContextBuilder.of("FRB", RateType.HISTORIC) .set("providerDescription", "Federal Reserve Bank of the United States").build(); public USFederalReserveRateProvider() { super(CONTEXT); initalize(); } public USFederalReserveRateProvider(ProviderContext providerContext) { super(providerContext); initalize(); } private void initalize() { saxParserFactory.setNamespaceAware(false); saxParserFactory.setValidating(false); LoaderService loader = Bootstrap.getService(LoaderService.class); loader.addLoaderListener(this, getDataId()); try { loader.loadDataAsync(getDataId()); } catch(Exception e) { throw new RuntimeException(e); } } private String getDataId() { return DATA_ID; } @Override public ExchangeRate getExchangeRate(ConversionQuery conversionQuery) { Objects.requireNonNull(conversionQuery); if (rates.isEmpty()) { return null; } RateResult result = findExchangeRate(conversionQuery); ExchangeRateBuilder builder = getBuilder(conversionQuery, result.date); ExchangeRate sourceRate = result.targets.get(conversionQuery.getBaseCurrency().getCurrencyCode()); ExchangeRate target = result.targets.get(conversionQuery.getCurrency().getCurrencyCode()); return createExchangeRate(conversionQuery, builder, sourceRate, target); } private ExchangeRateBuilder getBuilder(ConversionQuery query, LocalDate localDate) { ExchangeRateBuilder builder = new ExchangeRateBuilder(getExchangeContext("frb.digit.fraction")); builder.setBase(query.getBaseCurrency()); builder.setTerm(query.getCurrency()); return builder; } @Override public void newDataLoaded(String resourceId, InputStream is) { final int oldSize = this.rates.size(); try { Map<LocalDate, Map<String, ExchangeRate>> newRates = new HashMap<>(); SAXParser parser = saxParserFactory.newSAXParser(); parser.parse(is, new USFederalReserveRateReadingHandler(newRates, getContext())); //Remove any older rates so the map continually only has one week of rates cached Set<LocalDate> existingDates = new HashSet<>(rates.keySet()); rates.putAll(newRates); for(LocalDate ld : existingDates) { if(!newRates.containsKey(ld)) { rates.remove(ld); } } } catch (Exception e) { LOG.log(Level.WARNING, "Error during data load.", e); } int newSize = this.rates.size(); LOG.info("Loaded " + resourceId + " exchange rates for days:" + (newSize - oldSize)); } private RateResult findExchangeRate(ConversionQuery conversionQuery) { LocalDate[] dates = getQueryDates(conversionQuery); if (dates == null) { Comparator<LocalDate> comparator = Comparator.naturalOrder(); LocalDate date = this.rates .keySet() .stream() .sorted(comparator.reversed()) .findFirst() .orElseThrow( () -> new MonetaryException("There is not more recent exchange rate to rate on " + getDataId())); return new RateResult(date, this.rates.get(date)); } else { for (LocalDate localDate : dates) { Map<String, ExchangeRate> targets = this.rates.get(localDate); if (Objects.nonNull(targets)) { return new RateResult(localDate, targets); } } String datesOnErros = Stream.of(dates).map(date -> date.format(DateTimeFormatter.ISO_LOCAL_DATE)) .collect(Collectors.joining(",")); throw new MonetaryException("There is not exchange on day " + datesOnErros + " to rate to rate on " + getDataId() + "."); } } private ExchangeRate createExchangeRate(ConversionQuery query, ExchangeRateBuilder builder, ExchangeRate sourceRate, ExchangeRate target) { if (areBothBaseCurrencies(query)) { builder.setFactor(DefaultNumberValue.ONE); return builder.build(); } else if (BASE_CURRENCY_CODE.equals(query.getCurrency().getCurrencyCode())) { if (Objects.isNull(sourceRate)) { return null; } return reverse(sourceRate); } else if (BASE_CURRENCY_CODE.equals(query.getBaseCurrency().getCurrencyCode())) { return target; } else { ExchangeRate rate1 = getExchangeRate(query.toBuilder().setTermCurrency(Monetary.getCurrency(BASE_CURRENCY_CODE)).build()); ExchangeRate rate2 = getExchangeRate(query.toBuilder().setBaseCurrency(Monetary.getCurrency(BASE_CURRENCY_CODE)) .setTermCurrency(query.getCurrency()).build()); if (Objects.nonNull(rate1) && Objects.nonNull(rate2)) { builder.setFactor(multiply(rate1.getFactor(), rate2.getFactor())); builder.setRateChain(rate1, rate2); return builder.build(); } throw new CurrencyConversionException(query.getBaseCurrency(), query.getCurrency(), sourceRate.getContext()); } } private boolean areBothBaseCurrencies(ConversionQuery query) { return BASE_CURRENCY_CODE.equals(query.getBaseCurrency().getCurrencyCode()) && BASE_CURRENCY_CODE.equals(query.getCurrency().getCurrencyCode()); } protected static ExchangeRate reverse(ExchangeRate rate) { if (Objects.isNull(rate)) { throw new IllegalArgumentException("Rate null is not reversible."); } return new ExchangeRateBuilder(rate).setRate(rate).setBase(rate.getCurrency()).setTerm(rate.getBaseCurrency()) .setFactor(divide(DefaultNumberValue.ONE, rate.getFactor(), MathContext.DECIMAL64)).build(); } private class RateResult { private final LocalDate date; private final Map<String, ExchangeRate> targets; RateResult(LocalDate date, Map<String, ExchangeRate> targets) { this.date = date; this.targets = targets; } } }