Add Transaction Properties #15
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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())));
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -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())));
|
||||
|
|
|
@ -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(() -> {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue