Saturday, May 9, 2009

CurrencyConverter API

/*
* Copyright (c) 2007 Thomas Knierim
* http://www.thomasknierim.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package etudes;

import java.net.*;
import java.io.*;
import org.xml.sax.*;
import org.xml.sax.helpers.*;
import java.text.*;
import java.util.*;

/**
* CurrencyConverter provides an API for accessing the European Central Bank's
* (ECB) foreign exchange rates. The published ECB rates contain exchange rates
* for approx. 35 of the world's major currencies. They are updated daily at
* 14:15 CET. These rates use EUR as reference currency and are specified with a
* precision of 1/10000 of the currency unit (one hundredth cent). See:
*
* http://www.ecb.int/stats/exchange/eurofxref/html/index.en.html
*
* The convert() method performs currency conversions using either double values
* or 64-bit long integer values. Long values are preferred in order to avoid
* problems associated with floating point arithmetics. A local cache file is
* used for storing exchange rates to reduce network latency. The cache file is
* updated automatically when new exchange rates become available. It is
* created/updated the first time a call to convert() is made.
*
* @version 1.0 2008-16-02
* @author Thomas Knierim
*
*/
public final class CurrencyConverter {

/** singleton instance */
private static CurrencyConverter instance = null;

/** URL for XML file containing EGB's daily exchange rates */
private final String ecbRatesURL = "http://www.ecb.int/stats/eurofxref/eurofxref-daily.xml";

/** local cache file */
transient private File cacheFile = null;

/** name of local cache file */
private String cacheFileName = null;

/** exchange rate collection */
private HashMap fxRates = new HashMap(40);

/** publishing date */
private Date referenceDate = null;

/** internal error message */
private String lastError = null;

/** singleton without subclassing */
private CurrencyConverter() {}

/**
* Returns a singleton instance of CurrencyConverter.
* @return CurrencyConverter instance
*/
public static CurrencyConverter getInstance() {
if (instance == null)
instance = new CurrencyConverter();
return instance;
}

/**
* Converts a double precision floating point value from one currency to
* another. Example: convert(29.95, "USD", "EUR") - converts $29.95 US Dollars
* to Euro.
*
* @param amount
* Amount of money (in source currency) to be converted.
* @param fromCurrency
* Three letter ISO 4217 currency code of source currency.
* @param toCurrency
* Three letter ISO 4217 currency code of target currency.
* @return Amount in target currency
* @throws IOException
* If cache file cannot be read/written or if URL cannot be
* opened.
* @throws ParseException
* If an error occurs while parsing the XML cache file.
* @throws IllegalArgumentException
* If a wrong (non-existing) currency argument was supplied.
*/
public double convert(double amount, String fromCurrency, String toCurrency)
throws IOException, ParseException, IllegalArgumentException {
if (checkCurrencyArgs(fromCurrency, toCurrency)) {
amount *= fxRates.get(toCurrency);
amount /= fxRates.get(fromCurrency);
}
return amount;
}

/**
* Converts a long value from one currency to another. Internally long
* values represent monetary amounts in 1/10000 of the currency unit, e.g.
* the long value 975573l represents 97.5573 (precision = four digits after
* comma). Using long values instead of floating point numbers prevents
* imprecision / calculation errors resulting from floating point
* arithmetics.
*
* @param amount
* Amount of money (in source currency) to be converted.
* @param fromCurrency
* Three letter ISO 4217 currency code of source currency.
* @param toCurrency
* Three letter ISO 4217 currency code of target currency.
* @return Amount in target currency
* @throws IOException
* If cache file cannot be read/written or if URL cannot be
* opened.
* @throws ParseException
* If an error occurs while parsing the XML cache file.
* @throws IllegalArgumentException
* If a wrong (non-existing) currency argument was supplied.
*/
public long convert(long amount, String fromCurrency, String toCurrency)
throws IOException, ParseException, IllegalArgumentException {
if (checkCurrencyArgs(fromCurrency, toCurrency)) {
amount *= fxRates.get(toCurrency);
amount /= fxRates.get(fromCurrency);
}
return amount;
}

/**
* Check whether currency arguments are valid and not equal.
*
* @param fromCurrency
* ISO 4217 source currency code.
* @param toCurrency
* ISO 4217 target currency code.
* @return true if both currency arguments are not equal.
* @throws IOException
* If cache file cannot be read/written or if URL cannot be
* opened.
* @throws ParseException
* If an error occurs while parsing the XML cache file.
* @throws IllegalArgumentException
* If a wrong (non-existing) currency argument was supplied.
*/
private boolean checkCurrencyArgs(String fromCurrency, String toCurrency)
throws IOException, ParseException, IllegalArgumentException {
update();
if (!fxRates.containsKey(fromCurrency))
throw new IllegalArgumentException(fromCurrency
+ " currency is not available.");
if (!fxRates.containsKey(toCurrency))
throw new IllegalArgumentException(toCurrency
+ " currency is not available.");
return (!fromCurrency.equals(toCurrency));
}

/**
* Check whether the exchange rate for a given currency is available.
*
* @param currency
* Three letter ISO 4217 currency code of source currency.
* @return True if exchange rate exists, false otherwise.
*/
public boolean isAvailable(String currency) {
return (fxRates.containsKey(currency));
}

/**
* Returns currencies for which exchange rates are available.
*
* @return String array with ISO 4217 currency codes.
* @throws IOException
* If cache file cannot be read/written or if URL cannot be
* opened.
* @throws ParseException
* If an error occurs while parsing the XML cache file.
*/
public String[] getCurrencies() throws IOException, ParseException {
if (fxRates.isEmpty())
update();
String[] currencies = fxRates.keySet().toArray(
new String[fxRates.size()]);
return currencies;
}

/**
* Get the reference date for the exchange rates as a Java Date. The time
* part is always 14:15 Central European Time (CET).
*
* @return Date for which currency exchange rates are valid, or null if the
* data structure has not yet been initialised.
*
*/
public Date getReferenceDate() {
return referenceDate;
}

/**
* Get the name of the fully qualified path name of the XML cache file. By
* default this is a file named "ExchangeRates.xml" located in the system's
* temporary file directory. The cache file can be shared by multiple
* threads/applications.
*
* @return Path name of the XML cache file.
*/
public String getCacheFileName() {
return cacheFileName;
}

/**
* Set the location where the XML cache file should be stored.
*
* @param cacheFileName
* @see #getCacheFileName() Fully qualified path name of the XML cache file.
*/
public void setCacheFileName(String cacheFileName) {
this.cacheFileName = cacheFileName;
}

/**
* Delete XML cache file and reset internal data structure. Calling
* clearCache() before the convert() method forces a fresh download of the
* currency exchange rates.
*/
public void clearCache() {
initCacheFile();
cacheFile.delete();
cacheFile = null;
referenceDate = null;
}

/**
* Check whether cache is initialised and up-to-date. If not, re-download
* cache file and parse data into internal data structure.
*
* @throws IOException
* If cache file cannot be read/written or if URL cannot be
* opened.
* @throws ParseException
* If an error occurs while parsing the XML cache file.
*/
private void update() throws IOException, ParseException {
if (referenceDate == null) {
initCacheFile();
if (!cacheFile.exists()) {
refreshCacheFile();
}
parse();
}
if (cacheIsExpired()) {
refreshCacheFile();
parse();
}
}

/**
* Initialises cache file member variable if not already initialised.
*/
private void initCacheFile() {
if (cacheFile == null) {
if (cacheFileName == null || cacheFileName.equals(""))
cacheFileName = System.getProperty("java.io.tmpdir")
+ "ExchangeRates.xml";
cacheFile = new File(cacheFileName);
}
}

/**
* Checks whether XML cache file needs to be updated. The cache file is up
* to date for 24 hours after the reference date (plus a certain tolerance).
* On weekends, it is 72 hours because no rates are published during
* weekends.
*
* @return true if cache file needs to be updated, false otherwise.
*/
private boolean cacheIsExpired() {
final int tolerance = 12;
if (referenceDate == null)
return true;
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
long hoursOld = (cal.getTimeInMillis() - referenceDate.getTime())
/ (1000 * 60 * 60);
int hoursValid = 24 + tolerance;
cal.setTime(referenceDate);
if (cal.get(Calendar.DAY_OF_WEEK) == Calendar.FRIDAY)
hoursValid = 72;
else if (cal.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY)
hoursValid = 48; // hypothetical... rates are never published on
// Saturdays
if (hoursOld > hoursValid)
return true;
return false;
}

/**
* (Re-) download the XML cache file and store it in a temporary location.
*
* @throws IOException
* If (1) URL cannot be opened, or (2) if cache file cannot
* be opened, or (3) if a read/write error occurs.
*/
private void refreshCacheFile() throws IOException {
lastError = null;
initCacheFile();
InputStreamReader in;
FileWriter out;
try {
URL ecbRates = new URL(ecbRatesURL);
in = new InputStreamReader(ecbRates.openStream());
out = new FileWriter(cacheFile);
try {
int c;
while ((c = in.read()) != -1)
out.write(c);
} catch (IOException e) {
lastError = "Read/Write Error: " + e.getMessage();
} finally {
out.flush();
out.close();
in.close();
}
} catch (IOException e) {
lastError = "Connection/Open Error: " + e.getMessage();
}
if (lastError != null) {
throw new IOException(lastError);
}
}

/**
* Convert a numeric string to a long value with a precision of four digits
* after the decimal point without rounding. E.g. "123.456789" becomes
* 1234567l.
*
* @param str
* Positive numeric string expression.
* @return Value representing 1/10000th of a currency unit.
* @throws NumberFormatException
* If "str" argument is not numeric.
*/
private long stringToLong(String str) throws NumberFormatException {
int decimalPoint = str.indexOf('.');
String wholePart = "";
String fractionPart = "";
if (decimalPoint > -1) {
if (decimalPoint > 0)
wholePart = str.substring(0, decimalPoint);
fractionPart = str.substring(decimalPoint + 1);
String padString = "0000";
int padLength = 4 - fractionPart.length();
if (padLength > 0)
fractionPart += padString.substring(0, padLength);
else if (padLength < 0)
fractionPart = fractionPart.substring(0, 4);
} else {
wholePart = str;
fractionPart = "0000";
}
return (Long.parseLong(wholePart + fractionPart));
}

/**
* Parse XML cache file and create internal data structures containing
* exchange rates and reference dates.
*
* @throws ParseException
* If XML file cannot be parsed.
*/
private void parse() throws ParseException {
try {
FileReader input = new FileReader(cacheFile);
XMLReader saxReader = XMLReaderFactory.createXMLReader();
DefaultHandler handler = new DefaultHandler() {
public void startElement(String uri, String localName,
String qName, Attributes attributes) {
if (localName.equals("Cube")) {
String date = attributes.getValue("time");
if (date != null) {
SimpleDateFormat df = new SimpleDateFormat(
"yyyy-MM-dd HH:mm z");
try {
referenceDate = df.parse(date + " 14:15 CET");
} catch (ParseException e) {
lastError = "Cannot parse reference date: "
+ date;
}
}
String currency = attributes.getValue("currency");
String rate = attributes.getValue("rate");
if (currency != null && rate != null) {
try {
fxRates.put(currency, stringToLong(rate));
} catch (Exception e) {
lastError = "Cannot parse exchange rate: "
+ rate + ". " + e.getMessage();
}
}
}
}
};
lastError = null;
fxRates.clear();
fxRates.put("EUR", 10000L);
saxReader.setContentHandler(handler);
saxReader.setErrorHandler(handler);
saxReader.parse(new InputSource(input));
input.close();
} catch (Exception e) {
lastError = "Parser error: " + e.getMessage();
}
if (lastError != null) {
throw new ParseException(lastError, 0);
}
}

}

No comments:

Post a Comment

LinkWithin

Related Posts with Thumbnails