3

I am writing a small JavaFx Application, where the Main Class holds an ObservableArryList of Users. These Users have ObservableList's of Accounts, and those Accounts have ObservableList's of Transactions and so on...

Here is the Class-Diagram:Class-Diagram

I would like to save and later read the data of the application to/fram a file.

I already try to save it by implementing Serializable interface in all my classes, but apparently you can't Serialize an ObservableList.

I also tried to save it in a Json file by using Gson, or as XML file with JAXB, but none of them stored the Lists recursively.

So my Question is: Does someone know any way how i could save all the objects that are currently in my Application, and then later load them again?

EDIT: I implemented the JAXB based storage approach given by jewelsea, and the saving/loading of the data is now working perfectly.

UniqUnicorn
  • 133
  • 2
  • 7

1 Answers1

3

General design approach recommendation

For your problem, I'd be inclined to use a database instead of serialization. There are many to choose from, depending on your needs. For a small embedded database, something like H2 would be a reasonable choice. An example for integrating JavaFX and H2 is provided here.

For persistence you could use straight JDBC or JPA. For a substantial application, JPA will be better. For a small application, JDBC suffices. If you use JPA, you can integrate it with JavaFX property based classes, as defined in the articles linked to Put together JavaFX properties and JPA Entities (NO MIXED MODE) and this JavaFX plus JPA example. However, you may wish to keep the JavaFX view model property objects separate and use a DAO pattern for your persistence. Keeping the objects separate gives you a bit more flexibility in your application design and implementation, but violates DRY principles. It's a trade-off though as the resultant objects better respect the single responsibility principle.

Define separate tables for each of your entities (users, accounts, recipients, transactions). Assign each entity entry a unique id key. Use relations to link the item references that you have stored in your ObservableLists.

If you want to access the database from remote locations and you can't open up a direct port connection to it, then you will need to provide a service on the server that provides the data (e.g. a REST based server that performs the database access and exposes required data as JSON over HTTP that your JavaFX client accesses via a REST client then processes the REST call responses into client JavaFX property based data structures). Such implementations quickly become a lot of work :-)

Probably I shouldn't have answered this, as the question (or my interpretation of it) is too broad by StackOverflow principles, but hopefully the info here is useful to you.

Specific answer based upon additional info

I actually already have a spring-boot based web application with DAO and Hibernate that is working fine, and this JavaFX App is planned to connect to that web app. I just need this locally saved files as a little "demo" of the program, if there is currently no internet connection available

Gotcha, that makes total sense. I have integrated JavaFX with SpringBoot before, but unfortunately I can't publish source for those implementations publicly.

For your demo program, persistence via JAXB or Jackson should suffice. Makery provide a nice example for JAXB based persistence for JavaFX.

The trick with the JAXB based approach is to get something that works with your nested data model.

JAXB based storage approach example

This example is based upon ideas from the Makery JavaFX tutorial. To better understand it, consult the tutorial. The nested observable list persistence is achieved using the concepts from: JAXB: How to marshal objects in lists?.

The key to the solution is this bit of code in the User class. It provides the account list as a nested ObservableList and provides a standard accessor accounts() to retrieve the ObservableList as per JavaFX conventions. It also provides a getAccounts() and setAccounts() method that copies in and out of the ObservableList to a standard Java List and notates the getter with a JAXB @Xml... annotations to enable JAXB to handle the serialization and deserialization of the accounts linked to the users.

private final ObservableList<Account> accounts = FXCollections.observableArrayList();

public ObservableList<Account> accounts() { return accounts; }

@XmlElementWrapper(name="accounts")
@XmlElement(name = "account")
public List<Account> getAccounts() {
    return new ArrayList<>(accounts);
}

public void setAccounts(List<Account> accounts) {
    this.accounts.setAll(accounts);
}

UserAccountPersistence.java

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.prefs.Preferences;
import java.util.stream.Collectors;

public class UserAccountPersistence {

    private ObservableList<User> users = FXCollections.observableArrayList();

    public UserAccountPersistence() throws JAXBException, IOException {
        File dbFile = getDatabaseFilePath();
        if (dbFile == null) {
            setDatabaseFilePath(new File(System.getProperty("user.home") + "/" + "user-account.xml"));
            dbFile = getDatabaseFilePath();
        }

        if (!dbFile.exists()) {
            createTestData();
            saveData(dbFile);
        } else {
            loadData(dbFile);
        }

        System.out.println("Persisted Data: ");
        System.out.println(
                Files.lines(dbFile.toPath())
                        .collect(Collectors.joining("\n"))
        );
        System.out.println("Database File: " + dbFile);
    }

    private void createTestData() {
        users.add(new User("Hans", "Muster"));
        users.add(new User("Ruth", "Mueller"));
        users.add(new User("Heinz", "Kurz"));

        users.get(0).accounts().addAll(
                new Account(10),
                new Account(20)
        );

        users.get(2).accounts().addAll(
                new Account(15)
        );
    }

    public File getDatabaseFilePath() {
        Preferences prefs = Preferences.userNodeForPackage(UserAccountPersistence.class);
        String filePath = prefs.get("filePath", null);
        if (filePath != null) {
            return new File(filePath);
        } else {
            return null;
        }
    }

    public void setDatabaseFilePath(File file) {
        Preferences prefs = Preferences.userNodeForPackage(UserAccountPersistence.class);
        if (file != null) {
            prefs.put("filePath", file.getPath());
        } else {
            prefs.remove("filePath");
        }
    }

    public void loadData(File file) throws JAXBException {
        JAXBContext context = JAXBContext
                .newInstance(UserListWrapper.class);
        Unmarshaller um = context.createUnmarshaller();

        UserListWrapper wrapper = (UserListWrapper) um.unmarshal(file);

        users.clear();
        users.addAll(wrapper.getPersons());

        setDatabaseFilePath(file);
    }

    public void saveData(File file) throws JAXBException {
        JAXBContext context = JAXBContext
                .newInstance(UserListWrapper.class);
        Marshaller m = context.createMarshaller();
        m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);

        UserListWrapper wrapper = new UserListWrapper();
        wrapper.setPersons(users);

        m.marshal(wrapper, file);

        setDatabaseFilePath(file);
    }

    public static void main(String[] args) throws JAXBException, IOException {
        UserAccountPersistence userAccountPersistence = new UserAccountPersistence();
    }
}

UserListWrapper.java

import java.util.List;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "users")
public class UserListWrapper {

    private List<User> persons;

    @XmlElement(name = "user")
    public List<User> getPersons() {
        return persons;
    }

    public void setPersons(List<User> persons) {
        this.persons = persons;
    }
}

User.java

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class User {
    private final StringProperty id;
    private final StringProperty firstName;
    private final StringProperty lastName;

    private final ObservableList<Account> accounts = FXCollections.observableArrayList();

    public User() {
        this(UUID.randomUUID().toString(), null, null);
    }

    public User(String firstName, String lastName) {
        this(UUID.randomUUID().toString(), firstName, lastName);
    }

    public User(String id, String firstName, String lastName) {
        this.id = new SimpleStringProperty(id);
        this.firstName = new SimpleStringProperty(firstName);
        this.lastName = new SimpleStringProperty(lastName);
    }

    public String getId() {
        return id.get();
    }

    public void setId(String id) {
        this.id.set(id);
    }

    public StringProperty idProperty() {
        return id;
    }

    public String getFirstName() {
        return firstName.get();
    }

    public void setFirstName(String firstName) {
        this.firstName.set(firstName);
    }

    public StringProperty firstNameProperty() {
        return firstName;
    }

    public String getLastName() {
        return lastName.get();
    }

    public void setLastName(String lastName) {
        this.lastName.set(lastName);
    }

    public StringProperty lastNameProperty() {
        return lastName;
    }

    public ObservableList<Account> accounts() { return accounts; }

    @XmlElementWrapper(name="accounts")
    @XmlElement(name = "account")
    public List<Account> getAccounts() {
        return new ArrayList<>(accounts);
    }

    public void setAccounts(List<Account> accounts) {
        this.accounts.setAll(accounts);
    }

}

Account.java

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

import java.util.UUID;

public class Account {
    private final StringProperty id;
    private final IntegerProperty balance;

    public Account() {
        this(UUID.randomUUID().toString(), 0);
    }

    public Account(int balance) {
        this(UUID.randomUUID().toString(), balance);
    }

    public Account(String id, int balance) {
        this.id = new SimpleStringProperty(id);
        this.balance = new SimpleIntegerProperty(balance);
    }

    public String getId() {
        return id.get();
    }

    public void setId(String id) {
        this.id.set(id);
    }

    public StringProperty idProperty() {
        return id;
    }

    public int getBalance() {
        return balance.get();
    }

    public IntegerProperty balanceProperty() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance.set(balance);
    }
}

Output

$ cat /Users/jewelsea/user-account.xml

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<users>
    <user>
        <accounts>
            <account>
                <balance>10</balance>
                <id>a17b8244-5d3a-4fb4-a992-da26f4e14917</id>
            </account>
            <account>
                <balance>20</balance>
                <id>f0b23df5-3cc0-418c-9840-633bc0f0b3ca</id>
            </account>
        </accounts>
        <firstName>Hans</firstName>
        <id>078dad74-ea9d-407d-9be5-d36c52c53b0d</id>
        <lastName>Muster</lastName>
    </user>
    <user>
        <accounts/>
        <firstName>Ruth</firstName>
        <id>78513f1b-75ee-4ca9-a6f0-444f517e3377</id>
        <lastName>Mueller</lastName>
    </user>
    <user>
        <accounts>
            <account>
                <balance>15</balance>
                <id>77c4fd3c-5f7a-46cf-a806-da1e6f93baab</id>
            </account>
        </accounts>
        <firstName>Heinz</firstName>
        <id>651d9206-42a5-4b76-b89e-be46dce8df74</id>
        <lastName>Kurz</lastName>
    </user>
</users>
Community
  • 1
  • 1
jewelsea
  • 150,031
  • 14
  • 366
  • 406
  • Thank you very much for your quick answer! I actually already have a spring-boot based web application with DAO and Hibernate that is working fine, and this JavaFX App is planned to connect to that web app. I just need this locally saved files as a little "demo" of the program, if there is currently no internet connection available. I'll definitely check out the examples of H2 that you provided! – UniqUnicorn Dec 08 '16 at 23:01
  • I updated the question with the new JAXB example that you provided! – UniqUnicorn Dec 09 '16 at 13:44
  • @UniqUnicorn I am not sure why the unmarshalling is not working for your version of the code. I double-checked the code in my answer and unmarshalling is working for the code I provided. I do not know the reason for difference in behavior that you experience for your implementation. – jewelsea Dec 09 '16 at 18:40
  • Pretty cool. Used this small pattern with JSON (Jackson) and worked immediately. – Wesos de Queso Jan 25 '20 at 01:50