diff --git a/pom.xml b/pom.xml
index 936b884..07d9d7d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -50,6 +50,7 @@
2.2.224
+
org.junit.jupiter
junit-jupiter-api
@@ -62,6 +63,19 @@
5.10.0
test
+
+
+
+ org.slf4j
+ slf4j-api
+ 2.0.10
+
+
+ ch.qos.logback
+ logback-classic
+ 1.4.12
+ runtime
+
diff --git a/src/main/java/com/andrewlalis/perfin/PerfinApp.java b/src/main/java/com/andrewlalis/perfin/PerfinApp.java
index 54426b4..e10b067 100644
--- a/src/main/java/com/andrewlalis/perfin/PerfinApp.java
+++ b/src/main/java/com/andrewlalis/perfin/PerfinApp.java
@@ -2,6 +2,8 @@ package com.andrewlalis.perfin;
import com.andrewlalis.javafx_scene_router.AnchorPaneRouterView;
import com.andrewlalis.javafx_scene_router.SceneRouter;
+import com.andrewlalis.perfin.control.Popups;
+import com.andrewlalis.perfin.data.DataSourceInitializationException;
import com.andrewlalis.perfin.model.Profile;
import com.andrewlalis.perfin.view.ImageCache;
import com.andrewlalis.perfin.view.SceneUtil;
@@ -89,6 +91,11 @@ public class PerfinApp extends Application {
private static void loadLastUsedProfile(Consumer msgConsumer) throws Exception {
msgConsumer.accept("Loading the most recent profile.");
- Profile.loadLast();
+ try {
+ Profile.loadLast();
+ } catch (DataSourceInitializationException e) {
+ Popups.error(e.getMessage());
+ throw e;
+ }
}
}
\ No newline at end of file
diff --git a/src/main/java/com/andrewlalis/perfin/control/AccountsViewController.java b/src/main/java/com/andrewlalis/perfin/control/AccountsViewController.java
index 17fdfe4..0228df5 100644
--- a/src/main/java/com/andrewlalis/perfin/control/AccountsViewController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/AccountsViewController.java
@@ -1,11 +1,12 @@
package com.andrewlalis.perfin.control;
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
-import com.andrewlalis.perfin.view.component.AccountTile;
-import com.andrewlalis.perfin.data.util.CurrencyUtil;
import com.andrewlalis.perfin.data.pagination.PageRequest;
import com.andrewlalis.perfin.data.pagination.Sort;
+import com.andrewlalis.perfin.data.util.CurrencyUtil;
+import com.andrewlalis.perfin.model.MoneyValue;
import com.andrewlalis.perfin.model.Profile;
+import com.andrewlalis.perfin.view.component.AccountTile;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
@@ -13,10 +14,6 @@ import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
-import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.util.Currency;
-
import static com.andrewlalis.perfin.PerfinApp.router;
public class AccountsViewController implements RouteSelectionListener {
@@ -63,9 +60,7 @@ public class AccountsViewController implements RouteSelectionListener {
var totals = profile.getDataSource().getCombinedAccountBalances();
StringBuilder sb = new StringBuilder("Totals: ");
for (var entry : totals.entrySet()) {
- Currency cur = entry.getKey();
- BigDecimal value = entry.getValue().setScale(cur.getDefaultFractionDigits(), RoundingMode.HALF_UP);
- sb.append(cur.getCurrencyCode()).append(' ').append(CurrencyUtil.formatMoney(value, cur)).append(' ');
+ sb.append(CurrencyUtil.formatMoneyWithCurrencyPrefix(new MoneyValue(entry.getValue(), entry.getKey())));
}
Platform.runLater(() -> totalLabel.setText(sb.toString().strip()));
});
diff --git a/src/main/java/com/andrewlalis/perfin/control/CreateBalanceRecordController.java b/src/main/java/com/andrewlalis/perfin/control/CreateBalanceRecordController.java
index da48bed..c94a13d 100644
--- a/src/main/java/com/andrewlalis/perfin/control/CreateBalanceRecordController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/CreateBalanceRecordController.java
@@ -5,6 +5,7 @@ import com.andrewlalis.perfin.data.util.CurrencyUtil;
import com.andrewlalis.perfin.data.util.DateUtil;
import com.andrewlalis.perfin.data.util.FileUtil;
import com.andrewlalis.perfin.model.Account;
+import com.andrewlalis.perfin.model.MoneyValue;
import com.andrewlalis.perfin.model.Profile;
import com.andrewlalis.perfin.view.component.FileSelectionArea;
import javafx.application.Platform;
@@ -39,7 +40,7 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
Profile.getCurrent().getDataSource().useAccountRepository(repo -> {
BigDecimal value = repo.deriveCurrentBalance(account.getId());
Platform.runLater(() -> balanceField.setText(
- CurrencyUtil.formatMoneyAsBasicNumber(value, account.getCurrency())
+ CurrencyUtil.formatMoneyAsBasicNumber(new MoneyValue(value, account.getCurrency()))
));
});
});
diff --git a/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java b/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java
index 21507d9..1027a26 100644
--- a/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java
@@ -1,6 +1,7 @@
package com.andrewlalis.perfin.control;
import com.andrewlalis.perfin.PerfinApp;
+import com.andrewlalis.perfin.data.DataSourceInitializationException;
import com.andrewlalis.perfin.data.util.FileUtil;
import com.andrewlalis.perfin.model.Profile;
import com.andrewlalis.perfin.view.ProfilesStage;
@@ -124,6 +125,9 @@ public class ProfilesViewController {
e.printStackTrace(System.err);
Popups.error("Failed to load profile: " + e.getMessage());
return false;
+ } catch (DataSourceInitializationException e) {
+ Popups.error("Failed to initialize the profile's data: " + e.getMessage());
+ return false;
}
}
diff --git a/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java b/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java
index 3aae013..d7b738c 100644
--- a/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java
@@ -44,7 +44,7 @@ public class TransactionViewController {
this.transaction = transaction;
if (transaction == null) return;
titleLabel.setText("Transaction #" + transaction.getId());
- amountLabel.setText(CurrencyUtil.formatMoney(transaction.getAmount(), transaction.getCurrency()));
+ amountLabel.setText(CurrencyUtil.formatMoney(transaction.getMoneyAmount()));
timestampLabel.setText(DateUtil.formatUTCAsLocalWithZone(transaction.getTimestamp()));
descriptionLabel.setText(transaction.getDescription());
diff --git a/src/main/java/com/andrewlalis/perfin/data/DataSource.java b/src/main/java/com/andrewlalis/perfin/data/DataSource.java
index 2956c11..c35366f 100644
--- a/src/main/java/com/andrewlalis/perfin/data/DataSource.java
+++ b/src/main/java/com/andrewlalis/perfin/data/DataSource.java
@@ -5,6 +5,7 @@ import com.andrewlalis.perfin.data.util.CurrencyUtil;
import com.andrewlalis.perfin.data.util.DbUtil;
import com.andrewlalis.perfin.data.util.ThrowableConsumer;
import com.andrewlalis.perfin.model.Account;
+import com.andrewlalis.perfin.model.MoneyValue;
import javafx.application.Platform;
import java.math.BigDecimal;
@@ -52,12 +53,11 @@ public interface DataSource {
// Utility methods:
default void getAccountBalanceText(Account account, Consumer balanceConsumer) {
- Thread.ofVirtual().start(() -> {
- useAccountRepository(repo -> {
- BigDecimal balance = repo.deriveCurrentBalance(account.getId());
- Platform.runLater(() -> balanceConsumer.accept(CurrencyUtil.formatMoney(balance, account.getCurrency())));
- });
- });
+ Thread.ofVirtual().start(() -> useAccountRepository(repo -> {
+ BigDecimal balance = repo.deriveCurrentBalance(account.getId());
+ MoneyValue money = new MoneyValue(balance, account.getCurrency());
+ Platform.runLater(() -> balanceConsumer.accept(CurrencyUtil.formatMoney(money)));
+ }));
}
default Map getCombinedAccountBalances() {
diff --git a/src/main/java/com/andrewlalis/perfin/data/DataSourceInitializationException.java b/src/main/java/com/andrewlalis/perfin/data/DataSourceInitializationException.java
new file mode 100644
index 0000000..3c8305a
--- /dev/null
+++ b/src/main/java/com/andrewlalis/perfin/data/DataSourceInitializationException.java
@@ -0,0 +1,11 @@
+package com.andrewlalis.perfin.data;
+
+public class DataSourceInitializationException extends Exception {
+ public DataSourceInitializationException(String message) {
+ super(message);
+ }
+
+ public DataSourceInitializationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcDataSourceFactory.java b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcDataSourceFactory.java
new file mode 100644
index 0000000..f1a7201
--- /dev/null
+++ b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcDataSourceFactory.java
@@ -0,0 +1,130 @@
+package com.andrewlalis.perfin.data.impl;
+
+import com.andrewlalis.perfin.data.DataSource;
+import com.andrewlalis.perfin.data.DataSourceInitializationException;
+import com.andrewlalis.perfin.data.util.FileUtil;
+import com.andrewlalis.perfin.model.Profile;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Component that's responsible for obtaining a JDBC data source for a profile.
+ */
+public class JdbcDataSourceFactory {
+ private static final Logger log = LoggerFactory.getLogger(JdbcDataSourceFactory.class);
+
+ /**
+ * The version of schema that this app is compatible with. If a profile is
+ * loaded with an old schema version, then we'll migrate to the latest. If
+ * the profile has a newer schema version, we'll exit and prompt the user
+ * to update their app.
+ */
+ public static final int SCHEMA_VERSION = 1;
+
+ public DataSource getDataSource(String profileName) throws DataSourceInitializationException {
+ final boolean dbExists = Files.exists(getDatabaseFile(profileName));
+ if (!dbExists) {
+ log.info("Creating new database for profile {}.", profileName);
+ createNewDatabase(profileName);
+ } else {
+ int loadedSchemaVersion;
+ try {
+ loadedSchemaVersion = getSchemaVersion(profileName);
+ } catch (IOException e) {
+ log.error("Failed to load schema version.", e);
+ throw new DataSourceInitializationException("Failed to determine database schema version.", e);
+ }
+ log.debug("Database loaded for profile {} has schema version {}.", profileName, loadedSchemaVersion);
+ if (loadedSchemaVersion < SCHEMA_VERSION) {
+ log.debug("Schema version {} is lower than the app's version {}. Performing migration.", loadedSchemaVersion, SCHEMA_VERSION);
+ // TODO: Do migration
+ } else if (loadedSchemaVersion > SCHEMA_VERSION) {
+ log.debug("Schema version {} is higher than the app's version {}. Cannot continue.", loadedSchemaVersion, SCHEMA_VERSION);
+ throw new DataSourceInitializationException("Profile " + profileName + " has a database with an unsupported schema version.");
+ }
+ }
+ return new JdbcDataSource(getJdbcUrl(profileName), Profile.getContentDir(profileName));
+ }
+
+ private void createNewDatabase(String profileName) throws DataSourceInitializationException {
+ log.info("Creating new database for profile {}.", profileName);
+ JdbcDataSource dataSource = new JdbcDataSource(getJdbcUrl(profileName), Profile.getContentDir(profileName));
+ try (
+ InputStream in = JdbcDataSourceFactory.class.getResourceAsStream("/sql/schema.sql");
+ Connection conn = dataSource.getConnection()
+ ) {
+ if (in == null) throw new IOException("Could not load database schema SQL file.");
+ String schemaStr = new String(in.readAllBytes(), StandardCharsets.UTF_8);
+ List statements = Arrays.stream(schemaStr.split(";"))
+ .map(String::strip).filter(s -> !s.isBlank()).toList();
+ for (String statementText : statements) {
+ try (Statement stmt = conn.createStatement()) {
+ stmt.executeUpdate(statementText);
+ }
+ }
+ try {
+ writeCurrentSchemaVersion(profileName);
+ } catch (IOException e) {
+ log.warn("Failed to write current schema version to file.", e);
+ }
+ } catch (IOException e) {
+ log.error("IO Exception when trying to create database.", e);
+ FileUtil.deleteIfPossible(getDatabaseFile(profileName));
+ throw new DataSourceInitializationException("Failed to read SQL data to create database schema.", e);
+ } catch (SQLException e) {
+ log.error("SQL Exception when trying to create database.", e);
+ FileUtil.deleteIfPossible(getDatabaseFile(profileName));
+ throw new DataSourceInitializationException("Failed to create the database due to an SQL error.", e);
+ }
+ if (!testConnection(dataSource)) {
+ FileUtil.deleteIfPossible(getDatabaseFile(profileName));
+ throw new DataSourceInitializationException("Testing the database connection failed.");
+ }
+ }
+
+ private boolean testConnection(JdbcDataSource dataSource) {
+ try (var conn = dataSource.getConnection(); var stmt = conn.createStatement()) {
+ return stmt.execute("SELECT 1;");
+ } catch (SQLException e) {
+ log.error("JDBC database connection failed.", e);
+ return false;
+ }
+ }
+
+ private static Path getDatabaseFile(String profileName) {
+ return Profile.getDir(profileName).resolve("database.mv.db");
+ }
+
+ private static String getJdbcUrl(String profileName) {
+ String dbPathAbs = getDatabaseFile(profileName).toAbsolutePath().toString();
+ return "jdbc:h2:" + dbPathAbs.substring(0, dbPathAbs.length() - 6);
+ }
+
+ private static Path getSchemaVersionFile(String profileName) {
+ return Profile.getDir(profileName).resolve(".jdbc-schema-version.txt");
+ }
+
+ private static int getSchemaVersion(String profileName) throws IOException {
+ if (Files.exists(getSchemaVersionFile(profileName))) {
+ return Integer.parseInt(Files.readString(getSchemaVersionFile(profileName)));
+ } else {
+ writeCurrentSchemaVersion(profileName);
+ return SCHEMA_VERSION;
+ }
+ }
+
+ private static void writeCurrentSchemaVersion(String profileName) throws IOException {
+ Files.writeString(getSchemaVersionFile(profileName), Integer.toString(SCHEMA_VERSION));
+ }
+}
diff --git a/src/main/java/com/andrewlalis/perfin/data/util/CurrencyUtil.java b/src/main/java/com/andrewlalis/perfin/data/util/CurrencyUtil.java
index 3f7a1dc..010482e 100644
--- a/src/main/java/com/andrewlalis/perfin/data/util/CurrencyUtil.java
+++ b/src/main/java/com/andrewlalis/perfin/data/util/CurrencyUtil.java
@@ -1,22 +1,27 @@
package com.andrewlalis.perfin.data.util;
+import com.andrewlalis.perfin.model.MoneyValue;
+
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.NumberFormat;
-import java.util.Currency;
public class CurrencyUtil {
- public static String formatMoney(BigDecimal amount, Currency currency) {
+ public static String formatMoney(MoneyValue money) {
NumberFormat nf = NumberFormat.getCurrencyInstance();
- nf.setCurrency(currency);
- nf.setMaximumFractionDigits(currency.getDefaultFractionDigits());
- nf.setMinimumFractionDigits(currency.getDefaultFractionDigits());
- BigDecimal displayValue = amount.setScale(currency.getDefaultFractionDigits(), RoundingMode.HALF_UP);
+ nf.setCurrency(money.currency());
+ nf.setMaximumFractionDigits(money.currency().getDefaultFractionDigits());
+ nf.setMinimumFractionDigits(money.currency().getDefaultFractionDigits());
+ BigDecimal displayValue = money.amount().setScale(money.currency().getDefaultFractionDigits(), RoundingMode.HALF_UP);
return nf.format(displayValue);
}
- public static String formatMoneyAsBasicNumber(BigDecimal amount, Currency currency) {
- BigDecimal displayValue = amount.setScale(currency.getDefaultFractionDigits(), RoundingMode.HALF_UP);
+ public static String formatMoneyWithCurrencyPrefix(MoneyValue money) {
+ return money.currency().getCurrencyCode() + ' ' + formatMoney(money);
+ }
+
+ public static String formatMoneyAsBasicNumber(MoneyValue money) {
+ BigDecimal displayValue = money.amount().setScale(money.currency().getDefaultFractionDigits(), RoundingMode.HALF_UP);
return displayValue.toString();
}
}
diff --git a/src/main/java/com/andrewlalis/perfin/data/util/FileUtil.java b/src/main/java/com/andrewlalis/perfin/data/util/FileUtil.java
index 8f811f7..540481d 100644
--- a/src/main/java/com/andrewlalis/perfin/data/util/FileUtil.java
+++ b/src/main/java/com/andrewlalis/perfin/data/util/FileUtil.java
@@ -1,6 +1,8 @@
package com.andrewlalis.perfin.data.util;
import javafx.stage.FileChooser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.FileVisitResult;
@@ -12,6 +14,8 @@ import java.util.HashMap;
import java.util.Map;
public class FileUtil {
+ private static final Logger log = LoggerFactory.getLogger(FileUtil.class);
+
public static Map MIMETYPES = new HashMap<>();
static {
MIMETYPES.put(".pdf", "application/pdf");
@@ -31,6 +35,14 @@ public class FileUtil {
MIMETYPES.put(".tiff", "image/tiff");
}
+ public static void deleteIfPossible(Path file) {
+ try {
+ Files.deleteIfExists(file);
+ } catch (IOException e) {
+ log.error("Failed to delete file " + file, e);
+ }
+ }
+
public static void deleteDirRecursive(Path startDir) throws IOException {
Files.walkFileTree(startDir, new SimpleFileVisitor<>() {
@Override
diff --git a/src/main/java/com/andrewlalis/perfin/model/AccountEntry.java b/src/main/java/com/andrewlalis/perfin/model/AccountEntry.java
index fbf34e0..74d3001 100644
--- a/src/main/java/com/andrewlalis/perfin/model/AccountEntry.java
+++ b/src/main/java/com/andrewlalis/perfin/model/AccountEntry.java
@@ -85,4 +85,8 @@ public class AccountEntry {
public BigDecimal getSignedAmount() {
return type == Type.DEBIT ? amount : amount.negate();
}
+
+ public MoneyValue getMoneyValue() {
+ return new MoneyValue(amount, currency);
+ }
}
diff --git a/src/main/java/com/andrewlalis/perfin/model/BalanceRecord.java b/src/main/java/com/andrewlalis/perfin/model/BalanceRecord.java
index f5d795d..ed3b6fe 100644
--- a/src/main/java/com/andrewlalis/perfin/model/BalanceRecord.java
+++ b/src/main/java/com/andrewlalis/perfin/model/BalanceRecord.java
@@ -44,4 +44,8 @@ public class BalanceRecord {
public Currency getCurrency() {
return currency;
}
+
+ public MoneyValue getMoneyAmount() {
+ return new MoneyValue(balance, currency);
+ }
}
diff --git a/src/main/java/com/andrewlalis/perfin/model/MoneyValue.java b/src/main/java/com/andrewlalis/perfin/model/MoneyValue.java
new file mode 100644
index 0000000..1115bc5
--- /dev/null
+++ b/src/main/java/com/andrewlalis/perfin/model/MoneyValue.java
@@ -0,0 +1,11 @@
+package com.andrewlalis.perfin.model;
+
+import java.math.BigDecimal;
+import java.util.Currency;
+
+/**
+ * An amount of money of a certain currency.
+ * @param amount The amount of money.
+ * @param currency The currency of the money.
+ */
+public record MoneyValue(BigDecimal amount, Currency currency) {}
diff --git a/src/main/java/com/andrewlalis/perfin/model/Profile.java b/src/main/java/com/andrewlalis/perfin/model/Profile.java
index 83e9ad7..dc380c2 100644
--- a/src/main/java/com/andrewlalis/perfin/model/Profile.java
+++ b/src/main/java/com/andrewlalis/perfin/model/Profile.java
@@ -2,14 +2,18 @@ package com.andrewlalis.perfin.model;
import com.andrewlalis.perfin.PerfinApp;
import com.andrewlalis.perfin.data.DataSource;
-import com.andrewlalis.perfin.data.impl.JdbcDataSource;
+import com.andrewlalis.perfin.data.DataSourceInitializationException;
+import com.andrewlalis.perfin.data.impl.JdbcDataSourceFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.IOException;
-import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.sql.SQLException;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Properties;
import java.util.function.Consumer;
/**
@@ -30,6 +34,8 @@ import java.util.function.Consumer;
*
*/
public class Profile {
+ private static final Logger log = LoggerFactory.getLogger(Profile.class);
+
private static Profile current;
private static final List> profileLoadListeners = new ArrayList<>();
@@ -67,10 +73,6 @@ public class Profile {
return getDir(name).resolve("settings.properties");
}
- public static Path getDatabaseFile(String name) {
- return getDir(name).resolve("database.mv.db");
- }
-
public static Profile getCurrent() {
return current;
}
@@ -89,6 +91,7 @@ public class Profile {
.map(path -> path.getFileName().toString())
.sorted().toList();
} catch (IOException e) {
+ log.error("Failed to get a list of available profiles.", e);
return Collections.emptyList();
}
}
@@ -100,8 +103,7 @@ public class Profile {
String s = Files.readString(lastProfileFile).strip().toLowerCase();
if (!s.isBlank()) return s;
} catch (IOException e) {
- System.err.println("Failed to read " + lastProfileFile);
- e.printStackTrace(System.err);
+ log.error("Failed to read " + lastProfileFile, e);
}
}
return "default";
@@ -112,16 +114,15 @@ public class Profile {
try {
Files.writeString(lastProfileFile, name);
} catch (IOException e) {
- System.err.println("Failed to write " + lastProfileFile);
- e.printStackTrace(System.err);
+ log.error("Failed to write " + lastProfileFile, e);
}
}
- public static void loadLast() throws Exception {
+ public static void loadLast() throws IOException, DataSourceInitializationException {
load(getLastProfile());
}
- public static void load(String name) throws IOException {
+ public static void load(String name) throws IOException, DataSourceInitializationException {
if (Files.notExists(getDir(name))) {
initProfileDir(name);
}
@@ -129,7 +130,7 @@ public class Profile {
try (var in = Files.newInputStream(getSettingsFile(name))) {
settings.load(in);
}
- current = new Profile(name, settings, initJdbcDataSource(name));
+ current = new Profile(name, settings, new JdbcDataSourceFactory().getDataSource(name));
saveLastProfile(current.getName());
for (var c : profileLoadListeners) {
c.accept(current);
@@ -144,38 +145,6 @@ public class Profile {
copyResourceFile("/text/contentDirReadme.txt", getContentDir(name).resolve("README.txt"));
}
- private static DataSource initJdbcDataSource(String name) throws IOException {
- String databaseFilename = getDatabaseFile(name).toAbsolutePath().toString();
- String jdbcUrl = "jdbc:h2:" + databaseFilename.substring(0, databaseFilename.length() - 6);
- boolean exists = Files.exists(getDatabaseFile(name));
- JdbcDataSource dataSource = new JdbcDataSource(jdbcUrl, getContentDir(name));
- if (!exists) {// Initialize the datasource using schema.sql.
- try (var in = Profile.class.getResourceAsStream("/sql/schema.sql"); var conn = dataSource.getConnection()) {
- if (in == null) throw new IOException("Could not load /sql/schema.sql");
- String schemaStr = new String(in.readAllBytes(), StandardCharsets.UTF_8);
- List statements = Arrays.stream(schemaStr.split(";"))
- .map(String::strip).filter(s -> !s.isBlank()).toList();
- for (var statementStr : statements) {
- try (var stmt = conn.createStatement()) {
- stmt.executeUpdate(statementStr);
- System.out.println("Executed update:\n" + statementStr + "\n-----");
- }
- }
- } catch (SQLException e) {
- Files.deleteIfExists(getDatabaseFile(name));
- throw new IOException("Failed to initialize database.", e);
- }
- }
- // Test the datasource before returning it.
- try (var conn = dataSource.getConnection(); var s = conn.createStatement()) {
- boolean success = s.execute("SELECT 1;");
- if (!success) throw new IOException("Failed to execute DB test statement.");
- } catch (SQLException e) {
- throw new IOException(e);
- }
- return dataSource;
- }
-
private static void copyResourceFile(String resource, Path dest) throws IOException {
try (
var in = Profile.class.getResourceAsStream(resource);
diff --git a/src/main/java/com/andrewlalis/perfin/model/Transaction.java b/src/main/java/com/andrewlalis/perfin/model/Transaction.java
index d3704c7..78627f8 100644
--- a/src/main/java/com/andrewlalis/perfin/model/Transaction.java
+++ b/src/main/java/com/andrewlalis/perfin/model/Transaction.java
@@ -46,6 +46,10 @@ public class Transaction {
return description;
}
+ public MoneyValue getMoneyAmount() {
+ return new MoneyValue(amount, currency);
+ }
+
@Override
public boolean equals(Object other) {
return other instanceof Transaction tx && id == tx.id;
diff --git a/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryItemTile.java b/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryItemTile.java
index 84bc8bb..9f4f5e7 100644
--- a/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryItemTile.java
+++ b/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryItemTile.java
@@ -42,7 +42,7 @@ public class AccountHistoryItemTile extends BorderPane {
}
private Node buildAccountEntryItem(AccountEntry entry) {
- Text amountText = new Text(CurrencyUtil.formatMoney(entry.getSignedAmount(), entry.getCurrency()));
+ Text amountText = new Text(CurrencyUtil.formatMoneyWithCurrencyPrefix(entry.getMoneyValue()));
Hyperlink transactionLink = new Hyperlink("Transaction #" + entry.getTransactionId());
transactionLink.setOnAction(event -> router.navigate(
"transactions",
@@ -56,7 +56,7 @@ public class AccountHistoryItemTile extends BorderPane {
}
private Node buildBalanceRecordItem(BalanceRecord balanceRecord) {
- Text amountText = new Text(CurrencyUtil.formatMoney(balanceRecord.getBalance(), balanceRecord.getCurrency()));
- return new TextFlow(new Text("Balance record added with value of "), amountText);
+ Text amountText = new Text(CurrencyUtil.formatMoneyWithCurrencyPrefix(balanceRecord.getMoneyAmount()));
+ return new TextFlow(new Text("Balance record #" + balanceRecord.getId() + " added with value of "), amountText);
}
}
diff --git a/src/main/java/com/andrewlalis/perfin/view/component/DataSourcePaginationControls.java b/src/main/java/com/andrewlalis/perfin/view/component/DataSourcePaginationControls.java
index 427122e..f2944c2 100644
--- a/src/main/java/com/andrewlalis/perfin/view/component/DataSourcePaginationControls.java
+++ b/src/main/java/com/andrewlalis/perfin/view/component/DataSourcePaginationControls.java
@@ -57,7 +57,6 @@ public class DataSourcePaginationControls extends BorderPane {
pageText.setTextAlignment(TextAlignment.CENTER);
BorderPane pageTextContainer = new BorderPane(pageText);
BorderPane.setAlignment(pageText, Pos.CENTER);
- pageTextContainer.setStyle("-fx-border-color: blue;");
Button previousPageButton = new Button("Previous Page");
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 833881a..9978d0a 100644
--- a/src/main/java/com/andrewlalis/perfin/view/component/TransactionTile.java
+++ b/src/main/java/com/andrewlalis/perfin/view/component/TransactionTile.java
@@ -55,7 +55,7 @@ public class TransactionTile extends BorderPane {
}
private Node getHeader(Transaction transaction) {
- Label currencyLabel = new Label(CurrencyUtil.formatMoney(transaction.getAmount(), transaction.getCurrency()));
+ Label currencyLabel = new Label(CurrencyUtil.formatMoney(transaction.getMoneyAmount()));
currencyLabel.setStyle("-fx-font-family: monospace;");
HBox headerHBox = new HBox(
currencyLabel
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index f70e4dc..21d0a50 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -9,6 +9,8 @@ module com.andrewlalis.perfin {
requires java.sql;
+ requires org.slf4j;
+
exports com.andrewlalis.perfin to javafx.graphics;
exports com.andrewlalis.perfin.view to javafx.graphics;
exports com.andrewlalis.perfin.model to javafx.graphics;