SOLR-3255: OpenExchangeRates.Org Exchange Rate Provider and improved ExchangeRateProvider interface

git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1307063 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Jan Høydahl 2012-03-29 19:14:07 +00:00
parent ea611beacc
commit e3374c9c9b
11 changed files with 570 additions and 23 deletions

View File

@ -581,6 +581,8 @@ New Features
* SOLR-2202: Currency FieldType, whith support for currencies and exchange rates
(Greg Fodor & Andrew Morrison via janhoy, rmuir, Uwe Schindler)
* SOLR-3255: OpenExchangeRates.Org Exchange Rate Provider for CurrencyField (janhoy)
* SOLR-3026: eDismax: Locking down which fields can be explicitly queried (user fields aka uf)
(janhoy, hossmann, Tomás Fernández Löbbe)

View File

@ -47,11 +47,11 @@ import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Currency;
import java.util.HashMap;
import java.util.List;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* Field type for support of monetary values.
@ -62,7 +62,7 @@ public class CurrencyField extends FieldType implements SchemaAware, ResourceLoa
protected static final String PARAM_DEFAULT_CURRENCY = "defaultCurrency";
protected static final String PARAM_RATE_PROVIDER_CLASS = "providerClass";
protected static final Object PARAM_PRECISION_STEP = "precisionStep";
protected static final String DEFAULT_RATE_PROVIDER_CLASS = "org.apache.solr.schema.FileExchangeRateProvider";
protected static final String DEFAULT_RATE_PROVIDER_CLASS = "solr.FileExchangeRateProvider";
protected static final String DEFAULT_DEFAULT_CURRENCY = "USD";
protected static final String DEFAULT_PRECISION_STEP = "0";
protected static final String FIELD_SUFFIX_AMOUNT_RAW = "_amount_raw";
@ -117,8 +117,7 @@ public class CurrencyField extends FieldType implements SchemaAware, ResourceLoa
args.remove(PARAM_PRECISION_STEP);
try {
// TODO: Are we using correct classloader?
Class<?> c = Class.forName(exchangeRateProviderClass);
Class<?> c = schema.getResourceLoader().findClass(exchangeRateProviderClass);
Object clazz = c.newInstance();
if (clazz instanceof ExchangeRateProvider) {
provider = (ExchangeRateProvider) clazz;
@ -512,14 +511,15 @@ class FileExchangeRateProvider implements ExchangeRateProvider {
}
@Override
public String[] listAvailableCurrencies() {
List<String> pairs = new ArrayList<String>();
public Set<String> listAvailableCurrencies() {
Set<String> currencies = new HashSet<String>();
for(String from : rates.keySet()) {
currencies.add(from);
for(String to : rates.get(from).keySet()) {
pairs.add(from+","+to);
currencies.add(to);
}
}
return pairs.toArray(new String[1]);
return currencies;
}
@Override

View File

@ -17,6 +17,7 @@ package org.apache.solr.schema;
*/
import java.util.Map;
import java.util.Set;
import org.apache.solr.common.ResourceLoader;
import org.apache.solr.common.SolrException;
@ -35,11 +36,10 @@ public interface ExchangeRateProvider {
public double getExchangeRate(String sourceCurrencyCode, String targetCurrencyCode) throws SolrException;
/**
* List all configured currency code pairs
* @return a string array of <a href="http://en.wikipedia.org/wiki/ISO_4217">ISO 4217</a> currency codes on the format
* ["SRC,DST", "SRC,DST"...]
* List all configured currency codes which are valid as source/target for this Provider
* @return a Set of <a href="http://en.wikipedia.org/wiki/ISO_4217">ISO 4217</a> currency code strings
*/
public String[] listAvailableCurrencies();
public Set<String> listAvailableCurrencies();
/**
* Ask the currency provider to explicitly reload/refresh its configuration.

View File

@ -0,0 +1,257 @@
package org.apache.solr.schema;
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.apache.noggit.JSONParser;
import org.apache.solr.common.ResourceLoader;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Exchange Rates Provider for {@link CurrencyField} implementing the freely available
* exchange rates from openexchangerates.org
* <p/>
* <b>Disclaimer:</b> This data is collected from various providers and provided free of charge
* for informational purposes only, with no guarantee whatsoever of accuracy, validity,
* availability or fitness for any purpose; use at your own risk. Other than that - have
* fun, and please share/watch/fork if you think data like this should be free!
*/
public class OpenExchangeRatesOrgProvider implements ExchangeRateProvider {
public static Logger log = LoggerFactory.getLogger(OpenExchangeRatesOrgProvider.class);
protected static final String PARAM_RATES_FILE_LOCATION = "ratesFileLocation";
protected static final String PARAM_REFRESH_INTERVAL = "refreshInterval";
protected static final String DEFAULT_RATES_FILE_LOCATION = "http://openexchangerates.org/latest.json";
protected static final String DEFAULT_REFRESH_INTERVAL = "1440";
protected String ratesFileLocation;
protected int refreshInterval;
protected ResourceLoader resourceLoader;
protected OpenExchangeRates rates;
/**
* Returns the currently known exchange rate between two currencies. The rates are fetched from
* the freely available OpenExchangeRates.org JSON, hourly updated. All rates are symmetrical with
* base currency being USD by default.
*
* @param sourceCurrencyCode The source currency being converted from.
* @param targetCurrencyCode The target currency being converted to.
* @return The exchange rate.
* @throws an exception if the requested currency pair cannot be found
*/
public double getExchangeRate(String sourceCurrencyCode, String targetCurrencyCode) {
if (rates == null) {
throw new SolrException(SolrException.ErrorCode.SERVICE_UNAVAILABLE, "Rates not initialized.");
}
if (sourceCurrencyCode == null || targetCurrencyCode == null) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Cannot get exchange rate; currency was null.");
}
if (rates.getTimestamp() + refreshInterval*60*1000 > System.currentTimeMillis()) {
log.debug("Refresh interval has expired. Refreshing exchange rates.");
reload();
}
Double source = (Double) rates.getRates().get(sourceCurrencyCode);
Double target = (Double) rates.getRates().get(targetCurrencyCode);
if (source == null || target == null) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
"No available conversion rate from " + sourceCurrencyCode + " to " + targetCurrencyCode + ". "
+ "Available rates are "+listAvailableCurrencies());
}
return target / source;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OpenExchangeRatesOrgProvider that = (OpenExchangeRatesOrgProvider) o;
return !(rates != null ? !rates.equals(that.rates) : that.rates != null);
}
@Override
public int hashCode() {
return rates != null ? rates.hashCode() : 0;
}
public String toString() {
return "["+this.getClass().getName()+" : " + rates.getRates().size() + " rates.]";
}
@Override
public Set<String> listAvailableCurrencies() {
if (rates == null)
throw new SolrException(ErrorCode.SERVER_ERROR, "Rates not initialized");
return rates.getRates().keySet();
}
@Override
public boolean reload() throws SolrException {
InputStream ratesJsonStream = null;
try {
log.info("Reloading exchange rates from "+ratesFileLocation);
try {
ratesJsonStream = (new URL(ratesFileLocation)).openStream();
} catch (Exception e) {
ratesJsonStream = resourceLoader.openResource(ratesFileLocation);
}
rates = new OpenExchangeRates(ratesJsonStream);
return true;
} catch (Exception e) {
throw new SolrException(ErrorCode.SERVER_ERROR, "Error reloading exchange rates", e);
} finally {
if (ratesJsonStream != null) try {
ratesJsonStream.close();
} catch (IOException e) {
throw new SolrException(ErrorCode.SERVER_ERROR, "Error closing stream", e);
}
}
}
@Override
public void init(Map<String,String> params) throws SolrException {
try {
ratesFileLocation = getParam(params.get(PARAM_RATES_FILE_LOCATION), DEFAULT_RATES_FILE_LOCATION);
refreshInterval = Integer.parseInt(getParam(params.get(PARAM_REFRESH_INTERVAL), DEFAULT_REFRESH_INTERVAL));
// Force a refresh interval of minimum one hour, since the API does not offer better resolution
if (refreshInterval < 60) {
refreshInterval = 60;
log.warn("Specified refreshInterval was too small. Setting to 60 minutes which is the update rate of openexchangerates.org");
}
log.info("Initialized with rates="+ratesFileLocation+", refreshInterval="+refreshInterval+".");
} catch (Exception e) {
throw new SolrException(ErrorCode.BAD_REQUEST, "Error initializing", e);
} finally {
// Removing config params custom to us
params.remove(PARAM_RATES_FILE_LOCATION);
params.remove(PARAM_REFRESH_INTERVAL);
}
}
@Override
public void inform(ResourceLoader loader) throws SolrException {
resourceLoader = loader;
reload();
}
private String getParam(String param, String defaultParam) {
return param == null ? defaultParam : param;
}
/**
* A simple class encapsulating the JSON data from openexchangerates.org
*/
class OpenExchangeRates {
private Map<String, Double> rates;
private String baseCurrency;
private long timestamp;
private String disclaimer;
private String license;
private JSONParser parser;
public OpenExchangeRates(InputStream ratesStream) throws IOException {
parser = new JSONParser(new InputStreamReader(ratesStream));
rates = new HashMap<String, Double>();
int ev;
do {
ev = parser.nextEvent();
switch( ev ) {
case JSONParser.STRING:
if( parser.wasKey() ) {
String key = parser.getString();
if(key.equals("disclaimer")) {
parser.nextEvent();
disclaimer = parser.getString();
} else if(key.equals("license")) {
parser.nextEvent();
license = parser.getString();
} else if(key.equals("timestamp")) {
parser.nextEvent();
timestamp = parser.getLong();
} else if(key.equals("base")) {
parser.nextEvent();
baseCurrency = parser.getString();
} else if(key.equals("rates")) {
ev = parser.nextEvent();
assert(ev == JSONParser.OBJECT_START);
ev = parser.nextEvent();
while (ev != JSONParser.OBJECT_END) {
String curr = parser.getString();
ev = parser.nextEvent();
Double rate = parser.getDouble();
rates.put(curr, rate);
ev = parser.nextEvent();
}
} else {
log.warn("Unknown key "+key);
}
break;
} else {
log.warn("Expected key, got "+JSONParser.getEventString(ev));
break;
}
case JSONParser.OBJECT_END:
case JSONParser.OBJECT_START:
break;
default:
log.info("Noggit UNKNOWN_EVENT_ID:"+JSONParser.getEventString(ev));
break;
}
} while( ev != JSONParser.EOF);
}
public Map<String, Double> getRates() {
return rates;
}
public long getTimestamp() {
return timestamp;
}
public String getDisclaimer() {
return disclaimer;
}
public String getBaseCurrency() {
return baseCurrency;
}
public String getLicense() {
return license;
}
}
}

View File

@ -0,0 +1,167 @@
{
"disclaimer": "This data is collected from various providers and provided free of charge for informational purposes only, with no guarantee whatsoever of accuracy, validity, availability or fitness for any purpose; use at your own risk. Other than that - have fun, and please share/watch/fork if you think data like this should be free!",
"license": "Data collected from various providers with public-facing APIs; copyright may apply; not for resale; no warranties given.",
"timestamp": 1332070464,
"base": "USD",
"rates": {
"AED": 3.6732,
"AFN": 48.299999,
"ALL": 105.919998,
"AMD": 388.890015,
"ANG": 1.79,
"AOA": 94.769997,
"ARS": 4.35,
"AUD": 0.943931,
"AWG": 1.7899,
"AZN": 0.7863,
"BAM": 1.48775,
"BBD": 2,
"BDT": 82,
"BGN": 1.4962,
"BHD": 0.37703,
"BIF": 1304.170044,
"BMD": 1,
"BND": 1.2575,
"BOB": 6.91,
"BRL": 1.8003,
"BSD": 1,
"BTN": 50.185001,
"BWP": 7.2307,
"BYR": 8150,
"BZD": 1.9135,
"CAD": 0.9921,
"CDF": 917.276917,
"CHF": 0.9164,
"CLF": 0.02146,
"CLP": 482.75,
"CNY": 6.3239,
"COP": 1760,
"CRC": 507.600006,
"CUP": 1,
"CVE": 84.190002,
"CZK": 18.606001,
"DJF": 179.490005,
"DKK": 5.64424,
"DOP": 39.025002,
"DZD": 74.544998,
"EGP": 6.0385,
"ETB": 17.720449,
"EUR": 0.758956,
"FJD": 1.7734,
"FKP": 0.6316,
"GBP": 0.631373,
"GEL": 1.6469,
"GHS": 1.7455,
"GIP": 0.63165,
"GMD": 31.5,
"GNF": 7100,
"GTQ": 7.6975,
"GYD": 203.699997,
"HKD": 7.76306,
"HNL": 19.055,
"HRK": 5.7333,
"HTG": 41,
"HUF": 219.850006,
"IDR": 9118,
"IEP": 0.5978,
"ILS": 3.7542,
"INR": 50.165001,
"IQD": 1165.5,
"IRR": 12308,
"ISK": 127.440002,
"JMD": 86.699997,
"JOD": 0.7095,
"JPY": 83.445,
"KES": 83.18,
"KGS": 46.699402,
"KHR": 4010.300049,
"KMF": 373.424255,
"KPW": 900,
"KRW": 1125.849976,
"KWD": 0.27925,
"KZT": 147.690002,
"LAK": 7993.799805,
"LBP": 1504,
"LKR": 125.224998,
"LRD": 73.459999,
"LSL": 7.5768,
"LTL": 2.6219,
"LVL": 0.5291,
"LYD": 1.2572,
"MAD": 8.4611,
"MDL": 11.89,
"MGA": 2155,
"MKD": 46.705002,
"MMK": 6.51,
"MNT": 1322.5,
"MOP": 7.9958,
"MRO": 293,
"MUR": 29.110001,
"MVR": 15.36,
"MWK": 165.206207,
"MXN": 12.6745,
"MYR": 3.0575,
"MZN": 27.200001,
"NAD": 7.58,
"NGN": 157.600006,
"NIO": 23.215,
"NOK": 5.73163,
"NPR": 80.620003,
"NZD": 1.212269,
"OMR": 0.38485,
"PAB": 1,
"PEN": 2.674,
"PGK": 2.0627,
"PHP": 43.02,
"PKR": 90.800003,
"PLN": 3.1285,
"PYG": 4245,
"QAR": 3.6415,
"RON": 3.3256,
"RSD": 84.100502,
"RUB": 29.2342,
"RWF": 606.717468,
"SAR": 3.7505,
"SBD": 7.075973,
"SCR": 14.0447,
"SDG": 2.6765,
"SEK": 6.74525,
"SGD": 1.258,
"SHP": 0.63165,
"SLL": 4364.5,
"SOS": 1629,
"SRD": 3.2875,
"STD": 18650,
"SVC": 8.7475,
"SYP": 57.450001,
"SZL": 7.5752,
"THB": 30.700001,
"TJS": 4.7588,
"TMT": 2.85,
"TND": 1.5178,
"TOP": 1.693601,
"TRY": 1.796,
"TTD": 6.40015,
"TWD": 29.532,
"TZS": 1595,
"UAH": 8.029,
"UGX": 2481.699951,
"USD": 1,
"UYU": 19.469999,
"UZS": 1835.75,
"VEF": 4.295,
"VND": 20820,
"VUV": 90.199997,
"WST": 2.247475,
"XAF": 497.898987,
"XCD": 2.7,
"XDR": 0.652794,
"XOF": 498.399994,
"XPF": 90.639999,
"YER": 216.005005,
"ZAR": 7.5688,
"ZMK": 5271.5,
"ZWD": 378.700012,
"ZWL": 322.355011
}
}

View File

@ -396,7 +396,10 @@
<!-- Currency type -->
<fieldType name="currency" class="solr.CurrencyField" currencyConfig="currency.xml"/>
<fieldType name="mock_currency" class="solr.CurrencyField" providerClass="org.apache.solr.schema.MockExchangeRateProvider" foo="bar" />
<fieldType name="mock_currency" class="solr.CurrencyField" providerClass="solr.MockExchangeRateProvider" foo="bar" />
<fieldType name="openexchangeratesorg_currency" class="solr.CurrencyField"
providerClass="solr.OpenExchangeRatesOrgProvider"
ratesFileLocation="open-exchange-rates.json" />
<!-- some per-field similarity examples -->

View File

@ -91,6 +91,7 @@ public abstract class AbstractZkTestCase extends SolrTestCaseJ4 {
putConfig(zkClient, "stopwords.txt");
putConfig(zkClient, "protwords.txt");
putConfig(zkClient, "currency.xml");
putConfig(zkClient, "open-exchange-rates.json");
putConfig(zkClient, "mapping-ISOLatin1Accent.txt");
putConfig(zkClient, "old_synonyms.txt");
putConfig(zkClient, "synonyms.txt");

View File

@ -23,8 +23,8 @@ import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import java.util.Arrays;
import java.util.Random;
import java.util.Set;
/**
* Tests currency field type.
@ -86,12 +86,26 @@ public class CurrencyFieldTest extends SolrTestCaseJ4 {
// A few tests on the provider directly
ExchangeRateProvider p = ((CurrencyField) tmp).getProvider();
String[] available = p.listAvailableCurrencies();
assert(available.length == 5);
Set<String> availableCurrencies = p.listAvailableCurrencies();
assert(availableCurrencies.size() == 4);
assert(p.reload() == true);
assert(p.getExchangeRate("USD", "EUR") == 2.5);
}
@Test
public void testMockExchangeRateProvider() throws Exception {
SolrCore core = h.getCore();
IndexSchema schema = core.getSchema();
SchemaField amount = schema.getField("mock_amount");
// A few tests on the provider directly
ExchangeRateProvider p = ((CurrencyField)amount.getType()).getProvider();
Set<String> availableCurrencies = p.listAvailableCurrencies();
assert(availableCurrencies.size() == 3);
assert(p.reload() == true);
assert(p.getExchangeRate("USD", "EUR") == 0.8);
}
@Test
public void testCurrencyRangeSearch() throws Exception {
for (int i = 1; i <= 10; i++) {
@ -197,7 +211,7 @@ public class CurrencyFieldTest extends SolrTestCaseJ4 {
}
@Test
public void testMockExchangeRateProvider() throws Exception {
public void testMockFieldType() throws Exception {
assertU(adoc("id", "1", "mock_amount", "1.00,USD"));
assertU(adoc("id", "2", "mock_amount", "1.00,EUR"));
assertU(adoc("id", "3", "mock_amount", "1.00,NOK"));

View File

@ -17,7 +17,9 @@ package org.apache.solr.schema;
*/
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.solr.common.ResourceLoader;
import org.apache.solr.common.SolrException;
@ -53,8 +55,17 @@ public class MockExchangeRateProvider implements ExchangeRateProvider {
}
@Override
public String[] listAvailableCurrencies() {
return map.keySet().toArray(new String[1]);
public Set<String> listAvailableCurrencies() {
Set<String> currenciesPairs = map.keySet();
Set<String> returnSet;
returnSet = new HashSet<String>();
for (String c : currenciesPairs) {
String[] pairs = c.split(",");
returnSet.add(pairs[0]);
returnSet.add(pairs[1]);
}
return returnSet;
}
@Override

View File

@ -0,0 +1,89 @@
package org.apache.solr.schema;
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.ResourceLoader;
import org.apache.solr.common.SolrException;
import org.apache.solr.core.SolrResourceLoader;
import org.junit.Before;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
/**
* Tests currency field type.
*/
public class OpenExchangeRatesOrgProviderTest extends SolrTestCaseJ4 {
OpenExchangeRatesOrgProvider oerp;
ResourceLoader loader;
private final Map<String,String> emptyParams = new HashMap<String,String>();
private Map<String,String> mockParams;
@Before
public void setUp() throws Exception {
super.setUp();
mockParams = new HashMap<String,String>();;
mockParams.put(OpenExchangeRatesOrgProvider.PARAM_RATES_FILE_LOCATION, "open-exchange-rates.json");
oerp = new OpenExchangeRatesOrgProvider();
loader = new SolrResourceLoader("solr");
}
@Test
public void testInit() throws Exception {
oerp.init(emptyParams);
assertTrue("Wrong default url", oerp.ratesFileLocation.toString().equals("http://openexchangerates.org/latest.json"));
assertTrue("Wrong default interval", oerp.refreshInterval == 1440);
Map<String,String> params = new HashMap<String,String>();
params.put(OpenExchangeRatesOrgProvider.PARAM_RATES_FILE_LOCATION, "http://foo.bar/baz");
params.put(OpenExchangeRatesOrgProvider.PARAM_REFRESH_INTERVAL, "100");
oerp.init(params);
assertTrue("Wrong param set url", oerp.ratesFileLocation.equals("http://foo.bar/baz"));
assertTrue("Wrong param interval", oerp.refreshInterval == 100);
}
@Test
public void testList() {
oerp.init(mockParams);
oerp.inform(loader);
assertEquals(159, oerp.listAvailableCurrencies().size());
}
@Test
public void testGetExchangeRate() {
oerp.init(mockParams);
oerp.inform(loader);
assertTrue(5.73163 == oerp.getExchangeRate("USD", "NOK"));
}
@Test
public void testReload() {
oerp.init(mockParams);
oerp.inform(loader);
assertTrue(oerp.reload());
assertEquals("USD", oerp.rates.getBaseCurrency());
assertEquals(new Long(1332070464L), new Long(oerp.rates.getTimestamp()));
}
@Test(expected=SolrException.class)
public void testNoInit() {
oerp.getExchangeRate("ABC", "DEF");
assertTrue("Should have thrown exception if not initialized", false);
}
}

View File

@ -459,12 +459,15 @@
Parameters:
defaultCurrency: Specifies the default currency if none specified. Defaults to "USD"
precisionStep: Specifies the precisionStep for the TrieLong field used for the amount
providerClass: Lets you plug in other exchange backend. Defaults to FileExchangeRateProvider
The FileExchangeRateProvider takes one parameter:
providerClass: Lets you plug in other exchange provider backend:
solr.FileExchangeRateProvider is the default and takes one parameter:
currencyConfig: name of an xml file holding exhange rates
solr.OpenExchangeRatesOrgProvider uses rates from openexchangerates.org:
ratesFileLocation: URL or path to rates JSON file (default latest.json on the web)
refreshInterval: Number of minutes between each rates fetch (default: 1440, min: 60)
-->
<fieldType name="currency" class="solr.CurrencyField" precisionStep="8" defaultCurrency="USD" currencyConfig="currency.xml" />
<!-- some examples for different languages (generally ordered by ISO code) -->
<!-- Arabic -->