Add Transaction Properties #15

Merged
andrewlalis merged 18 commits from transaction-properties into main 2024-02-04 04:31:04 +00:00
18 changed files with 183 additions and 146 deletions
Showing only changes of commit 4951b8720d - Show all commits

View File

@ -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<String> 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;

View File

@ -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<AccountHistoryItem> historyItems = repo.findMostRecentForAccount(
account.id,
loadHistoryFrom,

View File

@ -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<Account> 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())));

View File

@ -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<Attachment> 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();
}
}

View File

@ -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)
);

View File

@ -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();

View File

@ -111,7 +111,7 @@ public class EditTransactionController implements RouteSelectionListener {
List<Attachment> 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<Currency> currencies = accountRepo.findAllUsedCurrencies().stream()

View File

@ -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<String> profileNames = Profile.getAvailableProfiles();
String currentProfile = Profile.getCurrent() == null ? null : Profile.getCurrent().getName();
List<String> profileNames = ProfileLoader.getAvailableProfiles();
String currentProfile = Profile.getCurrent() == null ? null : Profile.getCurrent().name();
List<Node> 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();

View File

@ -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<Attachment> 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");
}
}

View File

@ -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<Transaction> 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<Account> 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");

View File

@ -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;
}

View File

@ -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));

View File

@ -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.
* </p>
*/
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<Consumer<Profile>> profileLoadListeners = new ArrayList<>();
private static final Set<WeakReference<Consumer<Profile>>> 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<Profile> consumer = ref.get();
if (consumer != null) {
consumer.accept(profile);
}
}
currentProfileListeners.removeIf(ref -> ref.get() == null);
}
public static void whenLoaded(Consumer<Profile> consumer) {
if (current != null) {
consumer.accept(current);
} else {
profileLoadListeners.add(consumer);
}
}
public static List<String> 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;
}
}

View File

@ -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<String> 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"));
}
}

View File

@ -110,7 +110,7 @@ public class AccountSelectionBox extends ComboBox<Account> {
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())));

View File

@ -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(() -> {

View File

@ -46,7 +46,7 @@ public class AttachmentPreview extends BorderPane {
boolean showDocIcon = true;
Set<String> 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());
}
});

View File

@ -100,7 +100,7 @@ public class TransactionTile extends BorderPane {
}
private CompletableFuture<CreditAndDebitAccounts> getCreditAndDebitAccounts(Transaction transaction) {
return Profile.getCurrent().getDataSource().mapRepoAsync(
return Profile.getCurrent().dataSource().mapRepoAsync(
TransactionRepository.class,
repo -> repo.findLinkedAccounts(transaction.id)
);