diff --git a/src/main/java/com/europeanexchangerates/exchangeapi/exception/NoDataFromSource.java b/src/main/java/com/europeanexchangerates/exchangeapi/exception/NoDataFromSource.java new file mode 100644 index 0000000..0ceca90 --- /dev/null +++ b/src/main/java/com/europeanexchangerates/exchangeapi/exception/NoDataFromSource.java @@ -0,0 +1,7 @@ +package com.europeanexchangerates.exchangeapi.exception; + +public class NoDataFromSource extends RuntimeException { + public NoDataFromSource(String message) { + super(message); + } +} diff --git a/src/main/java/com/europeanexchangerates/exchangeapi/provider/UrlCsvZipExchangeRateProvider.java b/src/main/java/com/europeanexchangerates/exchangeapi/provider/UrlCsvZipExchangeRateProvider.java index c01c442..d0cd5e0 100644 --- a/src/main/java/com/europeanexchangerates/exchangeapi/provider/UrlCsvZipExchangeRateProvider.java +++ b/src/main/java/com/europeanexchangerates/exchangeapi/provider/UrlCsvZipExchangeRateProvider.java @@ -1,24 +1,50 @@ package com.europeanexchangerates.exchangeapi.provider; +import java.io.InputStream; import java.time.LocalDate; import java.util.TreeMap; +import java.util.zip.ZipInputStream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.europeanexchangerates.exchangeapi.dto.ExchangeRate; -import com.europeanexchangerates.exchangeapi.util.DataDownloader; -import com.europeanexchangerates.exchangeapi.util.UrlCsvZipDataDownloader; +import com.europeanexchangerates.exchangeapi.exception.NoDataFromSource; +import com.europeanexchangerates.exchangeapi.util.datadownloader.DataDownloader; +import com.europeanexchangerates.exchangeapi.util.datadownloader.UrlCsvZipDataDownloader; +import com.europeanexchangerates.exchangeapi.util.dataparser.CsvDataParser; +import com.europeanexchangerates.exchangeapi.util.dataparser.DataParser; public class UrlCsvZipExchangeRateProvider implements ExchangeRateProvider { private DataDownloader downloader; + private DataParser parser; + private static final Logger LOGGER = LoggerFactory.getLogger(UrlCsvZipExchangeRateProvider.class); public UrlCsvZipExchangeRateProvider() { this.downloader = new UrlCsvZipDataDownloader(); + this.parser = new CsvDataParser(); } - public UrlCsvZipExchangeRateProvider(DataDownloader downloader) { + public UrlCsvZipExchangeRateProvider(DataDownloader downloader, DataParser parser) { this.downloader = downloader; + this.parser = parser; } public TreeMap getExchangeRates() throws Exception { - return downloader.downloadData(); + InputStream data = downloader.downloadData("https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.zip"); + + // Only one CSV file is expected from the ZIP file. + if (((ZipInputStream) data).getNextEntry() == null) { + throw new NoDataFromSource("No files found from the zip file"); + } + TreeMap exchangeRates = parser.parseData(data); + + // If the zip file has changed, skip the next files but log a warning. + // Ideally, alerts should be sent if running in a production environment. + if (((ZipInputStream) data).getNextEntry() != null) { + LOGGER.warn("The contents of the zip archive has changed. Please check the data source."); + } + + return exchangeRates; } } diff --git a/src/main/java/com/europeanexchangerates/exchangeapi/util/DataDownloader.java b/src/main/java/com/europeanexchangerates/exchangeapi/util/DataDownloader.java deleted file mode 100644 index 7916057..0000000 --- a/src/main/java/com/europeanexchangerates/exchangeapi/util/DataDownloader.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.europeanexchangerates.exchangeapi.util; - -import java.time.LocalDate; -import java.util.TreeMap; - -import com.europeanexchangerates.exchangeapi.dto.ExchangeRate; - -public interface DataDownloader { - public TreeMap downloadData() throws Exception; -} diff --git a/src/main/java/com/europeanexchangerates/exchangeapi/util/UrlCsvZipDataDownloader.java b/src/main/java/com/europeanexchangerates/exchangeapi/util/UrlCsvZipDataDownloader.java deleted file mode 100644 index 613659b..0000000 --- a/src/main/java/com/europeanexchangerates/exchangeapi/util/UrlCsvZipDataDownloader.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.europeanexchangerates.exchangeapi.util; - -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.math.BigDecimal; -import java.net.URL; -import java.time.LocalDate; -import java.util.HashMap; -import java.util.Map; -import java.util.TreeMap; -import java.util.zip.ZipInputStream; - -import com.europeanexchangerates.exchangeapi.dto.ExchangeRate; - -public class UrlCsvZipDataDownloader implements DataDownloader { - public TreeMap downloadData() throws Exception { - String url = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.zip"; - TreeMap exchangeRates = new TreeMap<>(); - ZipInputStream zipInputStream = new ZipInputStream( - (new URL(url)).openStream()); - BufferedReader bufferedReader = new BufferedReader( - new InputStreamReader(zipInputStream)); - - // Iterate over each entry in the zip file - while (zipInputStream.getNextEntry() != null) { - // Skip header line - String[] headers = bufferedReader.readLine().split(","); - String line; - while ((line = bufferedReader.readLine()) != null) { - String[] data = line.split(","); - LocalDate date = LocalDate.parse(data[0]); - - Map rates = new HashMap<>(); - for (int i = 1; i < headers.length; i++) { - if (!data[i].equals("N/A")) { - rates.put(headers[i], new BigDecimal(data[i])); - } - } - exchangeRates.put(date, new ExchangeRate(rates)); - } - } - - return exchangeRates; - } -} diff --git a/src/main/java/com/europeanexchangerates/exchangeapi/util/datadownloader/DataDownloader.java b/src/main/java/com/europeanexchangerates/exchangeapi/util/datadownloader/DataDownloader.java new file mode 100644 index 0000000..06829a8 --- /dev/null +++ b/src/main/java/com/europeanexchangerates/exchangeapi/util/datadownloader/DataDownloader.java @@ -0,0 +1,7 @@ +package com.europeanexchangerates.exchangeapi.util.datadownloader; + +import java.io.InputStream; + +public interface DataDownloader { + public InputStream downloadData(String url) throws Exception; +} diff --git a/src/main/java/com/europeanexchangerates/exchangeapi/util/datadownloader/UrlCsvZipDataDownloader.java b/src/main/java/com/europeanexchangerates/exchangeapi/util/datadownloader/UrlCsvZipDataDownloader.java new file mode 100644 index 0000000..c56cb68 --- /dev/null +++ b/src/main/java/com/europeanexchangerates/exchangeapi/util/datadownloader/UrlCsvZipDataDownloader.java @@ -0,0 +1,13 @@ +package com.europeanexchangerates.exchangeapi.util.datadownloader; + +import java.io.InputStream; +import java.net.URL; +import java.util.zip.ZipInputStream; + +public class UrlCsvZipDataDownloader implements DataDownloader { + public InputStream downloadData(String url) throws Exception { + ZipInputStream zipInputStream = new ZipInputStream( + (new URL(url)).openStream()); + return zipInputStream; + } +} diff --git a/src/main/java/com/europeanexchangerates/exchangeapi/util/dataparser/CsvDataParser.java b/src/main/java/com/europeanexchangerates/exchangeapi/util/dataparser/CsvDataParser.java new file mode 100644 index 0000000..b7c249a --- /dev/null +++ b/src/main/java/com/europeanexchangerates/exchangeapi/util/dataparser/CsvDataParser.java @@ -0,0 +1,39 @@ +package com.europeanexchangerates.exchangeapi.util.dataparser; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +import com.europeanexchangerates.exchangeapi.dto.ExchangeRate; + +public class CsvDataParser implements DataParser { + public TreeMap parseData(InputStream inputStream) throws Exception { + TreeMap exchangeRates = new TreeMap<>(); + BufferedReader bufferedReader = new BufferedReader( + new InputStreamReader(inputStream)); + + // Iterate over each entry in the zip file + // Skip header line + String[] headers = bufferedReader.readLine().split(","); + String line; + while ((line = bufferedReader.readLine()) != null) { + String[] data = line.split(","); + LocalDate date = LocalDate.parse(data[0]); + + Map rates = new HashMap<>(); + for (int i = 1; i < headers.length; i++) { + if (!data[i].equals("N/A")) { + rates.put(headers[i], new BigDecimal(data[i])); + } + } + exchangeRates.put(date, new ExchangeRate(rates)); + } + + return exchangeRates; + } +} diff --git a/src/main/java/com/europeanexchangerates/exchangeapi/util/dataparser/DataParser.java b/src/main/java/com/europeanexchangerates/exchangeapi/util/dataparser/DataParser.java new file mode 100644 index 0000000..1e22048 --- /dev/null +++ b/src/main/java/com/europeanexchangerates/exchangeapi/util/dataparser/DataParser.java @@ -0,0 +1,11 @@ +package com.europeanexchangerates.exchangeapi.util.dataparser; + +import java.io.InputStream; +import java.time.LocalDate; +import java.util.TreeMap; + +import com.europeanexchangerates.exchangeapi.dto.ExchangeRate; + +public interface DataParser { + public TreeMap parseData(InputStream inputStream) throws Exception; +} diff --git a/src/test/java/com/europeanexchangerates/exchangeapi/util/CsvZipDataParserTest.java b/src/test/java/com/europeanexchangerates/exchangeapi/util/CsvZipDataParserTest.java new file mode 100644 index 0000000..4c84c54 --- /dev/null +++ b/src/test/java/com/europeanexchangerates/exchangeapi/util/CsvZipDataParserTest.java @@ -0,0 +1,57 @@ +package com.europeanexchangerates.exchangeapi.util; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import com.europeanexchangerates.exchangeapi.dto.ExchangeRate; +import com.europeanexchangerates.exchangeapi.util.dataparser.CsvDataParser; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Map; +import java.util.TreeMap; + +import static org.junit.jupiter.api.Assertions.*; + +class CsvZipDataParserTest { + + @Mock + InputStream inputStream; + + private CsvDataParser parser; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + parser = new CsvDataParser(); + } + + @Test + void parseData() throws Exception { + String testInput = "Date,USD,EUR\n" + + "2023-06-04,1.2100,0.8400\n" + + "2023-06-03,1.2200,0.8500\n"; + InputStream inputStream = new ByteArrayInputStream(testInput.getBytes()); + + TreeMap result = parser.parseData(inputStream); + + assertEquals(new BigDecimal("1.2100"), result.get(LocalDate.of(2023, 6, 4)).getRates().get("USD")); + assertEquals(new BigDecimal("0.8400"), result.get(LocalDate.of(2023, 6, 4)).getRates().get("EUR")); + assertEquals(new BigDecimal("1.2200"), result.get(LocalDate.of(2023, 6, 3)).getRates().get("USD")); + assertEquals(new BigDecimal("0.8500"), result.get(LocalDate.of(2023, 6, 3)).getRates().get("EUR")); + } + + @Test + void parseData_throwsExceptionForMalformedData() { + String testInput = "Date,USD,EUR\n" + + "2023-06-04,1.2100,\n" + + "2023-06-03,1.2200,0.8500\n"; + InputStream inputStream = new ByteArrayInputStream(testInput.getBytes()); + + assertThrows(Exception.class, () -> parser.parseData(inputStream)); + } +}