Refactored repository access to use fancy generic method.

This commit is contained in:
Andrew Lalis 2024-01-12 22:44:29 -05:00
parent 4600470cdb
commit f0b061c34d
18 changed files with 165 additions and 136 deletions

View File

@ -1,6 +1,8 @@
package com.andrewlalis.perfin.control; package com.andrewlalis.perfin.control;
import com.andrewlalis.javafx_scene_router.RouteSelectionListener; import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
import com.andrewlalis.perfin.data.AccountHistoryItemRepository;
import com.andrewlalis.perfin.data.AccountRepository;
import com.andrewlalis.perfin.data.util.DateUtil; import com.andrewlalis.perfin.data.util.DateUtil;
import com.andrewlalis.perfin.model.Account; import com.andrewlalis.perfin.model.Account;
import com.andrewlalis.perfin.model.Profile; import com.andrewlalis.perfin.model.Profile;
@ -62,7 +64,8 @@ public class AccountViewController implements RouteSelectionListener {
accountNumberLabel.setText(account.getAccountNumber()); accountNumberLabel.setText(account.getAccountNumber());
accountCurrencyLabel.setText(account.getCurrency().getDisplayName()); accountCurrencyLabel.setText(account.getCurrency().getDisplayName());
accountCreatedAtLabel.setText(DateUtil.formatUTCAsLocalWithZone(account.getCreatedAt())); accountCreatedAtLabel.setText(DateUtil.formatUTCAsLocalWithZone(account.getCreatedAt()));
Profile.getCurrent().getDataSource().getAccountBalanceText(account, accountBalanceLabel::setText); Profile.getCurrent().getDataSource().getAccountBalanceText(account)
.thenAccept(accountBalanceLabel::setText);
reloadHistory(); reloadHistory();
} }
@ -93,7 +96,7 @@ public class AccountViewController implements RouteSelectionListener {
"later if you need to." "later if you need to."
); );
if (confirmResult) { if (confirmResult) {
Profile.getCurrent().getDataSource().useAccountRepository(repo -> repo.archive(account.id)); Profile.getCurrent().getDataSource().useRepo(AccountRepository.class, repo -> repo.archive(account.id));
router.replace("accounts"); router.replace("accounts");
} }
} }
@ -104,7 +107,7 @@ public class AccountViewController implements RouteSelectionListener {
"status?" "status?"
); );
if (confirm) { if (confirm) {
Profile.getCurrent().getDataSource().useAccountRepository(repo -> repo.unarchive(account.id)); Profile.getCurrent().getDataSource().useRepo(AccountRepository.class, repo -> repo.unarchive(account.id));
router.replace("accounts"); router.replace("accounts");
} }
} }
@ -119,31 +122,27 @@ public class AccountViewController implements RouteSelectionListener {
"want to hide it." "want to hide it."
); );
if (confirm) { if (confirm) {
Profile.getCurrent().getDataSource().useAccountRepository(repo -> repo.delete(account)); Profile.getCurrent().getDataSource().useRepo(AccountRepository.class, repo -> repo.delete(account));
router.replace("accounts"); router.replace("accounts");
} }
} }
@FXML public void loadMoreHistory() { @FXML public void loadMoreHistory() {
Thread.ofVirtual().start(() -> { Profile.getCurrent().getDataSource().useRepoAsync(AccountHistoryItemRepository.class, repo -> {
try (var historyRepo = Profile.getCurrent().getDataSource().getAccountHistoryItemRepository()) { List<AccountHistoryItem> historyItems = repo.findMostRecentForAccount(
List<AccountHistoryItem> historyItems = historyRepo.findMostRecentForAccount( account.id,
account.id, loadHistoryFrom,
loadHistoryFrom, historyLoadSize
historyLoadSize );
); if (historyItems.size() < historyLoadSize) {
if (historyItems.size() < historyLoadSize) { Platform.runLater(() -> loadMoreHistoryButton.setDisable(true));
Platform.runLater(() -> loadMoreHistoryButton.setDisable(true)); } else {
} else { loadHistoryFrom = historyItems.getLast().getTimestamp();
loadHistoryFrom = historyItems.getLast().getTimestamp();
}
List<? extends Node> nodes = historyItems.stream()
.map(item -> AccountHistoryItemTile.forItem(item, historyRepo, this))
.toList();
Platform.runLater(() -> historyItemsVBox.getChildren().addAll(nodes));
} catch (Exception e) {
throw new RuntimeException(e);
} }
List<? extends Node> nodes = historyItems.stream()
.map(item -> AccountHistoryItemTile.forItem(item, repo, this))
.toList();
Platform.runLater(() -> historyItemsVBox.getChildren().addAll(nodes));
}); });
} }
} }

View File

@ -1,6 +1,7 @@
package com.andrewlalis.perfin.control; package com.andrewlalis.perfin.control;
import com.andrewlalis.javafx_scene_router.RouteSelectionListener; import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
import com.andrewlalis.perfin.data.AccountRepository;
import com.andrewlalis.perfin.data.util.CurrencyUtil; import com.andrewlalis.perfin.data.util.CurrencyUtil;
import com.andrewlalis.perfin.model.Account; import com.andrewlalis.perfin.model.Account;
import com.andrewlalis.perfin.model.MoneyValue; import com.andrewlalis.perfin.model.MoneyValue;
@ -48,14 +49,14 @@ public class AccountsViewController implements RouteSelectionListener {
public void refreshAccounts() { public void refreshAccounts() {
Profile.whenLoaded(profile -> { Profile.whenLoaded(profile -> {
Thread.ofVirtual().start(() -> profile.getDataSource().useAccountRepository(repo -> { profile.getDataSource().useRepoAsync(AccountRepository.class, repo -> {
List<Account> accounts = repo.findAllOrderedByRecentHistory(); List<Account> accounts = repo.findAllOrderedByRecentHistory();
Platform.runLater(() -> accountsPane.getChildren() Platform.runLater(() -> accountsPane.getChildren()
.setAll(accounts.stream() .setAll(accounts.stream()
.map(AccountTile::new) .map(AccountTile::new)
.toList() .toList()
)); ));
})); });
// Compute grand totals! // Compute grand totals!
Thread.ofVirtual().start(() -> { Thread.ofVirtual().start(() -> {
var totals = profile.getDataSource().getCombinedAccountBalances(); var totals = profile.getDataSource().getCombinedAccountBalances();

View File

@ -1,6 +1,7 @@
package com.andrewlalis.perfin.control; package com.andrewlalis.perfin.control;
import com.andrewlalis.javafx_scene_router.RouteSelectionListener; import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
import com.andrewlalis.perfin.data.BalanceRecordRepository;
import com.andrewlalis.perfin.data.util.CurrencyUtil; import com.andrewlalis.perfin.data.util.CurrencyUtil;
import com.andrewlalis.perfin.data.util.DateUtil; import com.andrewlalis.perfin.data.util.DateUtil;
import com.andrewlalis.perfin.model.Attachment; import com.andrewlalis.perfin.model.Attachment;
@ -40,19 +41,16 @@ public class BalanceRecordViewController implements RouteSelectionListener {
timestampLabel.setText(DateUtil.formatUTCAsLocalWithZone(balanceRecord.getTimestamp())); timestampLabel.setText(DateUtil.formatUTCAsLocalWithZone(balanceRecord.getTimestamp()));
balanceLabel.setText(CurrencyUtil.formatMoney(balanceRecord.getMoneyAmount())); balanceLabel.setText(CurrencyUtil.formatMoney(balanceRecord.getMoneyAmount()));
currencyLabel.setText(balanceRecord.getCurrency().getDisplayName()); currencyLabel.setText(balanceRecord.getCurrency().getDisplayName());
Profile.getCurrent().getDataSource().useRepoAsync(BalanceRecordRepository.class, repo -> {
Thread.ofVirtual().start(() -> Profile.getCurrent().getDataSource().useBalanceRecordRepository(repo -> {
List<Attachment> attachments = repo.findAttachments(balanceRecord.id); List<Attachment> attachments = repo.findAttachments(balanceRecord.id);
Platform.runLater(() -> attachmentsViewPane.setAttachments(attachments)); Platform.runLater(() -> attachmentsViewPane.setAttachments(attachments));
})); });
} }
@FXML public void delete() { @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."); 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) { if (confirm) {
Profile.getCurrent().getDataSource().useBalanceRecordRepository(repo -> { Profile.getCurrent().getDataSource().useRepo(BalanceRecordRepository.class, repo -> repo.deleteById(balanceRecord.id));
repo.deleteById(balanceRecord.id);
});
router.navigateBackAndClear(); router.navigateBackAndClear();
} }
} }

View File

@ -1,6 +1,8 @@
package com.andrewlalis.perfin.control; package com.andrewlalis.perfin.control;
import com.andrewlalis.javafx_scene_router.RouteSelectionListener; import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
import com.andrewlalis.perfin.data.AccountRepository;
import com.andrewlalis.perfin.data.BalanceRecordRepository;
import com.andrewlalis.perfin.data.util.CurrencyUtil; import com.andrewlalis.perfin.data.util.CurrencyUtil;
import com.andrewlalis.perfin.data.util.DateUtil; import com.andrewlalis.perfin.data.util.DateUtil;
import com.andrewlalis.perfin.data.util.FileUtil; import com.andrewlalis.perfin.data.util.FileUtil;
@ -59,12 +61,12 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
return; return;
} }
BigDecimal reportedBalance = new BigDecimal(newValue); BigDecimal reportedBalance = new BigDecimal(newValue);
Thread.ofVirtual().start(() -> Profile.getCurrent().getDataSource().useAccountRepository(repo -> { Profile.getCurrent().getDataSource().useRepoAsync(AccountRepository.class, repo -> {
BigDecimal derivedBalance = repo.deriveCurrentBalance(account.id); BigDecimal derivedBalance = repo.deriveCurrentBalance(account.id);
Platform.runLater(() -> balanceWarningLabel.visibleProperty().set( Platform.runLater(() -> balanceWarningLabel.visibleProperty().set(
!reportedBalance.setScale(derivedBalance.scale(), RoundingMode.HALF_UP).equals(derivedBalance) !reportedBalance.setScale(derivedBalance.scale(), RoundingMode.HALF_UP).equals(derivedBalance)
)); ));
})); });
}); });
var formValid = timestampValid.and(balanceValid); var formValid = timestampValid.and(balanceValid);
@ -83,12 +85,12 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
public void onRouteSelected(Object context) { public void onRouteSelected(Object context) {
this.account = (Account) context; this.account = (Account) context;
timestampField.setText(LocalDateTime.now().format(DateUtil.DEFAULT_DATETIME_FORMAT)); timestampField.setText(LocalDateTime.now().format(DateUtil.DEFAULT_DATETIME_FORMAT));
Thread.ofVirtual().start(() -> Profile.getCurrent().getDataSource().useAccountRepository(repo -> { Profile.getCurrent().getDataSource().useRepoAsync(AccountRepository.class, repo -> {
BigDecimal value = repo.deriveCurrentBalance(account.id); BigDecimal value = repo.deriveCurrentBalance(account.id);
Platform.runLater(() -> balanceField.setText( Platform.runLater(() -> balanceField.setText(
CurrencyUtil.formatMoneyAsBasicNumber(new MoneyValue(value, account.getCurrency())) CurrencyUtil.formatMoneyAsBasicNumber(new MoneyValue(value, account.getCurrency()))
)); ));
})); });
attachmentSelectionArea.clear(); attachmentSelectionArea.clear();
} }
@ -102,7 +104,7 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
localTimestamp.atZone(ZoneId.systemDefault()).format(DateUtil.DEFAULT_DATETIME_FORMAT_WITH_ZONE) localTimestamp.atZone(ZoneId.systemDefault()).format(DateUtil.DEFAULT_DATETIME_FORMAT_WITH_ZONE)
)); ));
if (confirm && confirmIfInconsistentBalance(reportedBalance)) { if (confirm && confirmIfInconsistentBalance(reportedBalance)) {
Profile.getCurrent().getDataSource().useBalanceRecordRepository(repo -> { Profile.getCurrent().getDataSource().useRepo(BalanceRecordRepository.class, repo -> {
repo.insert( repo.insert(
DateUtil.localToUTC(localTimestamp), DateUtil.localToUTC(localTimestamp),
account.id, account.id,
@ -120,12 +122,10 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
} }
private boolean confirmIfInconsistentBalance(BigDecimal reportedBalance) { private boolean confirmIfInconsistentBalance(BigDecimal reportedBalance) {
BigDecimal currentDerivedBalance; BigDecimal currentDerivedBalance = Profile.getCurrent().getDataSource().mapRepo(
try (var accountRepo = Profile.getCurrent().getDataSource().getAccountRepository()) { AccountRepository.class,
currentDerivedBalance = accountRepo.deriveCurrentBalance(account.id); repo -> repo.deriveCurrentBalance(account.id)
} catch (Exception e) { );
throw new RuntimeException(e);
}
if (!reportedBalance.setScale(currentDerivedBalance.scale(), RoundingMode.HALF_UP).equals(currentDerivedBalance)) { if (!reportedBalance.setScale(currentDerivedBalance.scale(), RoundingMode.HALF_UP).equals(currentDerivedBalance)) {
String msg = "The balance you reported (%s) doesn't match the balance that Perfin derived from your account's transactions (%s). It's encouraged to go back and add any missing transactions first, but you may proceed now if you understand the consequences of an inconsistent account balance history.\n\nAre you absolutely sure you want to create this balance record?".formatted( String msg = "The balance you reported (%s) doesn't match the balance that Perfin derived from your account's transactions (%s). It's encouraged to go back and add any missing transactions first, but you may proceed now if you understand the consequences of an inconsistent account balance history.\n\nAre you absolutely sure you want to create this balance record?".formatted(
CurrencyUtil.formatMoney(new MoneyValue(reportedBalance, account.getCurrency())), CurrencyUtil.formatMoney(new MoneyValue(reportedBalance, account.getCurrency())),

View File

@ -1,6 +1,7 @@
package com.andrewlalis.perfin.control; package com.andrewlalis.perfin.control;
import com.andrewlalis.javafx_scene_router.RouteSelectionListener; import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
import com.andrewlalis.perfin.data.TransactionRepository;
import com.andrewlalis.perfin.data.pagination.PageRequest; import com.andrewlalis.perfin.data.pagination.PageRequest;
import com.andrewlalis.perfin.data.pagination.Sort; import com.andrewlalis.perfin.data.pagination.Sort;
import com.andrewlalis.perfin.data.util.CurrencyUtil; import com.andrewlalis.perfin.data.util.CurrencyUtil;
@ -104,23 +105,25 @@ public class EditTransactionController implements RouteSelectionListener {
} }
@FXML public void save() { @FXML public void save() {
LocalDateTime utcTimestamp = DateUtil.localToUTC(parseTimestamp()); final long idToNavigate;
BigDecimal amount = new BigDecimal(amountField.getText()); if (transaction == null) {
Currency currency = currencyChoiceBox.getValue(); LocalDateTime utcTimestamp = DateUtil.localToUTC(parseTimestamp());
String description = getSanitizedDescription(); BigDecimal amount = new BigDecimal(amountField.getText());
CreditAndDebitAccounts linkedAccounts = getSelectedAccounts(); Currency currency = currencyChoiceBox.getValue();
List<Path> attachments = attachmentsSelectionArea.getSelectedFiles(); String description = getSanitizedDescription();
Profile.getCurrent().getDataSource().useTransactionRepository(repo -> { CreditAndDebitAccounts linkedAccounts = getSelectedAccounts();
repo.insert( List<Path> attachments = attachmentsSelectionArea.getSelectedFiles();
utcTimestamp, Profile.getCurrent().getDataSource().useRepo(TransactionRepository.class, repo -> {
amount, repo.insert(
currency, utcTimestamp,
description, amount,
linkedAccounts, currency,
attachments description,
); linkedAccounts,
}); attachments
router.navigateBackAndClear(); );
});
}
} }
@FXML public void cancel() { @FXML public void cancel() {

View File

@ -1,5 +1,6 @@
package com.andrewlalis.perfin.control; package com.andrewlalis.perfin.control;
import com.andrewlalis.perfin.data.TransactionRepository;
import com.andrewlalis.perfin.data.util.CurrencyUtil; import com.andrewlalis.perfin.data.util.CurrencyUtil;
import com.andrewlalis.perfin.data.util.DateUtil; import com.andrewlalis.perfin.data.util.DateUtil;
import com.andrewlalis.perfin.model.Attachment; import com.andrewlalis.perfin.model.Attachment;
@ -44,27 +45,19 @@ public class TransactionViewController {
amountLabel.setText(CurrencyUtil.formatMoney(transaction.getMoneyAmount())); amountLabel.setText(CurrencyUtil.formatMoney(transaction.getMoneyAmount()));
timestampLabel.setText(DateUtil.formatUTCAsLocalWithZone(transaction.getTimestamp())); timestampLabel.setText(DateUtil.formatUTCAsLocalWithZone(transaction.getTimestamp()));
descriptionLabel.setText(transaction.getDescription()); descriptionLabel.setText(transaction.getDescription());
Profile.getCurrent().getDataSource().useRepoAsync(TransactionRepository.class, repo -> {
Thread.ofVirtual().start(() -> { CreditAndDebitAccounts accounts = repo.findLinkedAccounts(transaction.id);
Profile.getCurrent().getDataSource().useTransactionRepository(repo -> { List<Attachment> attachments = repo.findAttachments(transaction.id);
CreditAndDebitAccounts accounts = repo.findLinkedAccounts(transaction.id); Platform.runLater(() -> {
Platform.runLater(() -> { accounts.ifDebit(acc -> {
accounts.ifDebit(acc -> { debitAccountLink.setText(acc.getShortName());
debitAccountLink.setText(acc.getShortName()); debitAccountLink.setOnAction(event -> router.navigate("account", acc));
debitAccountLink.setOnAction(event -> router.navigate("account", acc));
});
accounts.ifCredit(acc -> {
creditAccountLink.setText(acc.getShortName());
creditAccountLink.setOnAction(event -> router.navigate("account", acc));
});
}); });
}); accounts.ifCredit(acc -> {
}); creditAccountLink.setText(acc.getShortName());
creditAccountLink.setOnAction(event -> router.navigate("account", acc));
Thread.ofVirtual().start(() -> { });
Profile.getCurrent().getDataSource().useTransactionRepository(repo -> { attachmentsViewPane.setAttachments(attachments);
List<Attachment> attachments = repo.findAttachments(transaction.id);
Platform.runLater(() -> attachmentsViewPane.setAttachments(attachments));
}); });
}); });
} }
@ -84,10 +77,8 @@ public class TransactionViewController {
"it's derived from the most recent balance-record, and transactions." "it's derived from the most recent balance-record, and transactions."
); );
if (confirm) { if (confirm) {
Profile.getCurrent().getDataSource().useTransactionRepository(repo -> { Profile.getCurrent().getDataSource().useRepo(TransactionRepository.class, repo -> repo.delete(transaction.id));
repo.delete(transaction.id); router.replace("transactions");
router.replace("transactions");
});
} }
} }

View File

@ -1,6 +1,8 @@
package com.andrewlalis.perfin.control; package com.andrewlalis.perfin.control;
import com.andrewlalis.javafx_scene_router.RouteSelectionListener; import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
import com.andrewlalis.perfin.data.AccountRepository;
import com.andrewlalis.perfin.data.TransactionRepository;
import com.andrewlalis.perfin.data.pagination.Page; import com.andrewlalis.perfin.data.pagination.Page;
import com.andrewlalis.perfin.data.pagination.PageRequest; import com.andrewlalis.perfin.data.pagination.PageRequest;
import com.andrewlalis.perfin.data.pagination.Sort; import com.andrewlalis.perfin.data.pagination.Sort;
@ -122,27 +124,25 @@ public class TransactionsViewController implements RouteSelectionListener {
transactionsVBox.getChildren().clear(); // Clear the transactions before reload initially. transactionsVBox.getChildren().clear(); // Clear the transactions before reload initially.
// Refresh account filter options. // Refresh account filter options.
Thread.ofVirtual().start(() -> { Profile.getCurrent().getDataSource().useRepoAsync(AccountRepository.class, repo -> {
Profile.getCurrent().getDataSource().useAccountRepository(repo -> { List<Account> accounts = repo.findAll(PageRequest.unpaged(Sort.asc("name"))).items();
List<Account> accounts = repo.findAll(PageRequest.unpaged(Sort.asc("name"))).items(); accounts.add(null);
accounts.add(null); Platform.runLater(() -> {
Platform.runLater(() -> { filterByAccountComboBox.getItems().clear();
filterByAccountComboBox.getItems().clear(); filterByAccountComboBox.getItems().addAll(accounts);
filterByAccountComboBox.getItems().addAll(accounts); filterByAccountComboBox.getSelectionModel().selectLast();
filterByAccountComboBox.getSelectionModel().selectLast(); filterByAccountComboBox.getButtonCell().updateIndex(accounts.size() - 1);
filterByAccountComboBox.getButtonCell().updateIndex(accounts.size() - 1);
});
}); });
}); });
// If a transaction id is given in the route context, navigate to the page it's on and select it. // 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) { if (context instanceof RouteContext ctx && ctx.selectedTransactionId != null) {
Thread.ofVirtual().start(() -> { Profile.getCurrent().getDataSource().useRepoAsync(TransactionRepository.class, repo -> {
Profile.getCurrent().getDataSource().useTransactionRepository(repo -> { repo.findById(ctx.selectedTransactionId).ifPresent(tx -> {
repo.findById(ctx.selectedTransactionId).ifPresent(tx -> { long offset = repo.countAllAfter(tx.id);
long offset = repo.countAllAfter(tx.id); int pageNumber = (int) (offset / paginationControls.getItemsPerPage()) + 1;
int pageNumber = (int) (offset / paginationControls.getItemsPerPage()) + 1; Platform.runLater(() -> {
paginationControls.setPage(pageNumber).thenRun(() -> selectedTransaction.set(tx)); paginationControls.setPage(pageNumber).thenRun(() -> selectedTransaction.set(tx));
}); });
}); });

View File

@ -7,7 +7,7 @@ import java.time.LocalDateTime;
import java.util.Currency; import java.util.Currency;
import java.util.List; import java.util.List;
public interface AccountEntryRepository extends AutoCloseable { public interface AccountEntryRepository extends Repository, AutoCloseable {
long insert( long insert(
LocalDateTime timestamp, LocalDateTime timestamp,
long accountId, long accountId,

View File

@ -9,7 +9,7 @@ import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
public interface AccountHistoryItemRepository extends AutoCloseable { public interface AccountHistoryItemRepository extends Repository, AutoCloseable {
void recordAccountEntry(LocalDateTime timestamp, long accountId, long entryId); void recordAccountEntry(LocalDateTime timestamp, long accountId, long entryId);
void recordBalanceRecord(LocalDateTime timestamp, long accountId, long recordId); void recordBalanceRecord(LocalDateTime timestamp, long accountId, long recordId);
void recordText(LocalDateTime timestamp, long accountId, String text); void recordText(LocalDateTime timestamp, long accountId, String text);

View File

@ -13,7 +13,7 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
public interface AccountRepository extends AutoCloseable { public interface AccountRepository extends Repository, AutoCloseable {
long insert(AccountType type, String accountNumber, String name, Currency currency); long insert(AccountType type, String accountNumber, String name, Currency currency);
Page<Account> findAll(PageRequest pagination); Page<Account> findAll(PageRequest pagination);
List<Account> findAllOrderedByRecentHistory(); List<Account> findAllOrderedByRecentHistory();

View File

@ -5,7 +5,7 @@ import com.andrewlalis.perfin.model.Attachment;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Optional; import java.util.Optional;
public interface AttachmentRepository extends AutoCloseable { public interface AttachmentRepository extends Repository, AutoCloseable {
Attachment insert(Path sourcePath); Attachment insert(Path sourcePath);
Optional<Attachment> findById(long attachmentId); Optional<Attachment> findById(long attachmentId);
Optional<Attachment> findByIdentifier(String identifier); Optional<Attachment> findByIdentifier(String identifier);

View File

@ -10,7 +10,7 @@ import java.util.Currency;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
public interface BalanceRecordRepository extends AutoCloseable { public interface BalanceRecordRepository extends Repository, AutoCloseable {
long insert(LocalDateTime utcTimestamp, long accountId, BigDecimal balance, Currency currency, List<Path> attachments); long insert(LocalDateTime utcTimestamp, long accountId, BigDecimal balance, Currency currency, List<Path> attachments);
BalanceRecord findLatestByAccountId(long accountId); BalanceRecord findLatestByAccountId(long accountId);
Optional<BalanceRecord> findClosestBefore(long accountId, LocalDateTime utcTimestamp); Optional<BalanceRecord> findClosestBefore(long accountId, LocalDateTime utcTimestamp);

View File

@ -2,8 +2,6 @@ package com.andrewlalis.perfin.data;
import com.andrewlalis.perfin.data.pagination.PageRequest; import com.andrewlalis.perfin.data.pagination.PageRequest;
import com.andrewlalis.perfin.data.util.CurrencyUtil; import com.andrewlalis.perfin.data.util.CurrencyUtil;
import com.andrewlalis.perfin.data.util.DbUtil;
import com.andrewlalis.perfin.data.util.ThrowableConsumer;
import com.andrewlalis.perfin.model.Account; import com.andrewlalis.perfin.model.Account;
import com.andrewlalis.perfin.model.AccountType; import com.andrewlalis.perfin.model.AccountType;
import com.andrewlalis.perfin.model.MoneyValue; import com.andrewlalis.perfin.model.MoneyValue;
@ -11,11 +9,11 @@ import javafx.application.Platform;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Currency; import java.util.*;
import java.util.HashMap; import java.util.concurrent.CompletableFuture;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
/** /**
* Interface for methods to obtain any data from a {@link com.andrewlalis.perfin.model.Profile} * Interface for methods to obtain any data from a {@link com.andrewlalis.perfin.model.Profile}
@ -35,30 +33,70 @@ public interface DataSource {
AttachmentRepository getAttachmentRepository(); AttachmentRepository getAttachmentRepository();
AccountHistoryItemRepository getAccountHistoryItemRepository(); AccountHistoryItemRepository getAccountHistoryItemRepository();
default void useAccountRepository(ThrowableConsumer<AccountRepository> repoConsumer) { // Repository helper methods:
DbUtil.useClosable(this::getAccountRepository, repoConsumer);
@SuppressWarnings("unchecked")
default <R extends Repository, T> T mapRepo(Class<R> repoType, Function<R, T> action) {
Supplier<R> repoSupplier = getRepo(repoType);
if (repoSupplier == null) throw new IllegalArgumentException("Repository type " + repoType + " is not supported.");
boolean repoCloseable = Arrays.asList(repoType.getInterfaces()).contains(AutoCloseable.class);
if (repoCloseable) {
try (AutoCloseable c = (AutoCloseable) repoSupplier.get()) {
R repo = (R) c;
return action.apply(repo);
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
R repo = repoSupplier.get();
return action.apply(repo);
}
} }
default void useBalanceRecordRepository(ThrowableConsumer<BalanceRecordRepository> repoConsumer) { default <R extends Repository, T> CompletableFuture<T> mapRepoAsync(Class<R> repoType, Function<R, T> action) {
DbUtil.useClosable(this::getBalanceRecordRepository, repoConsumer); CompletableFuture<T> cf = new CompletableFuture<>();
Thread.ofVirtual().start(() -> {
cf.complete(mapRepo(repoType, action));
});
return cf;
} }
default void useTransactionRepository(ThrowableConsumer<TransactionRepository> repoConsumer) { default <R extends Repository> void useRepo(Class<R> repoType, Consumer<R> action) {
DbUtil.useClosable(this::getTransactionRepository, repoConsumer); mapRepo(repoType, (Function<R, Void>) repo -> {
action.accept(repo);
return null;
});
} }
default void useAttachmentRepository(ThrowableConsumer<AttachmentRepository> repoConsumer) { default <R extends Repository> CompletableFuture<Void> useRepoAsync(Class<R> repoType, Consumer<R> action) {
DbUtil.useClosable(this::getAttachmentRepository, repoConsumer); return mapRepoAsync(repoType, repo -> {
action.accept(repo);
return null;
});
}
@SuppressWarnings("unchecked")
private <R extends Repository> Supplier<R> getRepo(Class<R> type) {
final Map<Class<? extends Repository>, Supplier<? extends Repository>> repoSuppliers = Map.of(
AccountRepository.class, this::getAccountRepository,
BalanceRecordRepository.class, this::getBalanceRecordRepository,
TransactionRepository.class, this::getTransactionRepository,
AttachmentRepository.class, this::getAttachmentRepository,
AccountHistoryItemRepository.class, this::getAccountHistoryItemRepository
);
return (Supplier<R>) repoSuppliers.get(type);
} }
// Utility methods: // Utility methods:
default void getAccountBalanceText(Account account, Consumer<String> balanceConsumer) { default CompletableFuture<String> getAccountBalanceText(Account account) {
Thread.ofVirtual().start(() -> useAccountRepository(repo -> { CompletableFuture<String> cf = new CompletableFuture<>();
mapRepoAsync(AccountRepository.class, repo -> {
BigDecimal balance = repo.deriveCurrentBalance(account.id); BigDecimal balance = repo.deriveCurrentBalance(account.id);
MoneyValue money = new MoneyValue(balance, account.getCurrency()); MoneyValue money = new MoneyValue(balance, account.getCurrency());
Platform.runLater(() -> balanceConsumer.accept(CurrencyUtil.formatMoney(money))); return CurrencyUtil.formatMoney(money);
})); }).thenAccept(s -> Platform.runLater(() -> cf.complete(s)));
return cf;
} }
default Map<Currency, BigDecimal> getCombinedAccountBalances() { default Map<Currency, BigDecimal> getCombinedAccountBalances() {

View File

@ -0,0 +1,4 @@
package com.andrewlalis.perfin.data;
public interface Repository {
}

View File

@ -14,7 +14,7 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
public interface TransactionRepository extends AutoCloseable { public interface TransactionRepository extends Repository, AutoCloseable {
long insert( long insert(
LocalDateTime utcTimestamp, LocalDateTime utcTimestamp,
BigDecimal amount, BigDecimal amount,

View File

@ -1,5 +1,6 @@
package com.andrewlalis.perfin.view.component; package com.andrewlalis.perfin.view.component;
import com.andrewlalis.perfin.data.AccountRepository;
import com.andrewlalis.perfin.data.util.CurrencyUtil; import com.andrewlalis.perfin.data.util.CurrencyUtil;
import com.andrewlalis.perfin.model.Account; import com.andrewlalis.perfin.model.Account;
import com.andrewlalis.perfin.model.MoneyValue; import com.andrewlalis.perfin.model.MoneyValue;
@ -109,13 +110,13 @@ public class AccountSelectionBox extends ComboBox<Account> {
nameLabel.setText(item.getName() + " (" + item.getAccountNumberSuffix() + ")"); nameLabel.setText(item.getName() + " (" + item.getAccountNumberSuffix() + ")");
if (showBalanceProp.get()) { if (showBalanceProp.get()) {
Thread.ofVirtual().start(() -> Profile.getCurrent().getDataSource().useAccountRepository(repo -> { Profile.getCurrent().getDataSource().useRepoAsync(AccountRepository.class, repo -> {
BigDecimal balance = repo.deriveCurrentBalance(item.id); BigDecimal balance = repo.deriveCurrentBalance(item.id);
Platform.runLater(() -> { Platform.runLater(() -> {
balanceLabel.setText(CurrencyUtil.formatMoney(new MoneyValue(balance, item.getCurrency()))); balanceLabel.setText(CurrencyUtil.formatMoney(new MoneyValue(balance, item.getCurrency())));
balanceLabel.setVisible(true); balanceLabel.setVisible(true);
}); });
})); });
} }
} }
} }

View File

@ -1,5 +1,6 @@
package com.andrewlalis.perfin.view.component; package com.andrewlalis.perfin.view.component;
import com.andrewlalis.perfin.data.AccountRepository;
import com.andrewlalis.perfin.data.util.CurrencyUtil; import com.andrewlalis.perfin.data.util.CurrencyUtil;
import com.andrewlalis.perfin.model.Account; import com.andrewlalis.perfin.model.Account;
import com.andrewlalis.perfin.model.AccountType; import com.andrewlalis.perfin.model.AccountType;
@ -80,7 +81,7 @@ public class AccountTile extends BorderPane {
Label balanceLabel = new Label("Computing balance..."); Label balanceLabel = new Label("Computing balance...");
balanceLabel.getStyleClass().addAll("mono-font"); balanceLabel.getStyleClass().addAll("mono-font");
balanceLabel.setDisable(true); balanceLabel.setDisable(true);
Thread.ofVirtual().start(() -> Profile.getCurrent().getDataSource().useAccountRepository(repo -> { Profile.getCurrent().getDataSource().useRepoAsync(AccountRepository.class, repo -> {
BigDecimal balance = repo.deriveCurrentBalance(account.id); BigDecimal balance = repo.deriveCurrentBalance(account.id);
String text = CurrencyUtil.formatMoney(new MoneyValue(balance, account.getCurrency())); String text = CurrencyUtil.formatMoney(new MoneyValue(balance, account.getCurrency()));
Platform.runLater(() -> { Platform.runLater(() -> {
@ -92,10 +93,6 @@ public class AccountTile extends BorderPane {
} }
balanceLabel.setDisable(false); balanceLabel.setDisable(false);
}); });
}));
Profile.getCurrent().getDataSource().getAccountBalanceText(account, text -> {
balanceLabel.setText(text);
balanceLabel.setDisable(false);
}); });
propertiesPane.getChildren().addAll( propertiesPane.getChildren().addAll(

View File

@ -1,5 +1,6 @@
package com.andrewlalis.perfin.view.component; package com.andrewlalis.perfin.view.component;
import com.andrewlalis.perfin.data.TransactionRepository;
import com.andrewlalis.perfin.data.util.CurrencyUtil; import com.andrewlalis.perfin.data.util.CurrencyUtil;
import com.andrewlalis.perfin.data.util.DateUtil; import com.andrewlalis.perfin.data.util.DateUtil;
import com.andrewlalis.perfin.model.CreditAndDebitAccounts; import com.andrewlalis.perfin.model.CreditAndDebitAccounts;
@ -99,13 +100,9 @@ public class TransactionTile extends BorderPane {
} }
private CompletableFuture<CreditAndDebitAccounts> getCreditAndDebitAccounts(Transaction transaction) { private CompletableFuture<CreditAndDebitAccounts> getCreditAndDebitAccounts(Transaction transaction) {
CompletableFuture<CreditAndDebitAccounts> cf = new CompletableFuture<>(); return Profile.getCurrent().getDataSource().mapRepoAsync(
Thread.ofVirtual().start(() -> { TransactionRepository.class,
Profile.getCurrent().getDataSource().useTransactionRepository(repo -> { repo -> repo.findLinkedAccounts(transaction.id)
CreditAndDebitAccounts accounts = repo.findLinkedAccounts(transaction.id); );
cf.complete(accounts);
});
});
return cf;
} }
} }