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