1+ /**
2+ * Copyright (c) 2012, 2014, Credit Suisse (Anatole Tresch), Werner Keil and others by the @author tag.
3+ *
4+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+ * use this file except in compliance with the License. You may obtain a copy of
6+ * the License at
7+ *
8+ * http://www.apache.org/licenses/LICENSE-2.0
9+ *
10+ * Unless required by applicable law or agreed to in writing, software
11+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+ * License for the specific language governing permissions and limitations under
14+ * the License.
15+ */
16+ package org .javamoney .moneta .convert .internal ;
17+
18+ import java .io .InputStream ;
19+ import java .math .MathContext ;
20+ import java .net .MalformedURLException ;
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 ;
27+ import java .util .concurrent .ConcurrentHashMap ;
28+ import java .util .logging .Level ;
29+
30+ import javax .money .CurrencyUnit ;
31+ import javax .money .MonetaryCurrencies ;
32+ import javax .money .convert .ConversionContextBuilder ;
33+ import javax .money .convert .ConversionQuery ;
34+ import javax .money .convert .CurrencyConversionException ;
35+ import javax .money .convert .ExchangeRate ;
36+ import javax .money .convert .ProviderContext ;
37+ import javax .money .convert .RateType ;
38+ import javax .money .spi .Bootstrap ;
39+ import javax .xml .parsers .SAXParser ;
40+ import javax .xml .parsers .SAXParserFactory ;
41+
42+ import org .javamoney .moneta .ExchangeRateBuilder ;
43+ import org .javamoney .moneta .spi .AbstractRateProvider ;
44+ import org .javamoney .moneta .spi .DefaultNumberValue ;
45+ import org .javamoney .moneta .spi .LoaderService ;
46+ import org .javamoney .moneta .spi .LoaderService .LoaderListener ;
47+
48+ /**
49+ * Base to all Europe Central Bank implementation.
50+ * @author otaviojava
51+ */
52+ abstract class AbstractECBCurrentRateProvider extends AbstractRateProvider implements
53+ LoaderListener {
54+
55+ static final String BASE_CURRENCY_CODE = "EUR" ;
56+
57+ /**
58+ * Base currency of the loaded rates is always EUR.
59+ */
60+ public static final CurrencyUnit BASE_CURRENCY = MonetaryCurrencies .getCurrency (BASE_CURRENCY_CODE );
61+
62+ /**
63+ * Historic exchange rates, rate timestamp as UTC long.
64+ */
65+ private final Map <Long , Map <String , ExchangeRate >> historicRates = new ConcurrentHashMap <>();
66+ /**
67+ * Parser factory.
68+ */
69+ private SAXParserFactory saxParserFactory = SAXParserFactory .newInstance ();
70+
71+ private Long recentKey ;
72+
73+ public AbstractECBCurrentRateProvider (ProviderContext context ) throws MalformedURLException {
74+ super (context );
75+ saxParserFactory .setNamespaceAware (false );
76+ saxParserFactory .setValidating (false );
77+ LoaderService loader = Bootstrap .getService (LoaderService .class );
78+ loader .addLoaderListener (this , getDataId ());
79+ loader .loadDataAsync (getDataId ());
80+ }
81+
82+ public abstract String getDataId ();
83+
84+ @ Override
85+ public void newDataLoaded (String data , InputStream is ){
86+ final int oldSize = this .historicRates .size ();
87+ try {
88+ SAXParser parser = saxParserFactory .newSAXParser ();
89+ parser .parse (is , new RateReadingHandler (historicRates , getProviderContext ()));
90+ recentKey = null ;
91+ }
92+ catch (Exception e ){
93+ LOGGER .log (Level .FINEST , "Error during data load." , e );
94+ }
95+ int newSize = this .historicRates .size ();
96+ LOGGER .info ("Loaded " + getDataId () + " exchange rates for days:" + (newSize - oldSize ));
97+ }
98+
99+
100+ @ Override
101+ public ExchangeRate getExchangeRate (ConversionQuery query ){
102+ Objects .requireNonNull (query );
103+ if (historicRates .isEmpty ()){
104+ return null ;
105+ }
106+
107+ Long timeStampMillis = getMillisSeconds (query );
108+ ExchangeRateBuilder builder = getBuilder (query , timeStampMillis );
109+
110+
111+ Map <String , ExchangeRate > targets = this .historicRates
112+ .get (timeStampMillis );
113+ if (Objects .isNull (targets )){
114+ return null ;
115+ }
116+ ExchangeRate sourceRate = targets .get (query .getBaseCurrency ()
117+ .getCurrencyCode ());
118+ ExchangeRate target = targets
119+ .get (query .getCurrency ().getCurrencyCode ());
120+ return createExchangeRate (query , builder , sourceRate , target );
121+ }
122+
123+ private ExchangeRate createExchangeRate (ConversionQuery query ,
124+ ExchangeRateBuilder builder , ExchangeRate sourceRate ,
125+ ExchangeRate target ) {
126+
127+ if (areBothBaseCurrencies (query )){
128+ builder .setFactor (DefaultNumberValue .ONE );
129+ return builder .build ();
130+ } else if (BASE_CURRENCY_CODE .equals (query .getCurrency ().getCurrencyCode ())){
131+ if (Objects .isNull (sourceRate )){
132+ return null ;
133+ }
134+ return reverse (sourceRate );
135+ } else if (BASE_CURRENCY_CODE .equals (query .getBaseCurrency ()
136+ .getCurrencyCode ())) {
137+ return target ;
138+ } else {
139+ // Get Conversion base as derived rate: base -> EUR -> term
140+ ExchangeRate rate1 = getExchangeRate (
141+ query .toBuilder ().setTermCurrency (MonetaryCurrencies .getCurrency (BASE_CURRENCY_CODE )).build ());
142+ ExchangeRate rate2 = getExchangeRate (
143+ query .toBuilder ().setBaseCurrency (MonetaryCurrencies .getCurrency (BASE_CURRENCY_CODE ))
144+ .setTermCurrency (query .getCurrency ()).build ());
145+ if (Objects .nonNull (rate1 ) && Objects .nonNull (rate2 )){
146+ builder .setFactor (multiply (rate1 .getFactor (), rate2 .getFactor ()));
147+ builder .setRateChain (rate1 , rate2 );
148+ return builder .build ();
149+ }
150+ throw new CurrencyConversionException (query .getBaseCurrency (),
151+ query .getCurrency (), sourceRate .getConversionContext ());
152+ }
153+ }
154+
155+ private boolean areBothBaseCurrencies (ConversionQuery query ) {
156+ return BASE_CURRENCY_CODE .equals (query .getBaseCurrency ().getCurrencyCode ()) &&
157+ BASE_CURRENCY_CODE .equals (query .getCurrency ().getCurrencyCode ());
158+ }
159+
160+ private Long getMillisSeconds (ConversionQuery query ) {
161+ if (Objects .nonNull (query .getTimestamp ())) {
162+ LocalDate timeStamp = query .getTimestamp ().toLocalDate ();
163+
164+ Date date = Date .from (timeStamp .atStartOfDay ()
165+ .atZone (ZoneId .systemDefault ()).toInstant ());
166+ Long timeStampMillis = date .getTime ();
167+ return timeStampMillis ;
168+ } else {
169+ return getRecentKey ();
170+ }
171+ }
172+
173+ private Long getRecentKey () {
174+ if (Objects .isNull (recentKey )) {
175+ Comparator <Long > reversed = Comparator .<Long >naturalOrder ().reversed ();
176+ recentKey = historicRates .keySet ().stream ().sorted (reversed ).findFirst ().get ();
177+ }
178+ return recentKey ;
179+ }
180+
181+ private ExchangeRateBuilder getBuilder (ConversionQuery query ,
182+ Long timeStampMillis ) {
183+ ExchangeRateBuilder builder = new ExchangeRateBuilder (
184+ ConversionContextBuilder .create (getProviderContext (), RateType .HISTORIC )
185+ .setTimestampMillis (timeStampMillis ).build ());
186+ builder .setBase (query .getBaseCurrency ());
187+ builder .setTerm (query .getCurrency ());
188+ return builder ;
189+ }
190+
191+ private ExchangeRate reverse (ExchangeRate rate ){
192+ if (Objects .isNull (rate )){
193+ throw new IllegalArgumentException ("Rate null is not reversable." );
194+ }
195+ return new ExchangeRateBuilder (rate ).setRate (rate ).setBase (rate .getCurrency ()).setTerm (rate .getBaseCurrency ())
196+ .setFactor (divide (DefaultNumberValue .ONE , rate .getFactor (), MathContext .DECIMAL64 )).build ();
197+ }
198+
199+ }
0 commit comments