Skip to content

Commit e02e3d2

Browse files
author
otaviojava
committed
adds ECBHistoric90RateProviderTest
1 parent 341d33b commit e02e3d2

File tree

4 files changed

+278
-156
lines changed

4 files changed

+278
-156
lines changed

src/main/java/org/javamoney/moneta/convert/internal/ECBHistoric90RateProvider.java

Lines changed: 107 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,26 @@
1616
package org.javamoney.moneta.convert.internal;
1717

1818
import java.io.InputStream;
19-
import java.math.BigDecimal;
19+
import java.math.MathContext;
2020
import java.net.MalformedURLException;
21-
import java.text.ParseException;
22-
import java.text.SimpleDateFormat;
23-
import java.util.*;
21+
import java.time.LocalDate;
22+
import java.time.ZoneId;
23+
import java.util.Comparator;
24+
import java.util.Date;
25+
import java.util.Map;
26+
import java.util.Objects;
2427
import java.util.concurrent.ConcurrentHashMap;
2528
import java.util.logging.Level;
2629

2730
import javax.money.CurrencyUnit;
2831
import javax.money.MonetaryCurrencies;
29-
import javax.money.convert.*;
32+
import javax.money.convert.ConversionContext;
33+
import javax.money.convert.ConversionContextBuilder;
34+
import javax.money.convert.ConversionQuery;
35+
import javax.money.convert.ExchangeRate;
36+
import javax.money.convert.ProviderContext;
37+
import javax.money.convert.ProviderContextBuilder;
38+
import javax.money.convert.RateType;
3039
import javax.money.spi.Bootstrap;
3140
import javax.xml.parsers.SAXParser;
3241
import javax.xml.parsers.SAXParserFactory;
@@ -36,9 +45,6 @@
3645
import org.javamoney.moneta.spi.DefaultNumberValue;
3746
import org.javamoney.moneta.spi.LoaderService;
3847
import org.javamoney.moneta.spi.LoaderService.LoaderListener;
39-
import org.xml.sax.Attributes;
40-
import org.xml.sax.SAXException;
41-
import org.xml.sax.helpers.DefaultHandler;
4248

4349
/**
4450
* This class implements an {@link javax.money.convert.ExchangeRateProvider} that loads data from
@@ -50,12 +56,12 @@
5056
* @author Werner Keil
5157
*/
5258
public class ECBHistoric90RateProvider extends AbstractRateProvider implements LoaderListener{
53-
/**
59+
/**
5460
* The data id used for the LoaderService.
5561
*/
5662
private static final String DATA_ID = ECBHistoric90RateProvider.class.getSimpleName();
63+
static final String BASE_CURRENCY_CODE = "EUR";
5764

58-
private static final String BASE_CURRENCY_CODE = "EUR";
5965
/**
6066
* Base currency of the loaded rates is always EUR.
6167
*/
@@ -64,7 +70,7 @@ public class ECBHistoric90RateProvider extends AbstractRateProvider implements L
6470
/**
6571
* Historic exchange rates, rate timestamp as UTC long.
6672
*/
67-
private final Map<Long,Map<String,ExchangeRate>> rates = new ConcurrentHashMap<>();
73+
private final Map<Long, Map<String, ExchangeRate>> historicRates = new ConcurrentHashMap<>();
6874
/**
6975
* Parser factory.
7076
*/
@@ -76,7 +82,9 @@ public class ECBHistoric90RateProvider extends AbstractRateProvider implements L
7682
ProviderContextBuilder.of("ECB-HIST90", RateType.HISTORIC, RateType.DEFERRED)
7783
.set("providerDescription", "European Central Bank (last 90 days)").set("days", 90).build();
7884

79-
/**
85+
private Long recentKey;
86+
87+
/*
8088
* Constructor, also loads initial data.
8189
*
8290
* @throws MalformedURLException
@@ -90,178 +98,126 @@ public ECBHistoric90RateProvider() throws MalformedURLException{
9098
loader.loadDataAsync(DATA_ID);
9199
}
92100

93-
/**
94-
* (Re)load the given data feed. Logs an error if loading fails.
95-
*/
96101
@Override
97102
public void newDataLoaded(String data, InputStream is){
98-
final int oldSize = this.rates.size();
103+
final int oldSize = this.historicRates.size();
99104
try{
100105
SAXParser parser = saxParserFactory.newSAXParser();
101-
parser.parse(is, new RateReadingHandler());
106+
parser.parse(is, new RateReadingHandler(historicRates, CONTEXT));
107+
recentKey = null;
102108
}
103109
catch(Exception e){
104110
LOGGER.log(Level.FINEST, "Error during data load.", e);
105111
}
106-
int newSize = this.rates.size();
112+
int newSize = this.historicRates.size();
107113
LOGGER.info("Loaded " + DATA_ID + " exchange rates for days:" + (newSize - oldSize));
108114
}
109115

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()){
116+
/*
117+
* (non-Javadoc)
118+
*
119+
* @see javax.money.convert.spi.ExchangeRateProviderSpi#getExchangeRateType
120+
* ()
121+
*/
122+
@Override
123+
public ProviderContext getProviderContext(){
124+
return CONTEXT;
125+
}
126+
127+
@Override
128+
public ExchangeRate getExchangeRate(ConversionQuery query){
129+
if(historicRates.isEmpty()){
120130
return null;
121131
}
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();
129132

130-
builder.setBase(conversionQuery.getBaseCurrency());
131-
builder.setTerm(conversionQuery.getCurrency());
132-
Map<String,ExchangeRate> targets = this.rates.get(targetTS);
133+
Long timeStampMillis = getMillisSeconds(query);
134+
ExchangeRateBuilder builder = getBuilder(query, timeStampMillis);
135+
136+
137+
Map<String, ExchangeRate> targets = this.historicRates
138+
.get(timeStampMillis);
133139
if(Objects.isNull(targets)){
134140
return null;
135141
}
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())){
142+
ExchangeRate sourceRate = targets.get(query.getBaseCurrency()
143+
.getCurrencyCode());
144+
ExchangeRate target = targets
145+
.get(query.getCurrency().getCurrencyCode());
146+
return createExchangeRate(query, builder, sourceRate, target);
147+
}
148+
149+
private ExchangeRate createExchangeRate(ConversionQuery query,
150+
ExchangeRateBuilder builder, ExchangeRate sourceRate,
151+
ExchangeRate target) {
152+
153+
if(areBothBaseCurrencies(query)){
140154
builder.setFactor(DefaultNumberValue.ONE);
141155
return builder.build();
142-
}else if(BASE_CURRENCY_CODE.equals(conversionQuery.getCurrency().getCurrencyCode())){
156+
} else if(BASE_CURRENCY_CODE.equals(query.getCurrency().getCurrencyCode())){
143157
if(Objects.isNull(sourceRate)){
144158
return null;
145159
}
146-
return getReversed(sourceRate);
147-
}else if(BASE_CURRENCY_CODE.equals(conversionQuery.getBaseCurrency().getCurrencyCode())){
160+
return reverse(sourceRate);
161+
} else if (BASE_CURRENCY_CODE.equals(query.getBaseCurrency()
162+
.getCurrencyCode())) {
148163
return target;
149-
}else{
164+
} else{
150165
// Get Conversion base as derived rate: base -> EUR -> term
151166
ExchangeRate rate1 = getExchangeRate(
152-
conversionQuery.toBuilder().setBaseCurrency(conversionQuery.getBaseCurrency())
153-
.setTermCurrency(MonetaryCurrencies.getCurrency(BASE_CURRENCY_CODE)).build());
167+
query.toBuilder().setTermCurrency(MonetaryCurrencies.getCurrency(BASE_CURRENCY_CODE)).build());
154168
ExchangeRate rate2 = getExchangeRate(
155-
conversionQuery.toBuilder().setBaseCurrency(MonetaryCurrencies.getCurrency(BASE_CURRENCY_CODE))
156-
.setTermCurrency(conversionQuery.getCurrency()).build());
169+
query.toBuilder().setBaseCurrency(MonetaryCurrencies.getCurrency(BASE_CURRENCY_CODE))
170+
.setTermCurrency(query.getCurrency()).build());
157171
if(Objects.nonNull(rate1) || Objects.nonNull(rate2)){
158172
builder.setFactor(multiply(rate1.getFactor(), rate2.getFactor()));
159173
builder.setRateChain(rate1, rate2);
160174
return builder.build();
161175
}
162176
return null;
163177
}
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-
}
178+
}
179+
180+
private boolean areBothBaseCurrencies(ConversionQuery query) {
181+
return BASE_CURRENCY_CODE.equals(query.getBaseCurrency().getCurrencyCode()) &&
182+
BASE_CURRENCY_CODE.equals(query.getCurrency().getCurrencyCode());
183+
}
184+
185+
private Long getMillisSeconds(ConversionQuery query) {
186+
if (Objects.nonNull(query.getTimestamp())) {
187+
LocalDate timeStamp = query.getTimestamp().toLocalDate();
188+
189+
Date date = Date.from(timeStamp.atStartOfDay()
190+
.atZone(ZoneId.systemDefault()).toInstant());
191+
Long timeStampMillis = date.getTime();
192+
return timeStampMillis;
193+
} else {
194+
return getRecentKey();
195+
}
196+
}
197+
198+
private Long getRecentKey() {
199+
if(Objects.isNull(recentKey)) {
200+
Comparator<Long> reversed = Comparator.<Long>naturalOrder().reversed();
201+
recentKey = historicRates.keySet().stream().sorted(reversed).findFirst().get();
202+
}
203+
return recentKey;
204+
}
205+
206+
private ExchangeRateBuilder getBuilder(ConversionQuery query,
207+
Long timeStampMillis) {
208+
ExchangeRateBuilder builder = new ExchangeRateBuilder(
209+
ConversionContextBuilder.create(CONTEXT, RateType.HISTORIC)
210+
.setTimestampMillis(timeStampMillis).build());
211+
builder.setBase(query.getBaseCurrency());
212+
builder.setTerm(query.getCurrency());
213+
return builder;
214+
}
215+
216+
private ExchangeRate reverse(ExchangeRate rate){
217+
if(Objects.isNull(rate)){
218+
throw new IllegalArgumentException("Rate null is not reversable.");
263219
}
264-
rateMap.put(term.getCurrencyCode(), exchangeRate);
220+
return new ExchangeRateBuilder(rate).setRate(rate).setBase(rate.getCurrency()).setTerm(rate.getBaseCurrency())
221+
.setFactor(divide(DefaultNumberValue.ONE, rate.getFactor(), MathContext.DECIMAL64)).build();
265222
}
266-
267223
}

src/main/java/org/javamoney/moneta/convert/internal/ECBHistoricRateProvider.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public class ECBHistoricRateProvider extends AbstractRateProvider implements
9090
/**
9191
* The {@link ConversionContext} of this provider.
9292
*/
93-
static final ProviderContext CONTEXT =
93+
private static final ProviderContext CONTEXT =
9494
ProviderContextBuilder.of("ECB-HIST", RateType.HISTORIC, RateType.DEFERRED)
9595
.set("providerDescription", "European Central Bank").set("days", 1500).build();
9696

@@ -115,7 +115,7 @@ public void newDataLoaded(String data, InputStream is){
115115
final int oldSize = this.historicRates.size();
116116
try{
117117
SAXParser parser = saxParserFactory.newSAXParser();
118-
parser.parse(is, new RateReadingHandler(historicRates));
118+
parser.parse(is, new RateReadingHandler(historicRates, CONTEXT));
119119
recentKey = null;
120120
}
121121
catch(Exception e){

src/main/java/org/javamoney/moneta/convert/internal/RateReadingHandler.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import javax.money.MonetaryCurrencies;
1414
import javax.money.convert.ConversionContextBuilder;
1515
import javax.money.convert.ExchangeRate;
16+
import javax.money.convert.ProviderContext;
1617
import javax.money.convert.RateType;
1718

1819
import org.javamoney.moneta.ExchangeRateBuilder;
@@ -47,8 +48,11 @@ class RateReadingHandler extends DefaultHandler{
4748

4849
private final Map<Long, Map<String, ExchangeRate>> historicRates;
4950

50-
public RateReadingHandler(Map<Long, Map<String, ExchangeRate>> historicRates) {
51+
private ProviderContext context;
52+
53+
public RateReadingHandler(Map<Long, Map<String, ExchangeRate>> historicRates, ProviderContext context) {
5154
this.historicRates = historicRates;
55+
this.context = context;
5256
}
5357

5458
@Override
@@ -86,9 +90,9 @@ void addRate(CurrencyUnit term, long timestamp, Number rate) {
8690
rateType = RateType.DEFERRED;
8791
}
8892
builder = new ExchangeRateBuilder(
89-
ConversionContextBuilder.create(ECBHistoricRateProvider.CONTEXT, rateType).setTimestampMillis(timestamp).build());
93+
ConversionContextBuilder.create(context, rateType).setTimestampMillis(timestamp).build());
9094
}else{
91-
builder = new ExchangeRateBuilder(ConversionContextBuilder.create(ECBHistoricRateProvider.CONTEXT, rateType).build());
95+
builder = new ExchangeRateBuilder(ConversionContextBuilder.create(context, rateType).build());
9296
}
9397
builder.setBase(ECBHistoricRateProvider.BASE_CURRENCY);
9498
builder.setTerm(term);

0 commit comments

Comments
 (0)