/**
* Copyright (c) 2012, 2015, 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.yahoo;
//import static org.javamoney.moneta.spi.AbstractCurrencyConversion.KEY_SCALE;
import java.io.InputStream;
import java.math.MathContext;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Comparator;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
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.RateType;
import javax.money.spi.Bootstrap;
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;
import org.javamoney.moneta.spi.MonetaryConfig;
/**
* Base of Yahoo provider implementation
*
* @author skosoy@gmail.com
*/
abstract class YahooAbstractRateProvider extends AbstractRateProvider implements
LoaderListener {
private static final Logger LOG = Logger.getLogger(YahooAbstractRateProvider.class.getName());
private static final String BASE_CURRENCY_CODE = "USD";
private final String DIGIT_FRACTION_KEY = "yahoo.digit.fraction";
public static final CurrencyUnit BASE_CURRENCY = Monetary.getCurrency(BASE_CURRENCY_CODE);
protected final Map<LocalDate, Map<String, ExchangeRate>> rates = new ConcurrentHashMap<>();
private final ProviderContext context;
protected abstract String getDataId();
YahooAbstractRateProvider(ProviderContext context) {
super(context);
this.context = context;
final LoaderService loader = Bootstrap.getService(LoaderService.class);
loader.addLoaderListener(this, getDataId());
loader.loadDataAsync(getDataId());
}
@Override
public void newDataLoaded(final String resourceId, final InputStream is) {
final int oldSize = this.rates.size();
try {
final YahooRateReadingHandler parser =
new YahooRateReadingHandler(rates, getContext());
parser.parse(is);
} catch (Exception e) {
LOG.log(Level.FINEST, "Error during data load.", e);
}
int newSize = this.rates.size();
LOG.info("Loaded " + resourceId + " exchange rates for days:" + (newSize - oldSize));
}
@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 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 ECBRateProvider."));
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 ECBRateProvider.");
}
}
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());
}
private ExchangeRateBuilder getBuilder(ConversionQuery query, LocalDate localDate) {
ExchangeRateBuilder builder = new ExchangeRateBuilder(getExchangeContext(DIGIT_FRACTION_KEY));
builder.setBase(query.getBaseCurrency());
builder.setTerm(query.getCurrency());
return builder;
}
private 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()).setContext(getExchangeContext(DIGIT_FRACTION_KEY))
.setFactor(divide(DefaultNumberValue.ONE, rate.getFactor(), MathContext.DECIMAL64)).build();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getName()).append('{')
.append(" context: ").append(context).append('}');
return sb.toString();
}
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;
}
}
// Patch for post 1.0 API in Moneta
private static final String KEY_SCALE = "exchangeRateScale";
@Override
protected int getScale(String key) {
String string = MonetaryConfig.getConfig().getOrDefault(
key, "-1");
if (string.isEmpty()) {
return -1;
} else {
try {
return Integer.valueOf(string);
} catch (NumberFormatException e) {
return -1;
}
}
}
@Override
protected ConversionContext getExchangeContext(String key) {
int scale = getScale(key);
if(scale < 0) {
return ConversionContext.of(this.context.getProviderName(), RateType.HISTORIC);
} else {
return ConversionContext.of(this.context.getProviderName(), RateType.HISTORIC).toBuilder().set(KEY_SCALE, scale).build();
}
}
@Override
protected LocalDate[] getQueryDates(ConversionQuery query) {
if (Objects.nonNull(query.get(LocalDate.class)) || Objects.nonNull(query.get(LocalDateTime.class))) {
LocalDate localDate = Optional.ofNullable(query.get(LocalDate.class)).orElseGet(() -> query.get(LocalDateTime.class).toLocalDate());
return new LocalDate[]{localDate};
} else if(Objects.nonNull(query.get(LocalDate[].class))) {
return query.get(LocalDate[].class);
}
return null;
}
}