diff --git a/millions/src/main/java/no/ntnu/gruppe53/FileHandler.java b/millions/src/main/java/no/ntnu/gruppe53/FileHandler.java index 8133e72..b1c616b 100644 --- a/millions/src/main/java/no/ntnu/gruppe53/FileHandler.java +++ b/millions/src/main/java/no/ntnu/gruppe53/FileHandler.java @@ -4,7 +4,7 @@ import java.io.BufferedWriter; import java.io.IOException; import java.math.BigDecimal; -import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -12,117 +12,127 @@ import java.util.List; /** - * Handles file operations for the application. + * Handles reading and writing of .csv files for stocks. */ - public class FileHandler { /** - * Writes a list of {@link Stock}s to a text-file. Class is structed for {@code .csv} files. + * Writes a list of {@link Stock}s to a CSV file in UTF-8 encoding. * - * @param stockList the list of stocks to be converted to text - * @param path output path for the file - * @param filename name of the file (not including extension) + * @param stockList the list of stocks to be written + * @param path directory path for the file + * @param filename name of the file (without or with .csv extension) + * @throws IllegalArgumentException if stockList, path, or filename is null, or if stockList is empty */ public static void writeStocksToFile(List stockList, String path, String filename) { - String pathString = path + filename; - Path filePath = Paths.get(pathString); - - try (BufferedWriter writer = Files.newBufferedWriter(filePath, Charset.defaultCharset())) { - if (!Files.exists(filePath)) { - Files.createDirectories(filePath); + if (stockList == null) throw new IllegalArgumentException("StockList cannot be null"); + if (path == null || path.isBlank()) throw new IllegalArgumentException("Path cannot be null or blank"); + if (filename == null || filename.isBlank()) throw new IllegalArgumentException( + "Filename cannot be null or blank"); + + + path = path.trim(); + filename = filename.trim(); + String filenameWithExtension = filename.endsWith(".csv") ? filename : filename + ".csv"; + + Path filePath = Paths.get(path, filenameWithExtension); + + Path dirPath = filePath.getParent(); + if (!Files.exists(dirPath)) { + try { + Files.createDirectories(dirPath); + } catch (IOException e) { + System.out.println("ERROR: Could not create directories."); + e.printStackTrace(); + return; } + } + try (BufferedWriter writer = Files.newBufferedWriter(filePath, StandardCharsets.UTF_8)) { writer.write("#Ticker,Name,Price"); writer.newLine(); - writer.newLine(); for (Stock stock : stockList) { - String line = String.join(",", stock.getSymbol(), stock.getCompany(), + if (stock == null) continue; + String line = String.join(",", + stock.getSymbol(), + stock.getCompany(), stock.getSalesPrice().toString()); writer.write(line); writer.newLine(); } } catch (IOException e) { - System.out.println("ERROR: Something went wrong while writing the csv file."); - System.out.println("Message: " + e.getMessage()); + System.out.println("ERROR: Something went wrong while writing the CSV file."); + e.printStackTrace(); } } /** - * Reads a .csv file and converts it to a list of {@code Stock} objects. + * Reads a CSV file and converts it to a list of {@link Stock} objects. * - * @param path path to the file to be read - * @param filename name of the file to be read (including extension) - * @return a list of {@code Stock} objects converted from text + * @param path directory path of the CSV file + * @param filename CSV file name (with or without .csv extension) + * @return list of Stock objects */ public static List readStocksFromFile(String path, String filename) { List stocks = new ArrayList<>(); - String pathString = path + filename; - Path filePath = Paths.get(pathString); + if (path == null || path.isBlank()) { + throw new IllegalArgumentException("Path cannot be null or blank."); + } + if (filename == null || filename.isBlank()) { + throw new IllegalArgumentException("Filename cannot be null or blank."); + } + + path = path.trim(); + filename = filename.trim(); + String filenameWithExtension = filename.endsWith(".csv") ? filename : filename + ".csv"; + + Path filePath = Paths.get(path, filenameWithExtension); + + if (!Files.exists(filePath)) { + System.out.println("ERROR: File not found: " + filePath.toAbsolutePath()); + return stocks; + } - try (BufferedReader reader = Files.newBufferedReader(filePath, Charset.defaultCharset())) { + try (BufferedReader reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8)) { String line; - String symbol; - String name; - BigDecimal price; while ((line = reader.readLine()) != null) { - symbol = null; - name = null; - price = null; - int counter = 0; - String trimValue = line.trim(); + line = line.trim(); - if (trimValue.isBlank() || (line.charAt(0) == '#')) { + if (line.isBlank() || line.startsWith("#")) continue; + + String[] values = line.split(",", -1); + if (values.length != 3) { + System.out.println("ERROR: Bad line in CSV (expected 3 values): " + line); continue; } - String[] values = line.split(","); + String symbol = values[0].trim(); + String name = values[1].trim(); + BigDecimal price; - if (values.length != 3) { - System.out.println("ERROR: Bad line in csv: "); - System.out.print(line); - System.out.println("There should be 3 values total: " + - "symbol, name, price."); + try { + price = new BigDecimal(values[2].trim()); + } catch (NumberFormatException e) { + System.out.println("ERROR: Price is not a valid number: " + values[2]); continue; } - for (String value : values) { - trimValue = value.trim(); - - switch (counter) { - case 0: - symbol = trimValue; - break; - case 1: - name = trimValue; - break; - case 2: - try { - price = new BigDecimal(trimValue); - } - catch (NumberFormatException e) { - System.out.println("ERROR: Not a number. Last column in .csv file " + - "needs to be the price of the stock (number). Skipping..."); - price = null; - break; - } - } - counter++; - } - if (symbol == null || symbol.isBlank() || name == null || name.isBlank() || - price == null || price.compareTo(BigDecimal.ZERO) <= 0) { + if (symbol.isBlank() || name.isBlank() || price.compareTo(BigDecimal.ZERO) <= 0) { + System.out.println("ERROR: Invalid stock data, skipping: " + line); continue; } + stocks.add(new Stock(symbol, name, price)); } + + } catch (IOException e) { + System.out.println("ERROR: Something went wrong while reading the CSV file."); + e.printStackTrace(); } - catch (IOException e) { - System.out.println("ERROR: Something went wrong while reading the csv file."); - System.out.println("Message: " + e.getMessage()); - } + return stocks; } -} +} \ No newline at end of file diff --git a/millions/src/test/java/no/ntnu/gruppe53/FileHandlerTest.java b/millions/src/test/java/no/ntnu/gruppe53/FileHandlerTest.java index 76c4914..8b860b9 100644 --- a/millions/src/test/java/no/ntnu/gruppe53/FileHandlerTest.java +++ b/millions/src/test/java/no/ntnu/gruppe53/FileHandlerTest.java @@ -1,90 +1,237 @@ package no.ntnu.gruppe53; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.io.File; import java.io.IOException; import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.ArrayList; +import java.nio.file.Path; +import java.util.Arrays; import java.util.List; -import static no.ntnu.gruppe53.FileHandler.*; import static org.junit.jupiter.api.Assertions.*; class FileHandlerTest { + + private Path tempDir; + + @BeforeEach + void setup() throws Exception { + tempDir = Files.createTempDirectory("stock_test_"); + } + + @AfterEach + void cleanup() throws Exception { + if (tempDir != null && Files.exists(tempDir)) { + Files.walk(tempDir) + .map(Path::toFile) + .forEach(File::delete); + } + } + + @Test + void writingAndReadingStocksFromListShouldYieldIdenticalStockList() { + Stock stock1 = new Stock("AAPL", "Apple Inc.", new BigDecimal("150.25")); + Stock stock2 = new Stock("MSFT", "Microsoft Corp.", new BigDecimal("300.50")); + List stocks = Arrays.asList(stock1, stock2); + + String filename = "testStocks.csv"; + + FileHandler.writeStocksToFile(stocks, tempDir.toString(), filename); + + List readStocks = FileHandler.readStocksFromFile(tempDir.toString(), filename); + + assertEquals(stocks.size(), readStocks.size(), "Stock list size should match"); + for (int i = 0; i < stocks.size(); i++) { + Stock expected = stocks.get(i); + Stock actual = readStocks.get(i); + assertEquals(expected.getSymbol(), actual.getSymbol()); + assertEquals(expected.getCompany(), actual.getCompany()); + assertEquals(0, expected.getSalesPrice().compareTo(actual.getSalesPrice())); + } + } + @Test - void writeTest() throws IOException { - String filePathString = System.getProperty("user.dir") + "\\target\\test-output\\"; - String fileName = "stockTest.csv"; + void writerShouldIgnoreNullStockObjects() { + Stock stock1 = new Stock("AAPL", "Apple Inc.", new BigDecimal("150.25")); + Stock stock2 = new Stock("MSFT", "Microsoft Corp.", new BigDecimal("300.50")); + List stocks = Arrays.asList(stock1, null, stock2); + + FileHandler.writeStocksToFile(stocks, tempDir.toString(), "stocks.csv"); + List writtenStocks = FileHandler.readStocksFromFile(tempDir.toString(),"stocks.csv"); + assertEquals(3, stocks.size(), "Stock list should include null stock."); + assertEquals(stocks.size()-1, writtenStocks.size(), + "Null stock should be ignored by the writer."); - String symbol1 = "AAPL"; - String company1 = "Apple Inc."; - BigDecimal price1 = new BigDecimal("100"); + } - String symbol2 = "NVDA"; - String company2 = "Nvidia"; - BigDecimal price2 = new BigDecimal("191.27"); + @Test + void writeFileNameWithoutExtensionShouldAddExtension() { - Stock testStock1 = new Stock(symbol1,company1, price1); - Stock testStock2 = new Stock(symbol2, company2, price2); + List stocks = List.of( + new Stock("AAPL", "Apple Inc.", new BigDecimal("150.00")) + ); - List stockList = new ArrayList<>(); - stockList.add(testStock1); - stockList.add(testStock2); + String filenameWithoutExtension = "stocks"; - writeStocksToFile(stockList, filePathString, fileName); + FileHandler.writeStocksToFile(stocks, tempDir.toString(), filenameWithoutExtension); - List lines = Files.readAllLines(Paths.get(filePathString + fileName)); + Path expectedFile = tempDir.resolve("stocks.csv"); - assertTrue(Files.exists(Paths.get(filePathString + fileName)), - "File should have been created successfully."); - assertEquals("#Ticker,Name,Price",lines.getFirst(), "First line " + - "should contain correct headers."); - assertEquals("", lines.get(1), "Second line should be empty."); - assertEquals(symbol1 + "," + company1 + "," + price1, lines.get(2), - "Third line should contain correct values for first stock."); - assertEquals(symbol2 + "," + company2 + "," + price2, lines.getLast(), - "Last line should contain correct values for second stock."); + assertTrue(Files.exists(expectedFile), "File should end with .csv extension."); + } + + @Test + void writeToNonExistingDirectoriesShouldCreateTheDirectories() { + Path nonExistingDir = tempDir.resolve("subdir1/subdir2"); + + List stocks = List.of( + new Stock("AAPL", "Apple Inc.", new BigDecimal("150.00")) + ); + + FileHandler.writeStocksToFile(stocks, nonExistingDir.toString(), "stocks"); + + Path expectedFile = nonExistingDir.resolve("stocks.csv"); + + assertTrue(Files.exists(expectedFile), "File should be created in newly created directories"); + assertTrue(Files.exists(nonExistingDir), "Directories should have been created"); + } + + @Test + void writeEmptyListShouldReturnEmptyList() { + List emptyList = List.of(); + String filename = "emptyStocks.csv"; + + FileHandler.writeStocksToFile(emptyList, tempDir.toString(), filename); + + List result = FileHandler.readStocksFromFile(tempDir.toString(), filename); + assertTrue(result.isEmpty(), "Reading empty stock list should return empty list"); + } + + @Test + void writeInvalidStockListShouldThrowIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> + FileHandler.writeStocksToFile(null, tempDir.toString(), "file.csv")); } @Test - void readTest() { - String filePathString = System.getProperty("user.dir") + "\\target\\test-output\\"; - String fileName = "stockTest.csv"; - - - String symbol1 = "AAPL"; - String company1 = "Apple Inc."; - BigDecimal price1 = new BigDecimal("100"); - - String symbol2 = "NVDA"; - String company2 = "Nvidia"; - BigDecimal price2 = new BigDecimal("191.27"); - - Stock testStock1 = new Stock(symbol1,company1, price1); - Stock testStock2 = new Stock(symbol2, company2, price2); - - List stockList = new ArrayList<>(); - stockList.add(testStock1); - stockList.add(testStock2); - - writeStocksToFile(stockList, filePathString, fileName); - - List readStocks = readStocksFromFile(filePathString, fileName); - - assertEquals(symbol1, readStocks.getFirst().getSymbol(), - "First stock's symbol should be AAPL."); - assertEquals(company1, readStocks.getFirst().getCompany(), - "First stock's company name should be Apple Inc."); - assertEquals(0, price1.compareTo(readStocks.getFirst().getSalesPrice()), - "First stock's price should be 100."); - assertEquals(symbol2, readStocks.getLast().getSymbol(), - "Second stock's symbol should be NVDA."); - assertEquals(company2, readStocks.getLast().getCompany(), - "Second stock's company name should be Nvidia."); - assertEquals(0, price2.compareTo(readStocks.getLast().getSalesPrice()), - "Second stock's price should be 191.27"); + void writeInvalidFolderPathShouldThrowIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> + FileHandler.writeStocksToFile(List.of(new Stock("AAPL", "Apple", BigDecimal.ONE)), " ", "file.csv")); + assertThrows(IllegalArgumentException.class, () -> + FileHandler.writeStocksToFile(List.of(new Stock("AAPL", "Apple", BigDecimal.ONE)),null, "file.csv")); + } + + @Test + void writeInvalidFileNameShouldThrowIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> + FileHandler.writeStocksToFile(List.of(new Stock("AAPL", "Apple", BigDecimal.ONE)), tempDir.toString(), " ")); + assertThrows(IllegalArgumentException.class, () -> + FileHandler.writeStocksToFile(List.of(new Stock("AAPL", "Apple", BigDecimal.ONE)), tempDir.toString(), null)); + } + + @Test + void readStocksShouldIgnoreMalformedLines() throws Exception { + String filename = "malformedStocks.csv"; + Path filePath = tempDir.resolve(filename); + + List lines = List.of( + "# This is a comment", + "AAPL,Apple Inc.,150.25", + "INVALIDLINEWITHOUTCOMMAS", + "MSFT,Microsoft Corp.,300.50" + ); + + Files.write(filePath, lines, StandardCharsets.UTF_8); + + List stocks = FileHandler.readStocksFromFile(tempDir.toString(), filename); + + assertEquals(2, stocks.size(), "Only 2 valid stocks should be read"); + + assertEquals("AAPL", stocks.getFirst().getSymbol()); + assertEquals("Apple Inc.", stocks.getFirst().getCompany()); + assertEquals(0, stocks.getFirst().getSalesPrice().compareTo(new BigDecimal("150.25"))); + + assertEquals("MSFT", stocks.get(1).getSymbol()); + assertEquals("Microsoft Corp.", stocks.get(1).getCompany()); + assertEquals(0, stocks.get(1).getSalesPrice().compareTo(new BigDecimal("300.50"))); + } + + @Test + void readStocksShouldCatchExceptionsFromIllegalStockParameters() throws IOException { + String filename = "malformedStocks.csv"; + Path filePath = tempDir.resolve(filename); + + List lines = List.of( + "GOOG,Google,,", + "GGL, , 200", + " , Google, 100", + "GGL, Google, A", + " ", + "TSLA,Tesla Inc.,-100" + ); + Files.write(filePath, lines, StandardCharsets.UTF_8); + + List stocks = FileHandler.readStocksFromFile(tempDir.toString(), filename); + + assertEquals(0, stocks.size(), "No stocks with illegal parameters should be added."); + } + + @Test + void readStocksShouldThrowIllegalArgumentExceptionIfPathIsNullOrBlank() { + Stock stock1 = new Stock("AAPL", "Apple Inc.", new BigDecimal("150.25")); + Stock stock2 = new Stock("MSFT", "Microsoft Corp.", new BigDecimal("300.50")); + List stocks = Arrays.asList(stock1, stock2); + String filename = "stocks.csv"; + + FileHandler.writeStocksToFile(stocks, tempDir.toString(), filename); + + assertThrows(IllegalArgumentException.class, () -> FileHandler.readStocksFromFile(null, filename), + "Null path should not be allowed."); + assertThrows(IllegalArgumentException.class, () -> FileHandler.readStocksFromFile(" ", filename), + "Blank path should not be allowed."); + } + + @Test + void readStocksShouldThrowIllegalArgumentExceptionIfFileNameIsNullOrBlank() { + Stock stock1 = new Stock("AAPL", "Apple Inc.", new BigDecimal("150.25")); + Stock stock2 = new Stock("MSFT", "Microsoft Corp.", new BigDecimal("300.50")); + List stocks = Arrays.asList(stock1, stock2); + String filename = "stocks.csv"; + + FileHandler.writeStocksToFile(stocks, tempDir.toString(), filename); + + assertThrows(IllegalArgumentException.class, () -> FileHandler.readStocksFromFile( + tempDir.toString(), null), + "Null filename should not be allowed."); + assertThrows(IllegalArgumentException.class, () -> FileHandler.readStocksFromFile( + tempDir.toString(), " "), + "Blank filename should not be allowed."); + } + + @Test + void readShouldAddExtensionIfMissingFromFileName() { + Stock stock1 = new Stock("AAPL", "Apple Inc.", new BigDecimal("150.25")); + Stock stock2 = new Stock("MSFT", "Microsoft Corp.", new BigDecimal("300.50")); + List stocks = Arrays.asList(stock1, stock2); + String filename = "stocks"; + String filenameWithExtension = filename + ".csv"; + + FileHandler.writeStocksToFile(stocks, tempDir.toString(), filenameWithExtension); + List readStocks = FileHandler.readStocksFromFile(tempDir.toString(), filename); + + assertEquals(stocks.size(), readStocks.size(), "All stocks should be read successfully."); + } + + @Test + void readerShouldReturnEmptyListIfFileIsMissing() { + List stocks = FileHandler.readStocksFromFile(tempDir.toString(), "stocklist.csv"); + + assertEquals(0, stocks.size(), "Stock list should be empty."); } } \ No newline at end of file