- added logger
- restructured internal dialogs
This commit is contained in:
Sven Vogel 2023-07-11 20:43:40 +02:00
parent 0840db44c2
commit 2a0d97c834
19 changed files with 254 additions and 56 deletions

View File

@ -0,0 +1,63 @@
package me.teridax.jcash;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.logging.*;
public final class Logging {
public static final Logger LOGGER = Logger.getLogger(Logging.class.getName());
private static final String LOG_FOLDER_NAME = "logs/";
/**
* Initialize the global system logger.
* Adds a file logging handler
*/
static void initializeSystemLogger(Level level) {
LogManager.getLogManager().reset();
createConsoleLogger(level);
createFileLogger(level);
LOGGER.setLevel(level);
}
private static void createConsoleLogger(Level level) {
var ch = new ConsoleHandler();
ch.setLevel(level);
LOGGER.addHandler(ch);
}
private static void createFileLogger(Level level) {
var now = LocalDateTime.now();
var dateTime = DateTimeFormatter.ISO_DATE_TIME.format(now);
var logFileName = LOG_FOLDER_NAME + dateTime + ".log";
initializeLogFolder();
try {
var fh = new FileHandler(logFileName);
fh.setLevel(level);
LOGGER.addHandler(fh);
} catch (IOException e) {
LOGGER.warning("Unable to initialize logging for file: " + logFileName);
}
}
private static void initializeLogFolder() {
var folderPath = Path.of(LOG_FOLDER_NAME);
if (Files.isDirectory(folderPath))
return;
try {
Files.createDirectory(folderPath);
} catch (IOException e) {
LOGGER.warning("Unable to create directory: " + folderPath);
}
}
}

View File

@ -5,6 +5,13 @@ import me.teridax.jcash.gui.account.AccountController;
import me.teridax.jcash.gui.login.LoginController;
import javax.swing.*;
import java.util.Objects;
import java.util.logging.*;
import static javax.swing.JOptionPane.ERROR_MESSAGE;
import static javax.swing.JOptionPane.showMessageDialog;
import static me.teridax.jcash.Logging.LOGGER;
import static me.teridax.jcash.Logging.initializeSystemLogger;
public final class Main {
@ -27,6 +34,8 @@ public final class Main {
}
public static void main(String[] args) {
initializeSystemLogger(Level.FINE);
// create main instance and show the login screen
instance();
getInstance().showLoginScreen();
@ -51,6 +60,8 @@ public final class Main {
if (null != instance)
throw new IllegalStateException(Main.class.getName() + " is already initialized");
LOGGER.fine("Creating singleton instance of class " + Main.class.getName());
Main.instance = new Main();
}
@ -61,6 +72,8 @@ public final class Main {
*/
public void showLoginScreen() {
SwingUtilities.invokeLater(() -> {
LOGGER.finer("showing login screen");
try {
// select db file
var path = Loader.load();
@ -69,6 +82,8 @@ public final class Main {
// when we have logged in set the account viewer as window content
login.addAccountSelectionListener(account -> {
LOGGER.finer("account selected: " + Objects.toString(account, "null"));
var profileCont = new AccountController(account, login.getData().getBms());
this.window.setContentPane(profileCont.getView());
this.window.revalidate();
@ -81,7 +96,8 @@ public final class Main {
this.window.setVisible(true);
} catch (IllegalStateException e) {
System.out.println("no file selected. goodbye");
LOGGER.fine("Unable to show login mask: " + e.getMessage());
showMessageDialog(null, e.getMessage(), "Closing JCash", ERROR_MESSAGE);
System.exit(0);
}
});

View File

@ -1,5 +1,6 @@
package me.teridax.jcash.banking;
import me.teridax.jcash.Logging;
import me.teridax.jcash.banking.accounts.Account;
import me.teridax.jcash.banking.accounts.Owner;
import me.teridax.jcash.banking.management.Profile;
@ -107,6 +108,7 @@ public final class Bank {
}
}
}
Logging.LOGGER.finer("Account not found: " + iban);
return Optional.empty();
}
}

View File

@ -1,5 +1,6 @@
package me.teridax.jcash.banking.accounts;
import me.teridax.jcash.Logging;
import me.teridax.jcash.decode.StringDecoder;
import java.util.Objects;
@ -40,6 +41,9 @@ public abstract class Account {
*/
public static Account fromColumns(String[] columns) throws IllegalArgumentException, NullPointerException {
Objects.requireNonNull(columns);
Logging.LOGGER.finer("Parsing account from columns");
Logging.LOGGER.finer("Decoding account fields");
// deserialize fields
var iban = StringDecoder.decodeUniqueIdentificationNumber(columns[0]);
@ -50,15 +54,20 @@ public abstract class Account {
// try to detect the specific runtime class to deserialize
try {
if (type.equals("Sparkonto")) {
Logging.LOGGER.fine("Account detected as Sparkonto");
var interest = StringDecoder.decodePercent(columns[4]);
return new SavingsAccount(iban, pin, balance, interest);
} else if (type.equals("Girokonto")) {
Logging.LOGGER.fine("Account detected as Girokonto");
var overdraft = StringDecoder.decodeCurrency(columns[5]);
return new CurrentAccount(iban, pin, balance, overdraft);
} else
} else {
Logging.LOGGER.severe("Account type could not be detected");
throw new IllegalArgumentException("Invalid account type: " + type);
}
} catch (IllegalArgumentException | NullPointerException e) {
Logging.LOGGER.severe("Account field could not be decoded: " + e.getMessage());
throw new IllegalArgumentException("Account format: ", e);
}
}
@ -110,8 +119,10 @@ public abstract class Account {
* @throws IllegalArgumentException if amount is negative
*/
public void deposit(double amount) throws IllegalArgumentException {
if (amount < 0)
if (amount < 0) {
Logging.LOGGER.severe("Cannot deposit negative amount of money: " + amount);
throw new IllegalArgumentException("amount must be positive");
}
this.balance += amount;
}
@ -128,8 +139,10 @@ public abstract class Account {
* @throws IllegalArgumentException if amount is greater than the balance present
*/
public void takeoff(double amount) throws IllegalArgumentException {
if (amount > this.balance)
if (amount > this.balance) {
Logging.LOGGER.severe("Cannot take off more money than present in balance: " + amount);
throw new IllegalArgumentException("amount must be smaller or equals the accounts balance");
}
this.balance = this.balance - amount;
}

View File

@ -1,5 +1,7 @@
package me.teridax.jcash.banking.accounts;
import me.teridax.jcash.Logging;
/**
* Immutable currency account storing only overdraft.
* English equivalent to "Girokonto"
@ -30,9 +32,12 @@ public final class CurrentAccount extends Account {
*/
@Override
public void takeoff(double amount) throws IllegalArgumentException {
Logging.LOGGER.fine("taking off money: " + amount + " from account: " + iban);
var overflow = amount - getBalance();
if (overflow > 0) {
Logging.LOGGER.fine("taking off money with overflow: " + overflow);
this.overdraft += overflow;
this.balance = 0;
}

View File

@ -1,5 +1,6 @@
package me.teridax.jcash.banking.accounts;
import me.teridax.jcash.Logging;
import me.teridax.jcash.decode.StringDecoder;
import java.util.Objects;
@ -40,10 +41,12 @@ public final class Owner {
*/
public static Owner fromColumns(String... columns) throws IllegalArgumentException, NullPointerException {
Objects.requireNonNull(columns);
Logging.LOGGER.finer("parsing owner from columns");
if (columns.length != 6)
throw new IllegalArgumentException("Invalid number of columns: " + columns.length);
Logging.LOGGER.finer("Decoding owner fields");
// decode fields
var uid = StringDecoder.decodeUniqueIdentificationNumber(columns[0]);
var familyName = StringDecoder.decodeName(columns[1]);

View File

@ -10,6 +10,8 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import static me.teridax.jcash.Logging.LOGGER;
/**
* Management system for banks and all accounts provided by these banks and their respective account owners.
* This class serves a read only database which can only modify runtime data without any respect to CRUD or the ACID
@ -17,6 +19,11 @@ import java.util.*;
*/
public final class BankingManagementSystem {
/**
* Separator used to separate columns of CSV files
*/
private static final String SEPARATOR = ";";
/**
* A set of banks
*/
@ -64,6 +71,7 @@ public final class BankingManagementSystem {
* @return a valid BMS
*/
public static BankingManagementSystem loadFromCsv(Path file) throws IllegalArgumentException {
LOGGER.fine("parsing banking management system from file: " + Objects.toString(file, "null"));
try {
var bms = new BankingManagementSystem();
var content = Files.readString(file);
@ -71,13 +79,16 @@ public final class BankingManagementSystem {
// read line by line
// and skip the first line
content.lines().skip(1).forEach(line -> {
LOGGER.finest("splitting lines by separator: " + SEPARATOR);
// split the line into columns
var columns = line.split(";");
var columns = line.split(SEPARATOR);
// one line must contain exactly 14 columns
if (columns.length != 14)
throw new IllegalArgumentException("invalid column count: " + columns.length);
LOGGER.finer("reading members from line: " + line);
// read basic fields
var owner = Owner.fromColumns(tail(columns, 8));
var account = Account.fromColumns(tail(columns, 2));
@ -90,8 +101,10 @@ public final class BankingManagementSystem {
var bankOfSet = bms.banks.stream().filter(b -> b.equals(bankOfLine)).findFirst();
if (bankOfSet.isPresent()) {
LOGGER.fine("bank from current line is already present in management system");
bankOfSet.get().addAccount(owner, account);
} else {
LOGGER.fine("bank from current line is new for management system");
bankOfLine.addAccount(owner, account);
bms.banks.add(bankOfLine);
}
@ -100,8 +113,10 @@ public final class BankingManagementSystem {
return bms;
} catch (IOException e) {
LOGGER.severe("Could not read file: " + file + " due to: " + e.getMessage());
throw new IllegalArgumentException("Could not read file " + file, e);
} catch (IllegalArgumentException | NullPointerException e) {
LOGGER.severe("Could not parse file: " + file + " due to: " + e.getMessage());
throw new IllegalArgumentException("Could not parse file " + file, e);
}
}

View File

@ -1,5 +1,6 @@
package me.teridax.jcash.banking.management;
import me.teridax.jcash.Logging;
import me.teridax.jcash.banking.Bank;
import me.teridax.jcash.banking.accounts.Account;
import me.teridax.jcash.banking.accounts.Owner;
@ -32,8 +33,10 @@ public class Profile {
private final Account[] accounts;
public Profile(Owner owner, Bank bank, Account account, Account[] accounts) {
if (!Arrays.asList(accounts).contains(account))
if (!Arrays.asList(accounts).contains(account)) {
Logging.LOGGER.severe("Account not found:" + account.getDescription());
throw new IllegalArgumentException("Primary account is not registered at the bank");
}
this.owner = owner;
this.bank = bank;
@ -69,5 +72,6 @@ public class Profile {
break;
}
}
Logging.LOGGER.warning("Account " + description + " not found in associated account list");
}
}

View File

@ -1,5 +1,6 @@
package me.teridax.jcash.gui;
import me.teridax.jcash.Logging;
import me.teridax.jcash.banking.management.BankingManagementSystem;
import javax.swing.*;
@ -42,6 +43,7 @@ public class Loader {
}
}
Logging.LOGGER.warning("no file selected");
throw new IllegalStateException("No file selected");
}
}

View File

@ -1,5 +1,6 @@
package me.teridax.jcash.gui.account;
import me.teridax.jcash.Logging;
import me.teridax.jcash.Main;
import me.teridax.jcash.banking.management.BankingManagementSystem;
import me.teridax.jcash.banking.management.Profile;
@ -17,7 +18,10 @@ public class AccountController {
*/
private final AccountView view;
private final Profile profile;
public AccountController(Profile profile, BankingManagementSystem bms) {
this.profile = profile;
this.view = new AccountView();
this.view.setProfile(profile);
this.data = new AccountData(bms);
@ -26,16 +30,9 @@ public class AccountController {
}
private void createListeners(Profile profile) {
this.view.getAccountSelection().addActionListener(e -> {
var description = ((String) this.view.getAccountSelection().getSelectedItem());
profile.setPrimaryAccount(description);
this.view.setProfile(profile);
});
this.view.getAccountSelection().addActionListener(e -> changeAccount());
this.view.getLogout().addActionListener(e -> {
Main.getInstance().logout();
Main.getInstance().showLoginScreen();
});
this.view.getLogout().addActionListener(e -> logout());
this.view.getDeposit().addActionListener(e -> new DepositDialog(profile.getPrimaryAccount(), () -> this.view.setProfile(profile)));
@ -44,6 +41,19 @@ public class AccountController {
this.view.getTransfer().addActionListener(e -> new TransferDialog(profile.getPrimaryAccount(), data.getBms(), () -> this.view.setProfile(profile)));
}
private void logout() {
Logging.LOGGER.fine("Logging out of account");
Main.getInstance().logout();
Main.getInstance().showLoginScreen();
}
private void changeAccount() {
var description = ((String) this.view.getAccountSelection().getSelectedItem());
Logging.LOGGER.fine("Changing primary account selected: " + description);
this.profile.setPrimaryAccount(description);
this.view.setProfile(profile);
}
public AccountView getView() {
return view;
}

View File

@ -1,5 +1,6 @@
package me.teridax.jcash.gui.account;
import me.teridax.jcash.Logging;
import me.teridax.jcash.banking.accounts.CurrentAccount;
import me.teridax.jcash.banking.accounts.SavingsAccount;
import me.teridax.jcash.banking.management.Profile;
@ -35,6 +36,7 @@ public class AccountView extends JPanel {
}
public void setProfile(Profile profile) {
Logging.LOGGER.finer("Changing profile of account view");
var bank = profile.getBank();
var account = profile.getPrimaryAccount();
var owner = profile.getOwner();
@ -49,12 +51,14 @@ public class AccountView extends JPanel {
this.balance.setText(StringDecoder.LOCAL_NUMBER_FORMAT.format(account.getBalance()) + "");
this.type.setText(account.getClass().getSimpleName());
if (account instanceof CurrentAccount) {
if (account instanceof CurrentAccount) {;
this.typeSpecialLabel.setText("Overdraft");
this.typeSpecialProperty.setText( StringDecoder.LOCAL_NUMBER_FORMAT.format(((CurrentAccount) account).getOverdraft()) + "");
} else if (account instanceof SavingsAccount) {
this.typeSpecialLabel.setText("Interest rate");
this.typeSpecialProperty.setText( ((SavingsAccount) account).getInterest() + " %" );
} else {
Logging.LOGGER.severe("Type of new primary account cannot be determined: " + account.getClass().getName());
}
this.accountSelection.removeAllItems();

View File

@ -1,17 +1,37 @@
package me.teridax.jcash.gui.deposit;
import me.teridax.jcash.Logging;
import me.teridax.jcash.banking.accounts.Account;
public class DepositDialog {
private final DepositView view;
private final Account account;
private final Runnable onDeposit;
public DepositDialog(Account account, Runnable onDeposit) {
var view = new DepositView();
view.getDeposit().addActionListener(e -> {
account.deposit(view.getAmount());
this.account = account;
this.onDeposit = onDeposit;
this.view = new DepositView();
this.view.getDeposit().addActionListener(e -> depositMoney());
this.view.getCancel().addActionListener(e -> view.dispose());
this.view.showDialog();
}
private void depositMoney() {
var amount = view.getAmount();
Logging.LOGGER.fine("Depositing money of account: " + account.getIban() + " amount: " + amount);
try {
account.deposit(amount);
onDeposit.run();
} catch (IllegalArgumentException ex) {
Logging.LOGGER.severe("Cannot deposit money of account: " + account.getIban() + " because: " + ex.getMessage());
} finally {
view.dispose();
});
view.getCancel().addActionListener(e -> view.dispose());
view.showDialog();
}
}
}

View File

@ -1,5 +1,6 @@
package me.teridax.jcash.gui.deposit;
import me.teridax.jcash.Logging;
import me.teridax.jcash.decode.StringDecoder;
import javax.swing.*;
@ -96,6 +97,7 @@ public class DepositView {
try {
return NumberFormat.getNumberInstance().parse(value.getText()).doubleValue();
} catch (ParseException e) {
Logging.LOGGER.severe("Amount text field contains invalid value: " + value);
throw new RuntimeException(e);
}
}

View File

@ -1,5 +1,6 @@
package me.teridax.jcash.gui.login;
import me.teridax.jcash.Logging;
import me.teridax.jcash.banking.management.BankingManagementSystem;
import java.awt.event.ActionEvent;
@ -31,6 +32,7 @@ public class LoginController {
var iban = this.view.getIban().getText();
return Optional.of(Integer.parseUnsignedInt(iban));
} catch (NumberFormatException e) {
Logging.LOGGER.warning("IBAN text field contains invalid value: " + this.view.getIban().getText());
return Optional.empty();
}
}
@ -40,6 +42,7 @@ public class LoginController {
var iban = this.view.getPin().getPassword();
return Optional.of(Integer.parseUnsignedInt(new String(iban)));
} catch (NumberFormatException e) {
Logging.LOGGER.severe("PIN text field contains invalid value: " + String.valueOf(view.getPin().getPassword()));
return Optional.empty();
}
}
@ -48,12 +51,14 @@ public class LoginController {
var blz = this.view.getBlz().getText();
var iban = this.getIban();
if (iban.isEmpty()) {
Logging.LOGGER.severe("IBAN is invalid: " + iban);
showMessageDialog(null, "invalid IBAN", "Faulty login attempt", ERROR_MESSAGE);
return;
}
var pin = this.getPin();
if (pin.isEmpty()) {
Logging.LOGGER.severe("PIN is invalid: " + pin);
showMessageDialog(null, "invalid pin", "Faulty login attempt", ERROR_MESSAGE);
return;
}
@ -62,6 +67,7 @@ public class LoginController {
if (account.isPresent()) {
this.listener.onAccountSelected(account.get());
} else {
Logging.LOGGER.severe("invalid login credentials: " + iban + " / " + pin);
showMessageDialog(null, "invalid login credentials", "Faulty login attempt", ERROR_MESSAGE);
}
}

View File

@ -1,5 +1,6 @@
package me.teridax.jcash.gui.login;
import me.teridax.jcash.Logging;
import me.teridax.jcash.banking.management.BankingManagementSystem;
import me.teridax.jcash.banking.management.Profile;
@ -24,6 +25,7 @@ public class LoginData {
* @return an optional wrapping the specified account if authentication was successful
*/
public Optional<Profile> authenticateAccount(String blz, int iban, int pin) {
Logging.LOGGER.info("Authenticating account " + iban);
var optionalBank = bms.getBank(blz);
if (optionalBank.isEmpty())
return Optional.empty();

View File

@ -1,5 +1,6 @@
package me.teridax.jcash.gui.takeoff;
import me.teridax.jcash.Logging;
import me.teridax.jcash.banking.accounts.Account;
import static javax.swing.JOptionPane.ERROR_MESSAGE;
@ -7,18 +8,29 @@ import static javax.swing.JOptionPane.showMessageDialog;
public class TakeoffDialog {
private final Account account;
private final Runnable onTakeoff;
private final TakeoffView view;
public TakeoffDialog(Account account, Runnable onTakeoff) {
var view = new TakeoffView();
view.getTakeoff().addActionListener(e -> {
try {
account.takeoff(view.getAmount());
onTakeoff.run();
view.dispose();
} catch (IllegalArgumentException ex) {
showMessageDialog(null, "Reason: " + ex.getMessage(), "Could not take off money", ERROR_MESSAGE);
}
});
view.getCancel().addActionListener(e -> view.dispose());
view.showDialog();
this.account = account;
this.onTakeoff = onTakeoff;
this.view = new TakeoffView();
this.view.getTakeoff().addActionListener(e -> takeOff());
this.view.getCancel().addActionListener(e -> view.dispose());
this.view.showDialog();
}
private void takeOff() {
try {
account.takeoff(view.getAmount());
onTakeoff.run();
view.dispose();
} catch (IllegalArgumentException ex) {
Logging.LOGGER.severe("Could not take off money: " + ex.getMessage());
showMessageDialog(null, "Reason: " + ex.getMessage(), "Could not take off money", ERROR_MESSAGE);
}
}
}

View File

@ -1,5 +1,6 @@
package me.teridax.jcash.gui.transfer;
import me.teridax.jcash.Logging;
import me.teridax.jcash.banking.management.BankingManagementSystem;
import me.teridax.jcash.decode.StringDecoder;
@ -20,19 +21,24 @@ public class TransferData {
*/
public void transferValue(double amount, String blz, String ibanString) throws IllegalArgumentException {
var bank = bms.getBank(blz);
if (bank.isEmpty())
if (bank.isEmpty()) {
Logging.LOGGER.warning("Bank not found: " + blz);
throw new IllegalArgumentException("Bank not found: " + blz);
}
var iban = 0;
try {
iban = StringDecoder.decodeUniqueIdentificationNumber(ibanString);
} catch (Exception ex) {
Logging.LOGGER.warning("IBAN has invalid format: " + ibanString + " because: " + ex.getMessage());
throw new IllegalArgumentException("IBAN has invalid format: " + ibanString);
}
var account = bank.get().getAccount(iban);
if (account.isEmpty())
if (account.isEmpty()) {
Logging.LOGGER.warning("Account not found: " + iban);
throw new IllegalArgumentException("Account not found: " + iban);
}
account.get().getPrimaryAccount().deposit(amount);
}

View File

@ -1,5 +1,6 @@
package me.teridax.jcash.gui.transfer;
import me.teridax.jcash.Logging;
import me.teridax.jcash.banking.accounts.Account;
import me.teridax.jcash.banking.management.BankingManagementSystem;
@ -8,22 +9,33 @@ import static javax.swing.JOptionPane.showMessageDialog;
public class TransferDialog {
public TransferDialog(Account account, BankingManagementSystem bms, Runnable onDeposit) {
var view = new TransferView();
var data = new TransferData(bms);
view.getTransfer().addActionListener(e -> {
try {
var amount = view.getAmount();
private final Account account;
private final Runnable onDeposit;
private final TransferData transferData;
private final TransferView transferView;
account.takeoff(amount);
data.transferValue(amount, view.getBlz(), view.getIban());
onDeposit.run();
view.dispose();
} catch (IllegalArgumentException ex) {
showMessageDialog(null, "invalid account", "Could not transfer", ERROR_MESSAGE);
}
});
view.getCancel().addActionListener(e -> view.dispose());
view.showDialog();
public TransferDialog(Account account, BankingManagementSystem bms, Runnable onDeposit) {
this.account = account;
this.onDeposit = onDeposit;
transferView = new TransferView();
transferData = new TransferData(bms);
transferView.getTransfer().addActionListener(e -> transfer());
transferView.getCancel().addActionListener(e -> transferView.dispose());
transferView.showDialog();
}
private void transfer() {
try {
var amount = transferView.getAmount();
account.takeoff(amount);
transferData.transferValue(amount, transferView.getBlz(), transferView.getIban());
onDeposit.run();
transferView.dispose();
} catch (IllegalArgumentException ex) {
Logging.LOGGER.severe("Could not transfer: " + ex.getMessage());
showMessageDialog(null, "invalid account", "Could not transfer", ERROR_MESSAGE);
}
}
}

View File

@ -1,11 +1,10 @@
package me.teridax.jcash.gui.transfer;
import me.teridax.jcash.Logging;
import me.teridax.jcash.decode.StringDecoder;
import javax.swing.*;
import java.awt.*;
import java.text.NumberFormat;
import java.text.ParseException;
import static javax.swing.JOptionPane.ERROR_MESSAGE;
import static javax.swing.JOptionPane.showMessageDialog;
@ -118,13 +117,15 @@ public class TransferView {
public double getAmount() {
if (value.getText().isBlank()) {
Logging.LOGGER.severe("Amount is empty");
showMessageDialog(null, "invalid amount", "currency must not be blank", ERROR_MESSAGE);
return 0;
}
try {
return NumberFormat.getNumberInstance().parse(value.getText()).doubleValue();
} catch (ParseException e) {
return StringDecoder.decodeCurrency(value.getText());
} catch (IllegalArgumentException e) {
Logging.LOGGER.severe("Invalid amount: " + value.getText());
throw new RuntimeException(e);
}
}