javaintellij-ideajavafxjaropencsv

JavaFX Todo Application: How to Make CSV File Accessible in Exported JAR and EXE Files?


I've developed a JavaFX Todo application that reads and writes tasks to a CSV file. The application works perfectly in my development environment (IntelliJ). However, I'm running into issues when exporting the application as a JAR file and eventually converting it into an EXE file for distribution.

Problem:

When I run the JAR file, the application is unable to locate the CSV file. I'm unsure how to package the CSV file correctly with the JAR, or how to ensure that the application can read and write to the CSV file once it's exported.

What I Need:

How should I structure my project and configure the FileService class so that the CSV file is accessible when the application is exported as a JAR? Is there a recommended way to handle file paths for resources like CSV files when distributing a JavaFX application as a JAR or EXE?

Additional Information:

The CSV file is currently located in the Resources/Database/ directory within my project structure. I'm planning to convert the JAR to an EXE for easier distribution to friends.

Any advice on the correct approach or best practices would be greatly appreciated!

Project directory:

.
└── src/
    ├── Controller/
    │   ├── TodoController
    │   └── TodoItemController
    ├── Model/
    │   └── Status
    ├── View/
    │   ├── TodoItem.fxml
    │   └── TodoManager.fxml
    ├── Util/
    │   └── AlerUtil
    ├── Service/
    │   └── TodoManager
    └── Resources/
        ├── Database/
        │   └── tasks.csv
        ├── bin.png
        ├── plus.png
        └── logo.png

FileService Class:

package Service;

import com.opencsv.CSVReader;
import com.opencsv.CSVWriter;
import com.opencsv.exceptions.CsvException;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class FileService {

    private final String filePath = "src/Resources/Database/tasks.csv";

    /**
     * @return List of String Array representing rows and columns
     */
    public List<String[]> readCsv() {
        List<String[]> data = new ArrayList<>();

        try (CSVReader reader = new CSVReader(new FileReader(filePath))) {
            // skip headers row 1.
            reader.skip(1);
            // read the rest of the rows
            data = reader.readAll();
        } catch (FileNotFoundException e) {
            System.err.println("Unable to locate task.csv " + e);
        } catch (IOException | CsvException e) {
            System.err.println(e.getMessage());
        }
        return data;
    }

    public void appendData(String[] record) {
        // passing true as the second parameter for append
        try (CSVWriter writer = new CSVWriter(new FileWriter(filePath, true))) {
            // appending the record
            writer.writeNext(record);
        } catch (IOException e) {
            System.err.println(e.getMessage());
        }
    }

    public void writeData(List<String[]> data) {
        try (CSVWriter writer = new CSVWriter(new FileWriter(filePath))) {
            // write the header
            writer.writeNext(new String[] {"task", "dateCreated", "status", "dateCompleted"});

            // write the data
            writer.writeAll(data);
        } catch (IOException e) {
            System.err.println(e.getMessage());
        }
    }

}

What I've Done:


Solution

  • How should I structure my project

    You should follow the maven standard directory layout using a build tool (I recommend Maven, but you can use Gradle if you prefer). While you could use a non-standard layout, there is seldom a good reason to do so except if integrating a very large legacy project under limited time constraints.

    Put your code under:

    src/main/java 
    

    and your read-only resources under:

    src/main/resources
    

    The Idea new JavaFX project wizard will set up a working project with this structure for you, configuring a build tool of your choice. The wizard will provide a sample FXML file and usage, but you can remove that if you don't use FXML.

    Using Read-Only Resources

    Loading follows the pattern from:

    Which is generally going to be:

    URL resourceUrl = SomeClass.class.getResource(location);
    

    OR

    InputStream resourceStream = SomeClass.class.getResourceAsStream(location);
    

    Where the location is a String with these properties:

    A complete guide is at:

    Read-Write Data

    However, you want to be able to also write to your data file. So a read-only resource is either not useful or only useful for providing initial default data to be copied elsewhere (e.g. to a file on the file system or in a database store separate from the app).

    The Makery JavaFX tutorial provides info on a read/write data storage mechanism. It uses an XML storage format, but CSV would also be OK.

    Examples for copying default read-only resource data to a writable location (if you need to do that):

    App Distribution

    Distribution is a tricky topic but a jlink zip is easy to create. jpackage is more difficult, but can create an installer if you need one. More info can be found in the packaging section under in the JavaFX tag.