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 .ExchangeRate ;
35+ import javax .money .convert .ProviderContext ;
36+ import javax .money .convert .RateType ;
37+ import javax .money .spi .Bootstrap ;
38+ import javax .xml .parsers .SAXParser ;
39+ import javax .xml .parsers .SAXParserFactory ;
40+
41+ import org .javamoney .moneta .ExchangeRateBuilder ;
42+ import org .javamoney .moneta .spi .AbstractRateProvider ;
43+ import org .javamoney .moneta .spi .DefaultNumberValue ;
44+ import org .javamoney .moneta .spi .LoaderService ;
45+ import org .javamoney .moneta .spi .LoaderService .LoaderListener ;
46+
47+ /**
48+ * Base to all Europe Central Bank implementation.
49+ * @author otaviojava
50+ */
51+ abstract class AbstractECBCurrentRateProvider extends AbstractRateProvider implements
52+ LoaderListener {
53+
54+ static final String BASE_CURRENCY_CODE = "EUR" ;
55+
56+ /**
57+ * Base currency of the loaded rates is always EUR.
58+ */
59+ public static final CurrencyUnit BASE_CURRENCY = MonetaryCurrencies .getCurrency (BASE_CURRENCY_CODE );
60+
61+ /**
62+ * Historic exchange rates, rate timestamp as UTC long.
63+ */
64+ private final Map <Long , Map <String , ExchangeRate >> historicRates = new ConcurrentHashMap <>();
65+ /**
66+ * Parser factory.
67+ */
68+ private SAXParserFactory saxParserFactory = SAXParserFactory .newInstance ();
69+
70+ private Long recentKey ;
71+
72+ public AbstractECBCurrentRateProvider (ProviderContext context ) throws MalformedURLException {
73+ super (context );
74+ saxParserFactory .setNamespaceAware (false );
75+ saxParserFactory .setValidating (false );
76+ LoaderService loader = Bootstrap .getService (LoaderService .class );
77+ loader .addLoaderListener (this , getDataId ());
78+ loader .loadDataAsync (getDataId ());
79+ }
80+
81+ public abstract String getDataId ();
82+
83+ @ Override
84+ public void newDataLoaded (String data , InputStream is ){
85+ final int oldSize = this .historicRates .size ();
86+ try {
87+ SAXParser parser = saxParserFactory .newSAXParser ();
88+ parser .parse (is , new RateReadingHandler (historicRates , getProviderContext ()));
89+ recentKey = null ;
90+ }
91+ catch (Exception e ){
92+ LOGGER .log (Level .FINEST , "Error during data load." , e );
93+ }
94+ int newSize = this .historicRates .size ();
95+ LOGGER .info ("Loaded " + getDataId () + " exchange rates for days:" + (newSize - oldSize ));
96+ }
97+
98+
99+ @ Override
100+ public ExchangeRate getExchangeRate (ConversionQuery query ){
101+ if (historicRates .isEmpty ()){
102+ return null ;
103+ }
104+
105+ Long timeStampMillis = getMillisSeconds (query );
106+ ExchangeRateBuilder builder = getBuilder (query , timeStampMillis );
107+
108+
109+ Map <String , ExchangeRate > targets = this .historicRates
110+ .get (timeStampMillis );
111+ if (Objects .isNull (targets )){
112+ return null ;
113+ }
114+ ExchangeRate sourceRate = targets .get (query .getBaseCurrency ()
115+ .getCurrencyCode ());
116+ ExchangeRate target = targets
117+ .get (query .getCurrency ().getCurrencyCode ());
118+ return createExchangeRate (query , builder , sourceRate , target );
119+ }
120+
121+ private ExchangeRate createExchangeRate (ConversionQuery query ,
122+ ExchangeRateBuilder builder , ExchangeRate sourceRate ,
123+ ExchangeRate target ) {
124+
125+ if (areBothBaseCurrencies (query )){
126+ builder .setFactor (DefaultNumberValue .ONE );
127+ return builder .build ();
128+ } else if (BASE_CURRENCY_CODE .equals (query .getCurrency ().getCurrencyCode ())){
129+ if (Objects .isNull (sourceRate )){
130+ return null ;
131+ }
132+ return reverse (sourceRate );
133+ } else if (BASE_CURRENCY_CODE .equals (query .getBaseCurrency ()
134+ .getCurrencyCode ())) {
135+ return target ;
136+ } else {
137+ // Get Conversion base as derived rate: base -> EUR -> term
138+ ExchangeRate rate1 = getExchangeRate (
139+ query .toBuilder ().setTermCurrency (MonetaryCurrencies .getCurrency (BASE_CURRENCY_CODE )).build ());
140+ ExchangeRate rate2 = getExchangeRate (
141+ query .toBuilder ().setBaseCurrency (MonetaryCurrencies .getCurrency (BASE_CURRENCY_CODE ))
142+ .setTermCurrency (query .getCurrency ()).build ());
143+ if (Objects .nonNull (rate1 ) || Objects .nonNull (rate2 )){
144+ builder .setFactor (multiply (rate1 .getFactor (), rate2 .getFactor ()));
145+ builder .setRateChain (rate1 , rate2 );
146+ return builder .build ();
147+ }
148+ return null ;
149+ }
150+ }
151+
152+ private boolean areBothBaseCurrencies (ConversionQuery query ) {
153+ return BASE_CURRENCY_CODE .equals (query .getBaseCurrency ().getCurrencyCode ()) &&
154+ BASE_CURRENCY_CODE .equals (query .getCurrency ().getCurrencyCode ());
155+ }
156+
157+ private Long getMillisSeconds (ConversionQuery query ) {
158+ if (Objects .nonNull (query .getTimestamp ())) {
159+ LocalDate timeStamp = query .getTimestamp ().toLocalDate ();
160+
161+ Date date = Date .from (timeStamp .atStartOfDay ()
162+ .atZone (ZoneId .systemDefault ()).toInstant ());
163+ Long timeStampMillis = date .getTime ();
164+ return timeStampMillis ;
165+ } else {
166+ return getRecentKey ();
167+ }
168+ }
169+
170+ private Long getRecentKey () {
171+ if (Objects .isNull (recentKey )) {
172+ Comparator <Long > reversed = Comparator .<Long >naturalOrder ().reversed ();
173+ recentKey = historicRates .keySet ().stream ().sorted (reversed ).findFirst ().get ();
174+ }
175+ return recentKey ;
176+ }
177+
178+ private ExchangeRateBuilder getBuilder (ConversionQuery query ,
179+ Long timeStampMillis ) {
180+ ExchangeRateBuilder builder = new ExchangeRateBuilder (
181+ ConversionContextBuilder .create (getProviderContext (), RateType .HISTORIC )
182+ .setTimestampMillis (timeStampMillis ).build ());
183+ builder .setBase (query .getBaseCurrency ());
184+ builder .setTerm (query .getCurrency ());
185+ return builder ;
186+ }
187+
188+ private ExchangeRate reverse (ExchangeRate rate ){
189+ if (Objects .isNull (rate )){
190+ throw new IllegalArgumentException ("Rate null is not reversable." );
191+ }
192+ return new ExchangeRateBuilder (rate ).setRate (rate ).setBase (rate .getCurrency ()).setTerm (rate .getBaseCurrency ())
193+ .setFactor (divide (DefaultNumberValue .ONE , rate .getFactor (), MathContext .DECIMAL64 )).build ();
194+ }
195+
196+ }
0 commit comments