Storage Layer: Save the data into files

Introduction

Last time, we introduced the concept of handling errors using Exceptions. Another improvement to our application will be data storage. In the next chapter, we will use a database for this, but for now let’s try to save our suppliers in a text file. Since the application is already structured into specialized layers, we only need to work with the storage layer.

Lemonade stand file storage layer

What do we create?

In addition to our existing application, we should add some different functionalities:

  • All the suppliers will be loaded from a .csv file (comma separated file)
  • When we save, update or delete a supplier, the file will be updated accordingly

The file will look like this:

				
					1,Fresh lemons,freshlemons@email.com
2,Sweet Sugar,sweetsugar@email.com
3,Cold Ice,coldice@email.com

				
			

On the first column, we have the ID (“1”), then on the following columns are rest of the supplier’s attributes: name (“Fresh lemons”) and email (“freshlemons@email.com”).

How do we create it?

We mentioned that we need to modify only the storage layer. The rest of our application will remain unchanged.

To achieve this, we need to extend our existing memory Repository by adding two new features: reading from a file and writing to a file. We can do this by creating a new Repository Class that will inherit all the behavior of the previous one. In addition, it will implement the new methods: saveToFile and writeToFile. The newly created SupplierFileRepository will have an attribute: filename.

				
					public class SupplierFileRepository extends SupplierRepository {

    private String filename;

    ...
}
				
			

Keeping this in mind, we need to create a method which reads all the data from the .csv file:

				
					    public List<Supplier> readSuppliersFromFile() {
        List<Supplier> suppliers = new ArrayList<>();
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new FileReader(filename));
            String line;
            while ((line = reader.readLine()) != null) {
                String[] elems = line.split(",");

                int id = Integer.parseInt(elems[0]);
                String name = elems[1];
                String contactEmail = elems[2];

                Supplier supplier = new Supplier(id, name, contactEmail);
                suppliers.add(supplier);
            }

            reader.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        return suppliers;
    }
				
			

When an instance of SupplierFileRepository is created, all the data from the file should be loaded into memory:

				
					    public SupplierFileRepository(String filename) {
        super();
        this.filename = filename;
        loadSuppliersFromFile();
    }
    
    
private void loadSuppliersFromFile() throws IDNotUniqueException {
        List<Supplier> suppliers = readSuppliersFromFile();
        for(Supplier supplier: suppliers){
            this.save(supplier);
    }

				
			

The next step is to create a method to write suppliers in the file:

				
					private void writeToFile() {
        BufferedWriter writer = null;
        try {
            writer = new BufferedWriter(new FileWriter(filename));
            Iterable<Supplier> suppliers = findAll();
            
            for (Supplier supplier : suppliers) {
                String line = supplier.getId() + "," + supplier.getName() + "," + supplier.getContactEmail();
                writer.write(line);
                writer.newLine();
            }
            
            writer.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
				
			

The final step is to Override the saving method so that the supplier is saved to the file after being added to our list of suppliers. We need to apply on a similar approach to all the methods.

				
					   @Override
    public Supplier save(Supplier supplier) throws IDNotUniqueException {
        Supplier savedSupplier = super.save(supplier);
        writeToFile();
        return savedSupplier;
    }
				
			

The final class will look like this:

				
					public class SupplierFileRepository extends SupplierRepository {

    private String filename;

    public SupplierFileRepository(String filename) throws IDNotUniqueException {
        super();
        this.filename = filename;
        loadSuppliersFromFile();
    }

    @Override
    public Supplier save(Supplier supplier) throws IDNotUniqueException {
        Supplier savedSupplier = super.save(supplier);
        writeToFile();
        return savedSupplier;
    }


    @Override
    public Supplier update(Supplier supplier) {
        Supplier updatedSupplier = super.update(supplier);
        writeToFile();
        return updatedSupplier;
    }

    @Override
    public void delete(int supplierId) {
        super.delete(supplierId);
        writeToFile();
    }

    public List<Supplier> readSuppliersFromFile() {
        List<Supplier> suppliers = new ArrayList<>();
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new FileReader(filename));
            String line;
            while ((line = reader.readLine()) != null) {
                String[] elems = line.split(",");

                int id = Integer.parseInt(elems[0]);
                String name = elems[1];
                String contactEmail = elems[2];

                Supplier supplier = new Supplier(id, name, contactEmail);
                suppliers.add(supplier);
            }

            reader.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        return suppliers;
    }

    private void loadSuppliersFromFile() throws IDNotUniqueException {
        List<Supplier> suppliers = readSuppliersFromFile();
        for(Supplier supplier: suppliers){
            this.save(supplier);
        }
    }

    private void writeToFile() {
        BufferedWriter writer = null;
        try {
            writer = new BufferedWriter(new FileWriter(filename));
            Iterable<Supplier> suppliers = findAll();

            for (Supplier supplier : suppliers) {
                String line = supplier.getId() + "," + supplier.getName() + "," + supplier.getContactEmail();
                writer.write(line);
                writer.newLine();
            }

            writer.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
				
			

Let’s create some tests with our newly created class:

				
					public class SupplierFileRepositoryTest {

    private String filename;

    public SupplierFileRepositoryTest(String filename) {
        this.filename = filename;
    }

    private void clearFile() {
        try (FileOutputStream fos = new FileOutputStream(filename)) {
            // the file is emptied
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void shouldSaveToFileOneElement_whenSaveIsCalled() throws IDNotUniqueException {
        clearFile();
        SupplierFileRepository fileRepository = new SupplierFileRepository(filename);
        Supplier firstSupplierToSave = new Supplier(1, "Lemonades", "contact@lemonades.com");

        Supplier firstSavedSupplier = fileRepository.save(firstSupplierToSave);

        List<Supplier> suppliersFromFile = fileRepository.readSuppliersFromFile();

        assert suppliersFromFile.size() == 1;
        assert fileRepository.findById(1) != null;

        clearFile();
    }

    public void testAllSupplierFileRepository() throws IDNotUniqueException {
        shouldSaveToFileOneElement_whenSaveIsCalled();
    }
}
				
			
				
					public class MainTest {

    public void runAllTest() {
        try {
            SupplierTest domainTests = new SupplierTest();
            domainTests.testAllSupplier();

            SupplierRepositoryTest repositoryTests = new SupplierRepositoryTest();
            repositoryTests.testAllSupplierRepository();

            SupplierFileRepositoryTest repositoryFileTests = new SupplierFileRepositoryTest("test-file.csv");
            repositoryFileTests.testAllSupplierFileRepository();

            SupplierServiceTest serviceTests = new SupplierServiceTest();
            serviceTests.testAllSupplierService();
            System.out.println("All tests have ran successful!");
        } catch (ValidationException | IDNotUniqueException e) {
            System.out.println("The tests have failed, e=" + e.getMessage());
        }

    }
}

				
			

Finally we should add our class to the main program:

				
					public class Main {
    public static void main(String[] args) throws ValidationException, IDNotUniqueException {
        SupplierFileRepository supplierRepository = new SupplierFileRepository("suppliers.csv");
        SupplierValidator supplierValidator = new SupplierValidator();
        SupplierService supplierService = new SupplierService(supplierRepository, supplierValidator);
        UserInterface userInterface = new UserInterface(supplierService);

        MainTest mainTest = new MainTest();
        mainTest.runAllTest();
        userInterface.runMenu();
    }
}
				
			

The file “suppliers.csv” should be placed in the root of our program, at the same level as the Main class. Below is the new structure of our project:

java project structure with file storage

Key points

  • having good storage in a Java application is important because it ensures efficient data management, faster retrieval, scalability, and reliability, all of which directly impact the application’s performance and user experience
  • an application can store data in multiple ways: in-memory storage (using variables or data structures like arrays and collections), file storage (reading/writing to files), and database storage
  • since we have specialized layers, we modified only the storage layer while the rest of the application remained unchanged

Time to practice

We want to change the representation of the data in our files by adding a header and using “|” as the separator. An example is provided below.

Where do we need to make changes in our application?

				
					id|name|email
1|Fresh lemons|freshlemons@email.com
2|Sweet Sugar|sweetsugar@email.com
3|Cold Ice|coldice@email.com
				
			

Conclusion

In this tutorial, we enhanced our application by implementing file storage. We learned that by using layers, we can modify only the necessary part, ensuring that the rest of the application remains intact. In our next tutorial, we will discover how to handle the second entity in our given problem.