/*
* Copyright (c) 2012, 2014, Credit Suisse (Anatole Tresch), Werner Keil.
*
* Licensed 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.
*/
package org.javamoney.currencies.internal.data;
import org.javamoney.currencies.spi.CurrencyUnitNamespaceProviderSpi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import javax.inject.Singleton;
import javax.money.CurrencyContext;
import javax.money.CurrencyContextBuilder;
import javax.money.CurrencyQuery;
import javax.money.CurrencyUnit;
import javax.money.spi.CurrencyProviderSpi;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.InputStream;
import java.net.URL;
import java.util.*;
import java.util.Currency;
import java.util.concurrent.ConcurrentHashMap;
/**
* Online implementation of a {@link CurrencyUnitNamespaceProviderSpi} that provides the
* ISO 4217 currencies available from the JDK {@link Currency} class.
*
* @author Anatole Tresch
* @author Werner Keil
* @see <a href="www.currency-iso.org">Currency Code Services – ISO 4217
* Maintenance Agency</a>
*/
@Singleton
public class IsoCurrencyOnlineProvider implements CurrencyProviderSpi{
private static final Logger LOGGER = LoggerFactory.getLogger(IsoCurrencyOnlineProvider.class);
private static final CurrencyContext CURRENCY_CONTEXT = CurrencyContextBuilder.of("ISO").build();
private static final String PROP_FILE = "/currencyprovider.properties";
private final SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
private Map<String,String> countryCodeMap = new ConcurrentHashMap<>();
private Map<String,CurrencyUnit> currencies = new ConcurrentHashMap<>();
private final Properties prop = new Properties();
public IsoCurrencyOnlineProvider(){
saxParserFactory.setNamespaceAware(false);
saxParserFactory.setValidating(false);
new CurrencyLoader().start();
}
@Override
public String getProviderName(){
return "ISO";
}
@Override
public Set<CurrencyUnit> getCurrencies(CurrencyQuery query){
if(query.getTimestamp() != null){
return Collections.emptySet();
}
Set<CurrencyUnit> currencies = new HashSet<>();
if(!query.getCurrencyCodes().isEmpty()){
for(String code : query.getCurrencyCodes()){
CurrencyUnit cu = this.currencies.get(code);
if(cu != null && !isJDKCurrency(code)){
currencies.add(cu);
}
}
}else{
currencies.addAll(this.currencies.values());
}
return currencies;
}
private boolean isJDKCurrency(String code){
try{
Currency.getInstance(code);
return true;
}
catch(Exception e){
return false;
}
}
public void loadCurrencies(){
try(InputStream in = getClass().getResourceAsStream(PROP_FILE)){
if(in == null){
return;
}
prop.load(in);
final String urlAddress = prop.getProperty(getClass().getSimpleName() + ".currencies");
URL url = new URL(urlAddress);
LOGGER.debug("Loading " + urlAddress);
SAXParser parser = saxParserFactory.newSAXParser();
parser.parse(url.openStream(), new CurrencyHandler());
}
catch(Exception e){
LOGGER.warn("Error", e);
}
}
public void loadCountries(){
try(InputStream in = getClass().getResourceAsStream(PROP_FILE)){
if(in == null){
return;
}
prop.load(in);
final String urlAddress = prop.getProperty(getClass().getSimpleName() + ".countries");
LOGGER.debug("Loading " + urlAddress);
URL url = new URL(urlAddress);
SAXParser parser = saxParserFactory.newSAXParser();
parser.parse(url.openStream(), new CountryHandler());
}
catch(Exception e){
LOGGER.warn("Error", e);
}
}
private final class ISOCurrency implements CurrencyUnit{
private String currencyName;
private String currencyCode;
private int numericCode;
private int minorUnits;
public String getCurrencyCode(){
return currencyCode;
}
public int getNumericCode(){
return numericCode;
}
public int getDefaultFractionDigits(){
return minorUnits;
}
@Override
public CurrencyContext getCurrencyContext(){
return CURRENCY_CONTEXT;
}
@Override
public String toString(){
return this.currencyCode;
}
public String getDisplayName(Locale locale){
// TODO use Locale and add getDisplayName(), too
return currencyName;
}
public int getCashRounding(){
return -1;
}
@Override
public int compareTo(CurrencyUnit o){
return this.getCurrencyCode().compareTo(o.getCurrencyCode());
}
}
private class CountryHandler extends DefaultHandler{
// <ISO_3166-1_List_en xml:lang="en">
// <ISO_3166-1_Entry>
// <ISO_3166-1_Country_name>AFGHANISTAN</ISO_3166-1_Country_name>
// <ISO_3166-1_Alpha-2_Code_element>AF</ISO_3166-1_Alpha-2_Code_element>
// </ISO_3166-1_Entry>
// ...
// </ISO_3166-1_List_en xml:lang="en">
private String name;
private String alpha2Code;
private StringBuilder text = new StringBuilder();
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException{
if("ISO_3166-1_Entry".equals(qName)){
name = null;
alpha2Code = null;
}else if("ISO_3166-1_Country_name".equals(qName)){
text.setLength(0);
}else if("ISO_3166-1_Alpha-2_Code_element".equals(qName)){
text.setLength(0);
}
super.startElement(uri, localName, qName, attributes);
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException{
text.append(ch, start, length);
super.characters(ch, start, length);
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException{
if("ISO_3166-1_Entry".equals(qName)){
countryCodeMap.put(name, alpha2Code);
}else if("ISO_3166-1_Country_name".equals(qName)){
name = text.toString();
}else if("ISO_3166-1_Alpha-2_Code_element".equals(qName)){
alpha2Code = text.toString();
}
super.endElement(uri, localName, qName);
}
}
private class CurrencyHandler extends DefaultHandler{
// <ISO_CCY_CODES>
// <ISO_CURRENCY>
// <ENTITY>AFGHANISTAN</ENTITY>
// <CURRENCY>Afghani</CURRENCY>
// <ALPHABETIC_CODE>AFN</ALPHABETIC_CODE>
// <NUMERIC_CODE>971</NUMERIC_CODE>
// <MINOR_UNIT>2</MINOR_UNIT>
// </ISO_CURRENCY>
// ...
// </ISO_CCY_CODES>
private ISOCurrency currency = null;
private StringBuilder text = new StringBuilder();
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException{
if("ISO_CURRENCY".equals(qName)){
currency = new ISOCurrency();
}else if("ENTITY".equals(qName)){
text.setLength(0);
}else if("CURRENCY".equals(qName)){
text.setLength(0);
}else if("ALPHABETIC_CODE".equals(qName)){
text.setLength(0);
}else if("NUMERIC_CODE".equals(qName)){
text.setLength(0);
}else if("MINOR_UNIT".equals(qName)){
text.setLength(0);
}
super.startElement(uri, localName, qName, attributes);
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException{
text.append(ch, start, length);
super.characters(ch, start, length);
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException{
if("ISO_CURRENCY".equals(qName)){
currencies.put(currency.currencyCode, currency);
}else if("ENTITY".equals(qName)){
String countryName = text.toString();
String code = countryCodeMap.get(countryName);
if(code != null){
new Locale("", code);
}else{
// TODO is this a no-op?
}
}else if("CURRENCY".equals(qName)){
currency.currencyName = text.toString();
}else if("ALPHABETIC_CODE".equals(qName)){
currency.currencyCode = text.toString();
}else if("NUMERIC_CODE".equals(qName)){
String value = text.toString();
if(!value.isEmpty()){
try{
currency.numericCode = Integer.valueOf(value);
}
catch(NumberFormatException nfe){
currency.numericCode = -1;
}
}else{
currency.numericCode = -1;
}
}else if("MINOR_UNIT".equals(qName)){
String value = text.toString();
if(!value.isEmpty()){
try{
currency.minorUnits = Integer.valueOf(value);
}
catch(NumberFormatException nfe){
currency.minorUnits = -1;
}
}else{
currency.minorUnits = -1;
}
}
super.endElement(uri, localName, qName);
}
}
private final class CurrencyLoader extends Thread{
public CurrencyLoader(){
super("ISO Currency Online Loader");
}
public void run(){
loadCountries();
loadCurrencies();
LOGGER.debug("Currencies loaded from ISO:" + IsoCurrencyOnlineProvider.this.currencies.values() +
(IsoCurrencyOnlineProvider.this.countryCodeMap != null ? " for " +
IsoCurrencyOnlineProvider.this.countryCodeMap.size() + " countries" : ""));
}
}
}