google-site-verification: google61fe8ba583a51912.html
Currency Conversion with ECB Exchange Rates in Java

Building a currency conversion system using European Central Bank (ECB) exchange rates involves fetching real-time or historical forex data, parsing XML responses, and performing accurate currency calculations.


Understanding ECB Exchange Rate Data

The ECB provides daily reference exchange rates in XML format:

  • Base currency: EUR (Euro)
  • Update frequency: Daily around 16:00 CET
  • Format: XML with cube structure
  • URL: https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml

1. Basic Currency Conversion Setup

Domain Models

import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.util.*; public class CurrencyModel { // Currency pair representation public record CurrencyPair(String base, String target) { public static CurrencyPair of(String base, String target) { return new CurrencyPair(base.toUpperCase(), target.toUpperCase()); } @Override public String toString() { return base + "/" + target; } } // Exchange rate record public record ExchangeRate(String currency, BigDecimal rate, LocalDate date) { public ExchangeRate { Objects.requireNonNull(currency, "Currency cannot be null"); Objects.requireNonNull(rate, "Rate cannot be null"); Objects.requireNonNull(date, "Date cannot be null"); } @Override public String toString() { return String.format("%s: %.4f (%s)", currency, rate, date); } } // Exchange rate response public static class ExchangeRateResponse { private final LocalDate date; private final Map<String, ExchangeRate> rates; public ExchangeRateResponse(LocalDate date, Map<String, ExchangeRate> rates) { this.date = date; this.rates = new HashMap<>(rates); } public LocalDate getDate() { return date; } public Map<String, ExchangeRate> getRates() { return Collections.unmodifiableMap(rates); } public Optional<ExchangeRate> getRate(String currency) { return Optional.ofNullable(rates.get(currency.toUpperCase())); } public boolean containsCurrency(String currency) { return rates.containsKey(currency.toUpperCase()); } } }

2. ECB XML Parser Implementation

import org.w3c.dom.*; import javax.xml.parsers.*; import java.io.*; import java.math.BigDecimal; import java.net.HttpURLConnection; import java.net.URL; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.*; public class ECBRateParser { private static final String ECB_DAILY_RATES_URL = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml"; private static final String ECB_HISTORICAL_RATES_URL = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.xml"; private final DocumentBuilder documentBuilder; public ECBRateParser() { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); // Security: disable external entity processing factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); factory.setFeature("http://xml.org/sax/features/external-general-entities", false); factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); factory.setXIncludeAware(false); factory.setExpandEntityReferences(false); this.documentBuilder = factory.newDocumentBuilder(); } catch (Exception e) { throw new RuntimeException("Failed to create XML parser", e); } } public CurrencyModel.ExchangeRateResponse fetchDailyRates() throws RateFetchException { try { String xmlContent = fetchXmlContent(ECB_DAILY_RATES_URL); return parseRatesFromXml(xmlContent); } catch (Exception e) { throw new RateFetchException("Failed to fetch daily rates", e); } } public Map<LocalDate, CurrencyModel.ExchangeRateResponse> fetchHistoricalRates() throws RateFetchException { try { String xmlContent = fetchXmlContent(ECB_HISTORICAL_RATES_URL); return parseHistoricalRatesFromXml(xmlContent); } catch (Exception e) { throw new RateFetchException("Failed to fetch historical rates", e); } } private String fetchXmlContent(String urlString) throws IOException { HttpURLConnection connection = null; try { URL url = new URL(urlString); connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); connection.setConnectTimeout(10000); connection.setReadTimeout(10000); connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Java Currency Converter)"); int responseCode = connection.getResponseCode(); if (responseCode != 200) { throw new IOException("HTTP error code: " + responseCode); } try (BufferedReader reader = new BufferedReader( new InputStreamReader(connection.getInputStream()))) { StringBuilder content = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { content.append(line); } return content.toString(); } } finally { if (connection != null) { connection.disconnect(); } } } private CurrencyModel.ExchangeRateResponse parseRatesFromXml(String xmlContent) throws Exception { Document document = documentBuilder.parse(new ByteArrayInputStream(xmlContent.getBytes())); document.getDocumentElement().normalize(); // Find the latest time attribute NodeList cubeNodes = document.getElementsByTagName("Cube"); LocalDate rateDate = null; Map<String, CurrencyModel.ExchangeRate> rates = new HashMap<>(); for (int i = 0; i < cubeNodes.getLength(); i++) { Node node = cubeNodes.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { Element element = (Element) node; // Extract date from time attribute if (element.hasAttribute("time")) { String timeStr = element.getAttribute("time"); rateDate = LocalDate.parse(timeStr, DateTimeFormatter.ISO_LOCAL_DATE); } // Extract currency rates if (element.hasAttribute("currency") && element.hasAttribute("rate")) { String currency = element.getAttribute("currency"); BigDecimal rate = new BigDecimal(element.getAttribute("rate")); rates.put(currency, new CurrencyModel.ExchangeRate(currency, rate, rateDate)); } } } if (rateDate == null || rates.isEmpty()) { throw new RateFetchException("No valid rate data found in XML"); } return new CurrencyModel.ExchangeRateResponse(rateDate, rates); } private Map<LocalDate, CurrencyModel.ExchangeRateResponse> parseHistoricalRatesFromXml(String xmlContent) throws Exception { Document document = documentBuilder.parse(new ByteArrayInputStream(xmlContent.getBytes())); document.getDocumentElement().normalize(); Map<LocalDate, CurrencyModel.ExchangeRateResponse> historicalRates = new HashMap<>(); LocalDate currentDate = null; Map<String, CurrencyModel.ExchangeRate> currentRates = new HashMap<>(); NodeList cubeNodes = document.getElementsByTagName("Cube"); for (int i = 0; i < cubeNodes.getLength(); i++) { Node node = cubeNodes.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { Element element = (Element) node; if (element.hasAttribute("time")) { // Save previous day's rates if we have them if (currentDate != null && !currentRates.isEmpty()) { historicalRates.put(currentDate, new CurrencyModel.ExchangeRateResponse(currentDate, currentRates)); } // Start new day String timeStr = element.getAttribute("time"); currentDate = LocalDate.parse(timeStr, DateTimeFormatter.ISO_LOCAL_DATE); currentRates = new HashMap<>(); } if (element.hasAttribute("currency") && element.hasAttribute("rate") && currentDate != null) { String currency = element.getAttribute("currency"); BigDecimal rate = new BigDecimal(element.getAttribute("rate")); currentRates.put(currency, new CurrencyModel.ExchangeRate(currency, rate, currentDate)); } } } // Don't forget the last day if (currentDate != null && !currentRates.isEmpty()) { historicalRates.put(currentDate, new CurrencyModel.ExchangeRateResponse(currentDate, currentRates)); } return historicalRates; } public static class RateFetchException extends Exception { public RateFetchException(String message) { super(message); } public RateFetchException(String message, Throwable cause) { super(message, cause); } } }

3. Currency Conversion Engine

import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.util.*; public class CurrencyConverter { private final ECBRateParser rateParser; private CurrencyModel.ExchangeRateResponse currentRates; private final Map<LocalDate, CurrencyModel.ExchangeRateResponse> historicalRates; private final Set<String> supportedCurrencies; public CurrencyConverter() { this.rateParser = new ECBRateParser(); this.historicalRates = new HashMap<>(); this.supportedCurrencies = new HashSet<>(); initializeSupportedCurrencies(); } private void initializeSupportedCurrencies() { // ECB always uses EUR as base, plus all quoted currencies supportedCurrencies.add("EUR"); supportedCurrencies.addAll(Arrays.asList( "USD", "JPY", "BGN", "CZK", "DKK", "GBP", "HUF", "PLN", "RON", "SEK", "CHF", "ISK", "NOK", "HRK", "RUB", "TRY", "AUD", "BRL", "CAD", "CNY", "HKD", "IDR", "ILS", "INR", "KRW", "MXN", "MYR", "NZD", "PHP", "SGD", "THB", "ZAR" )); } public void refreshRates() throws ECBRateParser.RateFetchException { this.currentRates = rateParser.fetchDailyRates(); // Add EUR to EUR rate (1:1) currentRates.getRates().put("EUR", new CurrencyModel.ExchangeRate("EUR", BigDecimal.ONE, currentRates.getDate())); } public void loadHistoricalData() throws ECBRateParser.RateFetchException { historicalRates.clear(); historicalRates.putAll(rateParser.fetchHistoricalRates()); } public BigDecimal convert(BigDecimal amount, String fromCurrency, String toCurrency) throws ConversionException { return convert(amount, fromCurrency, toCurrency, LocalDate.now()); } public BigDecimal convert(BigDecimal amount, String fromCurrency, String toCurrency, LocalDate date) throws ConversionException { validateInput(amount, fromCurrency, toCurrency); if (fromCurrency.equalsIgnoreCase(toCurrency)) { return amount.setScale(4, RoundingMode.HALF_UP); } CurrencyModel.ExchangeRateResponse rates = getRatesForDate(date); return performConversion(amount, fromCurrency, toCurrency, rates); } private void validateInput(BigDecimal amount, String fromCurrency, String toCurrency) throws ConversionException { if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) { throw new ConversionException("Amount must be non-negative"); } if (!isCurrencySupported(fromCurrency)) { throw new ConversionException("Unsupported source currency: " + fromCurrency); } if (!isCurrencySupported(toCurrency)) { throw new ConversionException("Unsupported target currency: " + toCurrency); } } private CurrencyModel.ExchangeRateResponse getRatesForDate(LocalDate date) throws ConversionException { if (date.isEqual(LocalDate.now()) && currentRates != null) { return currentRates; } if (historicalRates.containsKey(date)) { return historicalRates.get(date); } throw new ConversionException("No exchange rate data available for date: " + date); } private BigDecimal performConversion(BigDecimal amount, String fromCurrency, String toCurrency, CurrencyModel.ExchangeRateResponse rates) throws ConversionException { try { // Since ECB uses EUR as base, we need to convert through EUR BigDecimal fromRate = getRateToEUR(fromCurrency, rates); BigDecimal toRate = getRateToEUR(toCurrency, rates); // Convert: fromCurrency → EUR → toCurrency BigDecimal amountInEUR = fromCurrency.equalsIgnoreCase("EUR") ? amount : amount.divide(fromRate, 10, RoundingMode.HALF_UP); BigDecimal result = toCurrency.equalsIgnoreCase("EUR") ? amountInEUR : amountInEUR.multiply(toRate); return result.setScale(4, RoundingMode.HALF_UP); } catch (ArithmeticException e) { throw new ConversionException("Conversion calculation error", e); } } private BigDecimal getRateToEUR(String currency, CurrencyModel.ExchangeRateResponse rates) throws ConversionException { if (currency.equalsIgnoreCase("EUR")) { return BigDecimal.ONE; } CurrencyModel.ExchangeRate rate = rates.getRate(currency) .orElseThrow(() -> new ConversionException("Exchange rate not found for currency: " + currency)); return rate.rate(); } public boolean isCurrencySupported(String currency) { return supportedCurrencies.contains(currency.toUpperCase()); } public Set<String> getSupportedCurrencies() { return Collections.unmodifiableSet(supportedCurrencies); } public LocalDate getLatestRateDate() { return currentRates != null ? currentRates.getDate() : null; } public Map<String, BigDecimal> getAllRates() { if (currentRates == null) { return Collections.emptyMap(); } Map<String, BigDecimal> rates = new HashMap<>(); currentRates.getRates().forEach((currency, rate) -> { rates.put(currency, rate.rate()); }); return rates; } public static class ConversionException extends Exception { public ConversionException(String message) { super(message); } public ConversionException(String message, Throwable cause) { super(message, cause); } } }

4. Advanced Features and Caching

import java.math.BigDecimal; import java.time.LocalDate; import java.util.*; import java.util.concurrent.*; public class AdvancedCurrencyConverter { private final CurrencyConverter baseConverter; private final Map<LocalDate, CurrencyModel.ExchangeRateResponse> rateCache; private final ScheduledExecutorService scheduler; private final long cacheDurationMinutes; public AdvancedCurrencyConverter() { this.baseConverter = new CurrencyConverter(); this.rateCache = new ConcurrentHashMap<>(); this.scheduler = Executors.newScheduledThreadPool(1); this.cacheDurationMinutes = 60; // Refresh every hour startScheduledRefresh(); } private void startScheduledRefresh() { scheduler.scheduleAtFixedRate(this::refreshCache, 0, cacheDurationMinutes, TimeUnit.MINUTES); } private void refreshCache() { try { baseConverter.refreshRates(); rateCache.clear(); System.out.println("Exchange rates refreshed at: " + new Date()); } catch (Exception e) { System.err.println("Failed to refresh exchange rates: " + e.getMessage()); } } public BigDecimal convertWithFee(BigDecimal amount, String fromCurrency, String toCurrency, BigDecimal feePercent) throws CurrencyConverter.ConversionException { BigDecimal convertedAmount = baseConverter.convert(amount, fromCurrency, toCurrency); BigDecimal fee = convertedAmount.multiply(feePercent).divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP); return convertedAmount.subtract(fee); } public Map<String, BigDecimal> convertToMultiple(BigDecimal amount, String fromCurrency, Set<String> toCurrencies) throws CurrencyConverter.ConversionException { Map<String, BigDecimal> results = new HashMap<>(); for (String toCurrency : toCurrencies) { try { BigDecimal converted = baseConverter.convert(amount, fromCurrency, toCurrency); results.put(toCurrency, converted); } catch (CurrencyConverter.ConversionException e) { System.err.println("Failed to convert to " + toCurrency + ": " + e.getMessage()); } } return results; } public BigDecimal calculatePercentageChange(String currency, LocalDate startDate, LocalDate endDate) throws CurrencyConverter.ConversionException { BigDecimal startRate = getHistoricalRate(currency, startDate); BigDecimal endRate = getHistoricalRate(currency, endDate); return endRate.subtract(startRate) .divide(startRate, 4, RoundingMode.HALF_UP) .multiply(BigDecimal.valueOf(100)); } private BigDecimal getHistoricalRate(String currency, LocalDate date) throws CurrencyConverter.ConversionException { if (!rateCache.containsKey(date)) { // Load historical data if not in cache try { baseConverter.loadHistoricalData(); } catch (ECBRateParser.RateFetchException e) { throw new CurrencyConverter.ConversionException("Failed to load historical data", e); } } CurrencyModel.ExchangeRateResponse rates = rateCache.get(date); if (rates == null) { throw new CurrencyConverter.ConversionException("No rates available for date: " + date); } return rates.getRate(currency) .orElseThrow(() -> new CurrencyConverter.ConversionException("Rate not found for " + currency)) .rate(); } public void shutdown() { scheduler.shutdown(); try { if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { scheduler.shutdownNow(); } } catch (InterruptedException e) { scheduler.shutdownNow(); Thread.currentThread().interrupt(); } } }

5. Usage Examples and Testing

import java.math.BigDecimal; import java.time.LocalDate; import java.util.Map; import java.util.Set; public class CurrencyConverterDemo { public static void main(String[] args) { try { AdvancedCurrencyConverter converter = new AdvancedCurrencyConverter(); // Basic conversion demoBasicConversion(converter); // Multiple currency conversion demoMultipleConversion(converter); // Historical analysis demoHistoricalAnalysis(converter); // With fees demoConversionWithFees(converter); converter.shutdown(); } catch (Exception e) { System.err.println("Demo failed: " + e.getMessage()); e.printStackTrace(); } } private static void demoBasicConversion(AdvancedCurrencyConverter converter) { try { System.out.println("=== Basic Currency Conversion ==="); BigDecimal amount = new BigDecimal("100.00"); BigDecimal converted = converter.convertWithFee(amount, "USD", "EUR", BigDecimal.ZERO); System.out.printf("%s USD = %s EUR%n", amount, converted); System.out.println("Latest rate date: " + converter.getLatestRateDate()); } catch (CurrencyConverter.ConversionException e) { System.err.println("Conversion error: " + e.getMessage()); } } private static void demoMultipleConversion(AdvancedCurrencyConverter converter) { try { System.out.println("\n=== Multiple Currency Conversion ==="); BigDecimal amount = new BigDecimal("1000.00"); Set<String> targetCurrencies = Set.of("GBP", "JPY", "CAD", "AUD", "CHF"); Map<String, BigDecimal> results = converter.convertToMultiple(amount, "EUR", targetCurrencies); System.out.printf("%s EUR converts to:%n", amount); results.forEach((currency, value) -> System.out.printf(" %s: %s%n", currency, value)); } catch (CurrencyConverter.ConversionException e) { System.err.println("Multiple conversion error: " + e.getMessage()); } } private static void demoHistoricalAnalysis(AdvancedCurrencyConverter converter) { try { System.out.println("\n=== Historical Analysis ==="); LocalDate startDate = LocalDate.now().minusMonths(1); LocalDate endDate = LocalDate.now(); BigDecimal change = converter.calculatePercentageChange("USD", startDate, endDate); System.out.printf("USD/EUR change from %s to %s: %.2f%%%n", startDate, endDate, change); } catch (CurrencyConverter.ConversionException e) { System.err.println("Historical analysis error: " + e.getMessage()); } } private static void demoConversionWithFees(AdvancedCurrencyConverter converter) { try { System.out.println("\n=== Conversion with Fees ==="); BigDecimal amount = new BigDecimal("500.00"); BigDecimal feePercent = new BigDecimal("1.5"); // 1.5% fee BigDecimal withoutFee = converter.convertWithFee(amount, "GBP", "USD", BigDecimal.ZERO); BigDecimal withFee = converter.convertWithFee(amount, "GBP", "USD", feePercent); System.out.printf("%s GBP = %s USD (without fee)%n", amount, withoutFee); System.out.printf("%s GBP = %s USD (with %.2f%% fee)%n", amount, withFee, feePercent); System.out.printf("Fee amount: %s USD%n", withoutFee.subtract(withFee)); } catch (CurrencyConverter.ConversionException e) { System.err.println("Fee conversion error: " + e.getMessage()); } } }

6. Error Handling and Validation

import java.math.BigDecimal; import java.util.regex.Pattern; public class CurrencyValidation { private static final Pattern CURRENCY_PATTERN = Pattern.compile("[A-Z]{3}"); private static final Set<String> VALID_CURRENCIES = Set.of( "EUR", "USD", "JPY", "GBP", "CHF", "CAD", "AUD", "CNY", "HKD", "NZD" ); public static void validateCurrency(String currency) throws ValidationException { if (currency == null || currency.trim().isEmpty()) { throw new ValidationException("Currency code cannot be null or empty"); } if (!CURRENCY_PATTERN.matcher(currency).matches()) { throw new ValidationException("Invalid currency code format: " + currency); } if (!VALID_CURRENCIES.contains(currency.toUpperCase())) { throw new ValidationException("Unsupported currency: " + currency); } } public static void validateAmount(BigDecimal amount) throws ValidationException { if (amount == null) { throw new ValidationException("Amount cannot be null"); } if (amount.compareTo(BigDecimal.ZERO) < 0) { throw new ValidationException("Amount cannot be negative: " + amount); } if (amount.scale() > 4) { throw new ValidationException("Amount has too many decimal places: " + amount); } } public static void validateDateRange(LocalDate start, LocalDate end) throws ValidationException { if (start == null || end == null) { throw new ValidationException("Dates cannot be null"); } if (start.isAfter(end)) { throw new ValidationException("Start date cannot be after end date"); } if (start.isAfter(LocalDate.now())) { throw new ValidationException("Start date cannot be in the future"); } } public static class ValidationException extends Exception { public ValidationException(String message) { super(message); } } }

7. Maven Dependencies

<dependencies> <!-- For XML parsing --> <dependency> <groupId>xerces</groupId> <artifactId>xercesImpl</artifactId> <version>2.12.2</version> </dependency> <!-- For HTTP client (alternative to HttpURLConnection) --> <dependency> <groupId>org.apache.httpcomponents.client5</groupId> <artifactId>httpclient5</artifactId> <version>5.1.3</version> </dependency> <!-- For JSON support (if needed) --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.14.2</version> </dependency> <!-- For testing --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.9.2</version> <scope>test</scope> </dependency> </dependencies>

Key Features Summary

  1. Real-time ECB Rates: Fetches daily exchange rates from ECB
  2. Historical Data: Supports historical rate analysis
  3. Accurate Calculations: Uses BigDecimal for precise financial calculations
  4. Caching: Implements caching for performance
  5. Error Handling: Comprehensive exception handling
  6. Thread Safety: Designed for concurrent usage
  7. Flexible API: Supports single and multiple currency conversions
  8. Fee Calculation: Built-in support for conversion fees
  9. Validation: Input validation and sanitization
  10. Scheduled Updates: Automatic rate refresh

This implementation provides a robust foundation for currency conversion applications that require accurate, up-to-date exchange rates from the European Central Bank.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper