From 4951b8720dfdf058e4e17937477bb683c2a2c6fa Mon Sep 17 00:00:00 2001
From: andrewlalis
Date: Thu, 18 Jan 2024 10:09:06 -0500
Subject: [PATCH] Refactor profile loading and turn profile into a record.
---
.../com/andrewlalis/perfin/PerfinApp.java | 9 +-
.../perfin/control/AccountViewController.java | 10 +-
.../control/AccountsViewController.java | 4 +-
.../control/BalanceRecordViewController.java | 4 +-
.../CreateBalanceRecordController.java | 8 +-
.../perfin/control/EditAccountController.java | 4 +-
.../control/EditTransactionController.java | 8 +-
.../control/ProfilesViewController.java | 15 ++-
.../control/TransactionViewController.java | 4 +-
.../control/TransactionsViewController.java | 10 +-
.../perfin/data/DataSourceFactory.java | 19 +++
.../data/impl/JdbcDataSourceFactory.java | 10 +-
.../com/andrewlalis/perfin/model/Profile.java | 124 +++---------------
.../perfin/model/ProfileLoader.java | 90 +++++++++++++
.../view/component/AccountSelectionBox.java | 2 +-
.../perfin/view/component/AccountTile.java | 2 +-
.../view/component/AttachmentPreview.java | 4 +-
.../view/component/TransactionTile.java | 2 +-
18 files changed, 183 insertions(+), 146 deletions(-)
create mode 100644 src/main/java/com/andrewlalis/perfin/data/DataSourceFactory.java
create mode 100644 src/main/java/com/andrewlalis/perfin/model/ProfileLoader.java
diff --git a/src/main/java/com/andrewlalis/perfin/PerfinApp.java b/src/main/java/com/andrewlalis/perfin/PerfinApp.java
index 22b0fa6..13d1701 100644
--- a/src/main/java/com/andrewlalis/perfin/PerfinApp.java
+++ b/src/main/java/com/andrewlalis/perfin/PerfinApp.java
@@ -3,7 +3,9 @@ package com.andrewlalis.perfin;
import com.andrewlalis.javafx_scene_router.AnchorPaneRouterView;
import com.andrewlalis.javafx_scene_router.SceneRouter;
import com.andrewlalis.perfin.data.ProfileLoadException;
+import com.andrewlalis.perfin.data.impl.JdbcDataSourceFactory;
import com.andrewlalis.perfin.model.Profile;
+import com.andrewlalis.perfin.model.ProfileLoader;
import com.andrewlalis.perfin.view.ImageCache;
import com.andrewlalis.perfin.view.SceneUtil;
import com.andrewlalis.perfin.view.StartupSplashScreen;
@@ -29,6 +31,7 @@ public class PerfinApp extends Application {
private static final Logger log = LoggerFactory.getLogger(PerfinApp.class);
public static final Path APP_DIR = Path.of(System.getProperty("user.home", "."), ".perfin");
public static PerfinApp instance;
+ public static ProfileLoader profileLoader;
/**
* The router that's used for navigating between different "pages" in the application.
@@ -48,6 +51,7 @@ public class PerfinApp extends Application {
@Override
public void start(Stage stage) {
instance = this;
+ profileLoader = new ProfileLoader(stage, new JdbcDataSourceFactory());
loadFonts();
var splashScreen = new StartupSplashScreen(List.of(
PerfinApp::defineRoutes,
@@ -112,9 +116,10 @@ public class PerfinApp extends Application {
}
private static void loadLastUsedProfile(Consumer msgConsumer) throws Exception {
- msgConsumer.accept("Loading the most recent profile.");
+ String lastProfile = ProfileLoader.getLastProfile();
+ msgConsumer.accept("Loading the most recent profile: \"" + lastProfile + "\".");
try {
- Profile.loadLast();
+ Profile.setCurrent(profileLoader.load(lastProfile));
} catch (ProfileLoadException e) {
msgConsumer.accept("Failed to load the profile: " + e.getMessage());
throw e;
diff --git a/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java b/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java
index 2694104..411a57a 100644
--- a/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java
@@ -64,7 +64,7 @@ public class AccountViewController implements RouteSelectionListener {
accountNumberLabel.setText(account.getAccountNumber());
accountCurrencyLabel.setText(account.getCurrency().getDisplayName());
accountCreatedAtLabel.setText(DateUtil.formatUTCAsLocalWithZone(account.getCreatedAt()));
- Profile.getCurrent().getDataSource().getAccountBalanceText(account)
+ Profile.getCurrent().dataSource().getAccountBalanceText(account)
.thenAccept(accountBalanceLabel::setText);
reloadHistory();
@@ -96,7 +96,7 @@ public class AccountViewController implements RouteSelectionListener {
"later if you need to."
);
if (confirmResult) {
- Profile.getCurrent().getDataSource().useRepo(AccountRepository.class, repo -> repo.archive(account.id));
+ Profile.getCurrent().dataSource().useRepo(AccountRepository.class, repo -> repo.archive(account.id));
router.replace("accounts");
}
}
@@ -107,7 +107,7 @@ public class AccountViewController implements RouteSelectionListener {
"status?"
);
if (confirm) {
- Profile.getCurrent().getDataSource().useRepo(AccountRepository.class, repo -> repo.unarchive(account.id));
+ Profile.getCurrent().dataSource().useRepo(AccountRepository.class, repo -> repo.unarchive(account.id));
router.replace("accounts");
}
}
@@ -122,13 +122,13 @@ public class AccountViewController implements RouteSelectionListener {
"want to hide it."
);
if (confirm) {
- Profile.getCurrent().getDataSource().useRepo(AccountRepository.class, repo -> repo.delete(account));
+ Profile.getCurrent().dataSource().useRepo(AccountRepository.class, repo -> repo.delete(account));
router.replace("accounts");
}
}
@FXML public void loadMoreHistory() {
- Profile.getCurrent().getDataSource().useRepoAsync(AccountHistoryItemRepository.class, repo -> {
+ Profile.getCurrent().dataSource().useRepoAsync(AccountHistoryItemRepository.class, repo -> {
List historyItems = repo.findMostRecentForAccount(
account.id,
loadHistoryFrom,
diff --git a/src/main/java/com/andrewlalis/perfin/control/AccountsViewController.java b/src/main/java/com/andrewlalis/perfin/control/AccountsViewController.java
index 7234eb6..5071b96 100644
--- a/src/main/java/com/andrewlalis/perfin/control/AccountsViewController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/AccountsViewController.java
@@ -49,7 +49,7 @@ public class AccountsViewController implements RouteSelectionListener {
public void refreshAccounts() {
Profile.whenLoaded(profile -> {
- profile.getDataSource().useRepoAsync(AccountRepository.class, repo -> {
+ profile.dataSource().useRepoAsync(AccountRepository.class, repo -> {
List accounts = repo.findAllOrderedByRecentHistory();
Platform.runLater(() -> accountsPane.getChildren()
.setAll(accounts.stream()
@@ -59,7 +59,7 @@ public class AccountsViewController implements RouteSelectionListener {
});
// Compute grand totals!
Thread.ofVirtual().start(() -> {
- var totals = profile.getDataSource().getCombinedAccountBalances();
+ var totals = profile.dataSource().getCombinedAccountBalances();
StringBuilder sb = new StringBuilder("Totals: ");
for (var entry : totals.entrySet()) {
sb.append(CurrencyUtil.formatMoneyWithCurrencyPrefix(new MoneyValue(entry.getValue(), entry.getKey())));
diff --git a/src/main/java/com/andrewlalis/perfin/control/BalanceRecordViewController.java b/src/main/java/com/andrewlalis/perfin/control/BalanceRecordViewController.java
index 642294a..32334ca 100644
--- a/src/main/java/com/andrewlalis/perfin/control/BalanceRecordViewController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/BalanceRecordViewController.java
@@ -41,7 +41,7 @@ public class BalanceRecordViewController implements RouteSelectionListener {
timestampLabel.setText(DateUtil.formatUTCAsLocalWithZone(balanceRecord.getTimestamp()));
balanceLabel.setText(CurrencyUtil.formatMoney(balanceRecord.getMoneyAmount()));
currencyLabel.setText(balanceRecord.getCurrency().getDisplayName());
- Profile.getCurrent().getDataSource().useRepoAsync(BalanceRecordRepository.class, repo -> {
+ Profile.getCurrent().dataSource().useRepoAsync(BalanceRecordRepository.class, repo -> {
List attachments = repo.findAttachments(balanceRecord.id);
Platform.runLater(() -> attachmentsViewPane.setAttachments(attachments));
});
@@ -50,7 +50,7 @@ public class BalanceRecordViewController implements RouteSelectionListener {
@FXML public void delete() {
boolean confirm = Popups.confirm("Are you sure you want to delete this balance record? This may have an effect on the derived balance of your account, as shown in Perfin.");
if (confirm) {
- Profile.getCurrent().getDataSource().useRepo(BalanceRecordRepository.class, repo -> repo.deleteById(balanceRecord.id));
+ Profile.getCurrent().dataSource().useRepo(BalanceRecordRepository.class, repo -> repo.deleteById(balanceRecord.id));
router.navigateBackAndClear();
}
}
diff --git a/src/main/java/com/andrewlalis/perfin/control/CreateBalanceRecordController.java b/src/main/java/com/andrewlalis/perfin/control/CreateBalanceRecordController.java
index f70b78a..68d1657 100644
--- a/src/main/java/com/andrewlalis/perfin/control/CreateBalanceRecordController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/CreateBalanceRecordController.java
@@ -60,7 +60,7 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
return;
}
BigDecimal reportedBalance = new BigDecimal(newValue);
- Profile.getCurrent().getDataSource().useRepoAsync(AccountRepository.class, repo -> {
+ Profile.getCurrent().dataSource().useRepoAsync(AccountRepository.class, repo -> {
BigDecimal derivedBalance = repo.deriveCurrentBalance(account.id);
Platform.runLater(() -> balanceWarningLabel.visibleProperty().set(
!reportedBalance.setScale(derivedBalance.scale(), RoundingMode.HALF_UP).equals(derivedBalance)
@@ -76,7 +76,7 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
public void onRouteSelected(Object context) {
this.account = (Account) context;
timestampField.setText(LocalDateTime.now().format(DateUtil.DEFAULT_DATETIME_FORMAT));
- Profile.getCurrent().getDataSource().useRepoAsync(AccountRepository.class, repo -> {
+ Profile.getCurrent().dataSource().useRepoAsync(AccountRepository.class, repo -> {
BigDecimal value = repo.deriveCurrentBalance(account.id);
Platform.runLater(() -> balanceField.setText(
CurrencyUtil.formatMoneyAsBasicNumber(new MoneyValue(value, account.getCurrency()))
@@ -95,7 +95,7 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
localTimestamp.atZone(ZoneId.systemDefault()).format(DateUtil.DEFAULT_DATETIME_FORMAT_WITH_ZONE)
));
if (confirm && confirmIfInconsistentBalance(reportedBalance)) {
- Profile.getCurrent().getDataSource().useRepo(BalanceRecordRepository.class, repo -> {
+ Profile.getCurrent().dataSource().useRepo(BalanceRecordRepository.class, repo -> {
repo.insert(
DateUtil.localToUTC(localTimestamp),
account.id,
@@ -113,7 +113,7 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
}
private boolean confirmIfInconsistentBalance(BigDecimal reportedBalance) {
- BigDecimal currentDerivedBalance = Profile.getCurrent().getDataSource().mapRepo(
+ BigDecimal currentDerivedBalance = Profile.getCurrent().dataSource().mapRepo(
AccountRepository.class,
repo -> repo.deriveCurrentBalance(account.id)
);
diff --git a/src/main/java/com/andrewlalis/perfin/control/EditAccountController.java b/src/main/java/com/andrewlalis/perfin/control/EditAccountController.java
index 3074aa9..3e3142e 100644
--- a/src/main/java/com/andrewlalis/perfin/control/EditAccountController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/EditAccountController.java
@@ -106,8 +106,8 @@ public class EditAccountController implements RouteSelectionListener {
@FXML
public void save() {
try (
- var accountRepo = Profile.getCurrent().getDataSource().getAccountRepository();
- var balanceRepo = Profile.getCurrent().getDataSource().getBalanceRecordRepository()
+ var accountRepo = Profile.getCurrent().dataSource().getAccountRepository();
+ var balanceRepo = Profile.getCurrent().dataSource().getBalanceRecordRepository()
) {
if (creatingNewAccount.get()) {
String name = accountNameField.getText().strip();
diff --git a/src/main/java/com/andrewlalis/perfin/control/EditTransactionController.java b/src/main/java/com/andrewlalis/perfin/control/EditTransactionController.java
index c708fe0..6bebcd8 100644
--- a/src/main/java/com/andrewlalis/perfin/control/EditTransactionController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/EditTransactionController.java
@@ -111,7 +111,7 @@ public class EditTransactionController implements RouteSelectionListener {
List existingAttachments = attachmentsSelectionArea.getSelectedAttachments();
final long idToNavigate;
if (transaction == null) {
- idToNavigate = Profile.getCurrent().getDataSource().mapRepo(
+ idToNavigate = Profile.getCurrent().dataSource().mapRepo(
TransactionRepository.class,
repo -> repo.insert(
utcTimestamp,
@@ -123,7 +123,7 @@ public class EditTransactionController implements RouteSelectionListener {
)
);
} else {
- Profile.getCurrent().getDataSource().useRepo(
+ Profile.getCurrent().dataSource().useRepo(
TransactionRepository.class,
repo -> repo.update(
transaction.id,
@@ -165,8 +165,8 @@ public class EditTransactionController implements RouteSelectionListener {
container.setDisable(true);
Thread.ofVirtual().start(() -> {
try (
- var accountRepo = Profile.getCurrent().getDataSource().getAccountRepository();
- var transactionRepo = Profile.getCurrent().getDataSource().getTransactionRepository()
+ var accountRepo = Profile.getCurrent().dataSource().getAccountRepository();
+ var transactionRepo = Profile.getCurrent().dataSource().getTransactionRepository()
) {
// First fetch all the data.
List currencies = accountRepo.findAllUsedCurrencies().stream()
diff --git a/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java b/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java
index 89d7aca..d9bb663 100644
--- a/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java
@@ -4,6 +4,7 @@ import com.andrewlalis.perfin.PerfinApp;
import com.andrewlalis.perfin.data.ProfileLoadException;
import com.andrewlalis.perfin.data.util.FileUtil;
import com.andrewlalis.perfin.model.Profile;
+import com.andrewlalis.perfin.model.ProfileLoader;
import com.andrewlalis.perfin.view.ProfilesStage;
import com.andrewlalis.perfin.view.component.validation.ValidationApplier;
import com.andrewlalis.perfin.view.component.validation.validators.PredicateValidator;
@@ -44,7 +45,7 @@ public class ProfilesViewController {
@FXML public void addProfile() {
String name = newProfileNameField.getText();
boolean valid = Profile.validateName(name);
- if (valid && !Profile.getAvailableProfiles().contains(name)) {
+ if (valid && !ProfileLoader.getAvailableProfiles().contains(name)) {
boolean confirm = Popups.confirm("Are you sure you want to add a new profile named \"" + name + "\"?");
if (confirm) {
if (openProfile(name, false)) {
@@ -56,8 +57,8 @@ public class ProfilesViewController {
}
private void refreshAvailableProfiles() {
- List profileNames = Profile.getAvailableProfiles();
- String currentProfile = Profile.getCurrent() == null ? null : Profile.getCurrent().getName();
+ List profileNames = ProfileLoader.getAvailableProfiles();
+ String currentProfile = Profile.getCurrent() == null ? null : Profile.getCurrent().name();
List nodes = new ArrayList<>(profileNames.size());
for (String profileName : profileNames) {
boolean isCurrent = profileName.equals(currentProfile);
@@ -104,7 +105,7 @@ public class ProfilesViewController {
private boolean openProfile(String name, boolean showPopup) {
log.info("Opening profile \"{}\".", name);
try {
- Profile.load(name);
+ PerfinApp.profileLoader.load(name);
ProfilesStage.closeView();
router.replace("accounts");
if (showPopup) Popups.message("The profile \"" + name + "\" has been loaded.");
@@ -123,11 +124,11 @@ public class ProfilesViewController {
try {
FileUtil.deleteDirRecursive(Profile.getDir(name));
// Reset the app's "last profile" to the default if it was the deleted profile.
- if (Profile.getLastProfile().equals(name)) {
- Profile.saveLastProfile("default");
+ if (ProfileLoader.getLastProfile().equals(name)) {
+ ProfileLoader.saveLastProfile("default");
}
// If the current profile was deleted, switch to the default.
- if (Profile.getCurrent() != null && Profile.getCurrent().getName().equals(name)) {
+ if (Profile.getCurrent() != null && Profile.getCurrent().name().equals(name)) {
openProfile("default", true);
}
refreshAvailableProfiles();
diff --git a/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java b/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java
index ca2181e..7315ec0 100644
--- a/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java
@@ -45,7 +45,7 @@ public class TransactionViewController {
amountLabel.setText(CurrencyUtil.formatMoney(transaction.getMoneyAmount()));
timestampLabel.setText(DateUtil.formatUTCAsLocalWithZone(transaction.getTimestamp()));
descriptionLabel.setText(transaction.getDescription());
- Profile.getCurrent().getDataSource().useRepoAsync(TransactionRepository.class, repo -> {
+ Profile.getCurrent().dataSource().useRepoAsync(TransactionRepository.class, repo -> {
CreditAndDebitAccounts accounts = repo.findLinkedAccounts(transaction.id);
List attachments = repo.findAttachments(transaction.id);
Platform.runLater(() -> {
@@ -81,7 +81,7 @@ public class TransactionViewController {
"it's derived from the most recent balance-record, and transactions."
);
if (confirm) {
- Profile.getCurrent().getDataSource().useRepo(TransactionRepository.class, repo -> repo.delete(transaction.id));
+ Profile.getCurrent().dataSource().useRepo(TransactionRepository.class, repo -> repo.delete(transaction.id));
router.replace("transactions");
}
}
diff --git a/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java b/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java
index bb00272..6db70a3 100644
--- a/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java
@@ -66,7 +66,7 @@ public class TransactionsViewController implements RouteSelectionListener {
@Override
public Page extends Node> fetchPage(PageRequest pagination) throws Exception {
Account accountFilter = filterByAccountComboBox.getValue();
- try (var repo = Profile.getCurrent().getDataSource().getTransactionRepository()) {
+ try (var repo = Profile.getCurrent().dataSource().getTransactionRepository()) {
Page result;
if (accountFilter == null) {
result = repo.findAll(pagination);
@@ -80,7 +80,7 @@ public class TransactionsViewController implements RouteSelectionListener {
@Override
public int getTotalCount() throws Exception {
Account accountFilter = filterByAccountComboBox.getValue();
- try (var repo = Profile.getCurrent().getDataSource().getTransactionRepository()) {
+ try (var repo = Profile.getCurrent().dataSource().getTransactionRepository()) {
if (accountFilter == null) {
return (int) repo.countAll();
} else {
@@ -124,7 +124,7 @@ public class TransactionsViewController implements RouteSelectionListener {
transactionsVBox.getChildren().clear(); // Clear the transactions before reload initially.
// Refresh account filter options.
- Profile.getCurrent().getDataSource().useRepoAsync(AccountRepository.class, repo -> {
+ Profile.getCurrent().dataSource().useRepoAsync(AccountRepository.class, repo -> {
List accounts = repo.findAll(PageRequest.unpaged(Sort.asc("name"))).items();
Platform.runLater(() -> {
filterByAccountComboBox.setAccounts(accounts);
@@ -135,7 +135,7 @@ public class TransactionsViewController implements RouteSelectionListener {
// If a transaction id is given in the route context, navigate to the page it's on and select it.
if (context instanceof RouteContext ctx && ctx.selectedTransactionId != null) {
- Profile.getCurrent().getDataSource().useRepoAsync(TransactionRepository.class, repo -> {
+ Profile.getCurrent().dataSource().useRepoAsync(TransactionRepository.class, repo -> {
repo.findById(ctx.selectedTransactionId).ifPresent(tx -> {
long offset = repo.countAllAfter(tx.id);
int pageNumber = (int) (offset / paginationControls.getItemsPerPage()) + 1;
@@ -161,7 +161,7 @@ public class TransactionsViewController implements RouteSelectionListener {
File file = fileChooser.showSaveDialog(detailPanel.getScene().getWindow());
if (file != null) {
try (
- var repo = Profile.getCurrent().getDataSource().getTransactionRepository();
+ var repo = Profile.getCurrent().dataSource().getTransactionRepository();
var out = new PrintWriter(file, StandardCharsets.UTF_8)
) {
out.println("id,utc-timestamp,amount,currency,description");
diff --git a/src/main/java/com/andrewlalis/perfin/data/DataSourceFactory.java b/src/main/java/com/andrewlalis/perfin/data/DataSourceFactory.java
new file mode 100644
index 0000000..44c5d3f
--- /dev/null
+++ b/src/main/java/com/andrewlalis/perfin/data/DataSourceFactory.java
@@ -0,0 +1,19 @@
+package com.andrewlalis.perfin.data;
+
+import java.io.IOException;
+
+/**
+ * Interface that defines the data source factory, a component responsible for
+ * obtaining a data source, and performing some introspection around that data
+ * source before one is obtained.
+ */
+public interface DataSourceFactory {
+ DataSource getDataSource(String profileName) throws ProfileLoadException;
+
+ enum SchemaStatus {
+ UP_TO_DATE,
+ NEEDS_MIGRATION,
+ INCOMPATIBLE
+ }
+ SchemaStatus getSchemaStatus(String profileName) throws IOException;
+}
diff --git a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcDataSourceFactory.java b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcDataSourceFactory.java
index e61e096..77ed7ab 100644
--- a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcDataSourceFactory.java
+++ b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcDataSourceFactory.java
@@ -1,6 +1,7 @@
package com.andrewlalis.perfin.data.impl;
import com.andrewlalis.perfin.data.DataSource;
+import com.andrewlalis.perfin.data.DataSourceFactory;
import com.andrewlalis.perfin.data.ProfileLoadException;
import com.andrewlalis.perfin.data.impl.migration.Migration;
import com.andrewlalis.perfin.data.impl.migration.Migrations;
@@ -23,7 +24,7 @@ import java.util.List;
/**
* Component that's responsible for obtaining a JDBC data source for a profile.
*/
-public class JdbcDataSourceFactory {
+public class JdbcDataSourceFactory implements DataSourceFactory {
private static final Logger log = LoggerFactory.getLogger(JdbcDataSourceFactory.class);
/**
@@ -59,6 +60,13 @@ public class JdbcDataSourceFactory {
return new JdbcDataSource(getJdbcUrl(profileName), Profile.getContentDir(profileName));
}
+ public SchemaStatus getSchemaStatus(String profileName) throws IOException {
+ int existingSchemaVersion = getSchemaVersion(profileName);
+ if (existingSchemaVersion == SCHEMA_VERSION) return SchemaStatus.UP_TO_DATE;
+ if (existingSchemaVersion < SCHEMA_VERSION) return SchemaStatus.NEEDS_MIGRATION;
+ return SchemaStatus.INCOMPATIBLE;
+ }
+
private void createNewDatabase(String profileName) throws ProfileLoadException {
log.info("Creating new database for profile {}.", profileName);
JdbcDataSource dataSource = new JdbcDataSource(getJdbcUrl(profileName), Profile.getContentDir(profileName));
diff --git a/src/main/java/com/andrewlalis/perfin/model/Profile.java b/src/main/java/com/andrewlalis/perfin/model/Profile.java
index 8881b3d..992dddc 100644
--- a/src/main/java/com/andrewlalis/perfin/model/Profile.java
+++ b/src/main/java/com/andrewlalis/perfin/model/Profile.java
@@ -2,23 +2,16 @@ package com.andrewlalis.perfin.model;
import com.andrewlalis.perfin.PerfinApp;
import com.andrewlalis.perfin.data.DataSource;
-import com.andrewlalis.perfin.data.ProfileLoadException;
-import com.andrewlalis.perfin.data.impl.JdbcDataSourceFactory;
-import com.andrewlalis.perfin.data.util.FileUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.io.IOException;
-import java.nio.file.Files;
+import java.lang.ref.WeakReference;
import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
+import java.util.HashSet;
import java.util.Properties;
+import java.util.Set;
import java.util.function.Consumer;
-import static com.andrewlalis.perfin.data.util.FileUtil.copyResourceFile;
-
/**
* A profile is essentially a complete set of data that the application can
* operate on, sort of like a save file or user account. The profile contains
@@ -36,34 +29,17 @@ import static com.andrewlalis.perfin.data.util.FileUtil.copyResourceFile;
* unloaded.
*
*/
-public class Profile {
+public record Profile(String name, Properties settings, DataSource dataSource) {
private static final Logger log = LoggerFactory.getLogger(Profile.class);
private static Profile current;
- private static final List> profileLoadListeners = new ArrayList<>();
+ private static final Set>> currentProfileListeners = new HashSet<>();
- private final String name;
- private final Properties settings;
- private final DataSource dataSource;
-
- private Profile(String name, Properties settings, DataSource dataSource) {
- this.name = name;
- this.settings = settings;
- this.dataSource = dataSource;
- }
-
- public String getName() {
+ @Override
+ public String toString() {
return name;
}
- public Properties getSettings() {
- return settings;
- }
-
- public DataSource getDataSource() {
- return dataSource;
- }
-
public static Path getDir(String name) {
return PerfinApp.APP_DIR.resolve(name);
}
@@ -80,79 +56,22 @@ public class Profile {
return current;
}
+ public static void setCurrent(Profile profile) {
+ current = profile;
+ for (var ref : currentProfileListeners) {
+ Consumer consumer = ref.get();
+ if (consumer != null) {
+ consumer.accept(profile);
+ }
+ }
+ currentProfileListeners.removeIf(ref -> ref.get() == null);
+ }
+
public static void whenLoaded(Consumer consumer) {
if (current != null) {
consumer.accept(current);
- } else {
- profileLoadListeners.add(consumer);
}
- }
-
- public static List getAvailableProfiles() {
- try (var files = Files.list(PerfinApp.APP_DIR)) {
- return files.filter(Files::isDirectory)
- .map(path -> path.getFileName().toString())
- .sorted().toList();
- } catch (IOException e) {
- log.error("Failed to get a list of available profiles.", e);
- return Collections.emptyList();
- }
- }
-
- public static String getLastProfile() {
- Path lastProfileFile = PerfinApp.APP_DIR.resolve("last-profile.txt");
- if (Files.exists(lastProfileFile)) {
- try {
- String s = Files.readString(lastProfileFile).strip().toLowerCase();
- if (!s.isBlank()) return s;
- } catch (IOException e) {
- log.error("Failed to read " + lastProfileFile, e);
- }
- }
- return "default";
- }
-
- public static void saveLastProfile(String name) {
- Path lastProfileFile = PerfinApp.APP_DIR.resolve("last-profile.txt");
- try {
- Files.writeString(lastProfileFile, name);
- } catch (IOException e) {
- log.error("Failed to write " + lastProfileFile, e);
- }
- }
-
- public static void loadLast() throws ProfileLoadException {
- load(getLastProfile());
- }
-
- public static void load(String name) throws ProfileLoadException {
- if (Files.notExists(getDir(name))) {
- try {
- initProfileDir(name);
- } catch (IOException e) {
- FileUtil.deleteIfPossible(getDir(name));
- throw new ProfileLoadException("Failed to initialize new profile directory.", e);
- }
- }
- Properties settings = new Properties();
- try (var in = Files.newInputStream(getSettingsFile(name))) {
- settings.load(in);
- } catch (IOException e) {
- throw new ProfileLoadException("Failed to load profile settings.", e);
- }
- current = new Profile(name, settings, new JdbcDataSourceFactory().getDataSource(name));
- saveLastProfile(current.getName());
- for (var c : profileLoadListeners) {
- c.accept(current);
- }
- }
-
- private static void initProfileDir(String name) throws IOException {
- Files.createDirectory(getDir(name));
- copyResourceFile("/text/profileDirReadme.txt", getDir(name).resolve("README.txt"));
- copyResourceFile("/text/defaultProfileSettings.properties", getSettingsFile(name));
- Files.createDirectory(getContentDir(name));
- copyResourceFile("/text/contentDirReadme.txt", getContentDir(name).resolve("README.txt"));
+ currentProfileListeners.add(new WeakReference<>(consumer));
}
public static boolean validateName(String name) {
@@ -160,9 +79,4 @@ public class Profile {
name.matches("\\w+") &&
name.toLowerCase().equals(name);
}
-
- @Override
- public String toString() {
- return name;
- }
}
diff --git a/src/main/java/com/andrewlalis/perfin/model/ProfileLoader.java b/src/main/java/com/andrewlalis/perfin/model/ProfileLoader.java
new file mode 100644
index 0000000..046ea58
--- /dev/null
+++ b/src/main/java/com/andrewlalis/perfin/model/ProfileLoader.java
@@ -0,0 +1,90 @@
+package com.andrewlalis.perfin.model;
+
+import com.andrewlalis.perfin.PerfinApp;
+import com.andrewlalis.perfin.data.DataSourceFactory;
+import com.andrewlalis.perfin.data.ProfileLoadException;
+import com.andrewlalis.perfin.data.util.FileUtil;
+import javafx.stage.Window;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.List;
+import java.util.Properties;
+
+import static com.andrewlalis.perfin.data.util.FileUtil.copyResourceFile;
+
+public class ProfileLoader {
+ private static final Logger log = LoggerFactory.getLogger(ProfileLoader.class);
+
+ private final Window window;
+ private final DataSourceFactory dataSourceFactory;
+
+ public ProfileLoader(Window window, DataSourceFactory dataSourceFactory) {
+ this.window = window;
+ this.dataSourceFactory = dataSourceFactory;
+ }
+
+ public Profile load(String name) throws ProfileLoadException {
+ if (Files.notExists(Profile.getDir(name))) {
+ try {
+ initProfileDir(name);
+ } catch (IOException e) {
+ FileUtil.deleteIfPossible(Profile.getDir(name));
+ throw new ProfileLoadException("Failed to initialize new profile directory.", e);
+ }
+ }
+ Properties settings = new Properties();
+ try (var in = Files.newInputStream(Profile.getSettingsFile(name))) {
+ settings.load(in);
+ } catch (IOException e) {
+ throw new ProfileLoadException("Failed to load profile settings.", e);
+ }
+ return new Profile(name, settings, dataSourceFactory.getDataSource(name));
+ }
+
+ public static List getAvailableProfiles() {
+ try (var files = Files.list(PerfinApp.APP_DIR)) {
+ return files.filter(Files::isDirectory)
+ .map(path -> path.getFileName().toString())
+ .sorted().toList();
+ } catch (IOException e) {
+ log.error("Failed to get a list of available profiles.", e);
+ return Collections.emptyList();
+ }
+ }
+
+ public static String getLastProfile() {
+ Path lastProfileFile = PerfinApp.APP_DIR.resolve("last-profile.txt");
+ if (Files.exists(lastProfileFile)) {
+ try {
+ String s = Files.readString(lastProfileFile).strip().toLowerCase();
+ if (!s.isBlank()) return s;
+ } catch (IOException e) {
+ log.error("Failed to read " + lastProfileFile, e);
+ }
+ }
+ return "default";
+ }
+
+ public static void saveLastProfile(String name) {
+ Path lastProfileFile = PerfinApp.APP_DIR.resolve("last-profile.txt");
+ try {
+ Files.writeString(lastProfileFile, name);
+ } catch (IOException e) {
+ log.error("Failed to write " + lastProfileFile, e);
+ }
+ }
+
+ @Deprecated
+ private static void initProfileDir(String name) throws IOException {
+ Files.createDirectory(Profile.getDir(name));
+ copyResourceFile("/text/profileDirReadme.txt", Profile.getDir(name).resolve("README.txt"));
+ copyResourceFile("/text/defaultProfileSettings.properties", Profile.getSettingsFile(name));
+ Files.createDirectory(Profile.getContentDir(name));
+ copyResourceFile("/text/contentDirReadme.txt", Profile.getContentDir(name).resolve("README.txt"));
+ }
+}
diff --git a/src/main/java/com/andrewlalis/perfin/view/component/AccountSelectionBox.java b/src/main/java/com/andrewlalis/perfin/view/component/AccountSelectionBox.java
index 2b38968..f9cbdb9 100644
--- a/src/main/java/com/andrewlalis/perfin/view/component/AccountSelectionBox.java
+++ b/src/main/java/com/andrewlalis/perfin/view/component/AccountSelectionBox.java
@@ -110,7 +110,7 @@ public class AccountSelectionBox extends ComboBox {
nameLabel.setText(item.getName() + " (" + item.getAccountNumberSuffix() + ")");
if (showBalanceProp.get()) {
- Profile.getCurrent().getDataSource().useRepoAsync(AccountRepository.class, repo -> {
+ Profile.getCurrent().dataSource().useRepoAsync(AccountRepository.class, repo -> {
BigDecimal balance = repo.deriveCurrentBalance(item.id);
Platform.runLater(() -> {
balanceLabel.setText(CurrencyUtil.formatMoney(new MoneyValue(balance, item.getCurrency())));
diff --git a/src/main/java/com/andrewlalis/perfin/view/component/AccountTile.java b/src/main/java/com/andrewlalis/perfin/view/component/AccountTile.java
index ee4a415..1787078 100644
--- a/src/main/java/com/andrewlalis/perfin/view/component/AccountTile.java
+++ b/src/main/java/com/andrewlalis/perfin/view/component/AccountTile.java
@@ -81,7 +81,7 @@ public class AccountTile extends BorderPane {
Label balanceLabel = new Label("Computing balance...");
balanceLabel.getStyleClass().addAll("mono-font");
balanceLabel.setDisable(true);
- Profile.getCurrent().getDataSource().useRepoAsync(AccountRepository.class, repo -> {
+ Profile.getCurrent().dataSource().useRepoAsync(AccountRepository.class, repo -> {
BigDecimal balance = repo.deriveCurrentBalance(account.id);
String text = CurrencyUtil.formatMoney(new MoneyValue(balance, account.getCurrency()));
Platform.runLater(() -> {
diff --git a/src/main/java/com/andrewlalis/perfin/view/component/AttachmentPreview.java b/src/main/java/com/andrewlalis/perfin/view/component/AttachmentPreview.java
index 2e986d0..094caeb 100644
--- a/src/main/java/com/andrewlalis/perfin/view/component/AttachmentPreview.java
+++ b/src/main/java/com/andrewlalis/perfin/view/component/AttachmentPreview.java
@@ -46,7 +46,7 @@ public class AttachmentPreview extends BorderPane {
boolean showDocIcon = true;
Set imageTypes = Set.of("image/png", "image/jpeg", "image/gif", "image/bmp");
if (imageTypes.contains(attachment.getContentType())) {
- try (var in = Files.newInputStream(attachment.getPath(Profile.getContentDir(Profile.getCurrent().getName())))) {
+ try (var in = Files.newInputStream(attachment.getPath(Profile.getContentDir(Profile.getCurrent().name())))) {
Image img = new Image(in, IMAGE_SIZE, IMAGE_SIZE, true, true);
contentContainer.setCenter(new ImageView(img));
showDocIcon = false;
@@ -69,7 +69,7 @@ public class AttachmentPreview extends BorderPane {
this.setCenter(stackPane);
this.setOnMouseClicked(event -> {
if (this.isHover()) {
- Path filePath = attachment.getPath(Profile.getContentDir(Profile.getCurrent().getName()));
+ Path filePath = attachment.getPath(Profile.getContentDir(Profile.getCurrent().name()));
PerfinApp.instance.getHostServices().showDocument(filePath.toAbsolutePath().toUri().toString());
}
});
diff --git a/src/main/java/com/andrewlalis/perfin/view/component/TransactionTile.java b/src/main/java/com/andrewlalis/perfin/view/component/TransactionTile.java
index fd0ff44..993fd4b 100644
--- a/src/main/java/com/andrewlalis/perfin/view/component/TransactionTile.java
+++ b/src/main/java/com/andrewlalis/perfin/view/component/TransactionTile.java
@@ -100,7 +100,7 @@ public class TransactionTile extends BorderPane {
}
private CompletableFuture getCreditAndDebitAccounts(Transaction transaction) {
- return Profile.getCurrent().getDataSource().mapRepoAsync(
+ return Profile.getCurrent().dataSource().mapRepoAsync(
TransactionRepository.class,
repo -> repo.findLinkedAccounts(transaction.id)
);