1616package org .javamoney .moneta .convert .internal ;
1717
1818import java .io .InputStream ;
19- import java .math .BigDecimal ;
19+ import java .math .MathContext ;
2020import 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 ;
2427import java .util .concurrent .ConcurrentHashMap ;
2528import java .util .logging .Level ;
2629
2730import javax .money .CurrencyUnit ;
2831import 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 ;
3039import javax .money .spi .Bootstrap ;
3140import javax .xml .parsers .SAXParser ;
3241import javax .xml .parsers .SAXParserFactory ;
3645import org .javamoney .moneta .spi .DefaultNumberValue ;
3746import org .javamoney .moneta .spi .LoaderService ;
3847import 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
5056 * @author Werner Keil
5157 */
5258public 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}
0 commit comments