Compare commits
No commits in common. "main" and "gui" have entirely different histories.
35
README.md
35
README.md
|
@ -1,34 +1,3 @@
|
|||
## JCash
|
||||
# JCash
|
||||
|
||||
## About
|
||||
|
||||
Draft program for the Java class of semester 2. The goal was to simulate basic cash machine that can read customer data from a `.csv` file and let the user view the data with crude forms of authentication.
|
||||
|
||||
> This project was graded with 100,0 out of 100,0 points
|
||||
|
||||
## Overview
|
||||
|
||||
The program can read `.csv` file from disk and allows the user login to an account specified.
|
||||
After login, the user can deposit, takeoff or transfer money from an account.
|
||||
The application does not write any changes back into the file.
|
||||
|
||||
## Images
|
||||
|
||||
![preview.png](https://git.teridax.de/dhbw/JCash/raw/branch/main/meta/Preview.png)
|
||||
|
||||
## Repository structure
|
||||
|
||||
The `meta/` folder only contains metadata required for the README. Its not a build relevant folder.
|
||||
Sample database files can be found under `res/`.
|
||||
|
||||
## CSV Database format
|
||||
|
||||
The first row is always ignored as this is generally contains the columns header.
|
||||
|
||||
Reference example:
|
||||
|
||||
|
||||
| Bank | BLZ | Kontonummer | PIN | Kontostand | Kontoart | Zins | Überziehungsbetrag | Kundennummer | Name | Vorname | Straße |
|
||||
| ---------------------- | -------- | ------------- | ------ | ------------ | ----------- | ------ | --------------------- | -------------- | --------- | --------- | ------------------------------ |
|
||||
| VR Bank Rhein-Neckar | MA2424 | 4711 | 1234 | 45,90€ | Girokonto | | 1000€ | 8745364 | Gunther | Peter | Katzenköttel an der Strulle |
|
||||
| Berliner Bank | 19087 | 987456 | 4578 | 9,05€ | Sparkonto | 3% | | 39845762 | Korb | Jauch | Unter der Brücke |
|
||||
Draft program for the Java class of semester 2. The goal was to simulate basic cash machine that can read customer data from a .csv file and let the user view the data with curde forms of authentication.
|
BIN
meta/Preview.png
BIN
meta/Preview.png
Binary file not shown.
Before Width: | Height: | Size: 222 KiB |
|
@ -1,97 +0,0 @@
|
|||
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.*;
|
||||
|
||||
import static java.nio.file.Path.of;
|
||||
|
||||
/**
|
||||
* Utility class for providing a global logger for the entire application instance at runtime
|
||||
*/
|
||||
public final class Logging {
|
||||
|
||||
/**
|
||||
* Publicly available logger instance.
|
||||
*/
|
||||
public static final Logger LOGGER = Logger.getLogger(Logging.class.getName());
|
||||
|
||||
/**
|
||||
* Folder the log files are stored in, relative to the file containing the class
|
||||
*/
|
||||
private static final String LOG_FOLDER_NAME = "logs/";
|
||||
/**
|
||||
* Format for the date time used for log files
|
||||
*/
|
||||
private static final String DATE_TIME_FORMAT = "yyyy-MM-dd_HH-mm-ss";
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a console log handler writing to the applications stderr
|
||||
*
|
||||
* @param level the level to set the handler to
|
||||
*/
|
||||
private static void createConsoleLogger(Level level) {
|
||||
ConsoleHandler ch = new ConsoleHandler();
|
||||
ch.setLevel(level);
|
||||
LOGGER.addHandler(ch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a log handler writing to a file.
|
||||
* The file will be located in a folder directly besides the application
|
||||
* with the name LOG_FOLDER_NAME. The name will follow the format DATE_TIME_FORMAT plus "log" as
|
||||
* file extension.
|
||||
*
|
||||
* @param level the log level to set the handler to
|
||||
*/
|
||||
private static void createFileLogger(Level level) {
|
||||
// setup log file name
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String dateTime = DateTimeFormatter.ofPattern(DATE_TIME_FORMAT).format(now);
|
||||
String logFileName = LOG_FOLDER_NAME + dateTime + ".log";
|
||||
|
||||
// setup the folder for the logs
|
||||
initializeLogFolder();
|
||||
|
||||
try {
|
||||
FileHandler fh = new FileHandler(logFileName);
|
||||
fh.setLevel(level);
|
||||
LOGGER.addHandler(fh);
|
||||
} catch (Exception e) {
|
||||
LOGGER.warning("Unable to initialize logging for file: " + logFileName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the folder containing log files is present.
|
||||
* If the folder does not exist, the function will create a new folder.
|
||||
*/
|
||||
private static void initializeLogFolder() {
|
||||
Path folderPath = of(LOG_FOLDER_NAME);
|
||||
|
||||
if (Files.isDirectory(folderPath))
|
||||
return;
|
||||
|
||||
try {
|
||||
Files.createDirectory(folderPath);
|
||||
} catch (IOException e) {
|
||||
LOGGER.warning("Unable to create directory: " + folderPath);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,19 +1,10 @@
|
|||
package me.teridax.jcash;
|
||||
|
||||
import me.teridax.jcash.gui.IconProvider;
|
||||
import me.teridax.jcash.gui.Loader;
|
||||
import me.teridax.jcash.gui.MainFrame;
|
||||
import me.teridax.jcash.gui.Utils;
|
||||
import me.teridax.jcash.lang.Locales;
|
||||
import me.teridax.jcash.gui.account.AccountController;
|
||||
import me.teridax.jcash.gui.login.LoginController;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static me.teridax.jcash.Logging.LOGGER;
|
||||
import static me.teridax.jcash.Logging.initializeSystemLogger;
|
||||
|
||||
public final class Main {
|
||||
|
||||
|
@ -23,99 +14,24 @@ public final class Main {
|
|||
private static Main instance;
|
||||
|
||||
/**
|
||||
* Primary class for controlling GUI of this application
|
||||
* Primary window of this program
|
||||
*/
|
||||
private final MainFrame window;
|
||||
private final JFrame window;
|
||||
|
||||
private Main() {
|
||||
this.window = new MainFrame();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts the user a dialog to select a file to load the database from.
|
||||
* If a valid database has been read a login screen will be shown.
|
||||
* If no file was selected or the database was invalid the application will close.
|
||||
*/
|
||||
public void loadDatabase() {
|
||||
try {
|
||||
var bms = Loader.load();
|
||||
this.window.setBms(bms);
|
||||
|
||||
} catch (Exception e) {
|
||||
LOGGER.severe("Failed to load database: " + e.getMessage());
|
||||
Utils.error("Failed to load database");
|
||||
System.exit(1);
|
||||
}
|
||||
// create main window and set defaults
|
||||
this.window = new JFrame();
|
||||
this.window.setTitle("Bankautomat");
|
||||
this.window.setLocationByPlatform(true);
|
||||
this.window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
initializeSystemLogger(Level.FINE);
|
||||
|
||||
setPlatformDependingTheme();
|
||||
|
||||
loadExtraFont();
|
||||
|
||||
Locales.autodetectDefaultLocale();
|
||||
|
||||
// create main instance and show the login screen
|
||||
instance();
|
||||
getInstance().loadDatabase();
|
||||
getInstance().showLoginScreen();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the extra font file used on the login button
|
||||
*/
|
||||
private static void loadExtraFont() {
|
||||
try {
|
||||
var font = Font.createFont(Font.TRUETYPE_FONT, Objects.requireNonNull(IconProvider.class.getResourceAsStream("res/Circus.ttf")));
|
||||
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
|
||||
ge.registerFont(font);
|
||||
} catch (IOException | FontFormatException | NullPointerException e) {
|
||||
LOGGER.warning("Could not load font file: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the look and feel via the ui manager.
|
||||
* This function will select a look and feel so that the application will
|
||||
* look most like a native application.
|
||||
* It will select the systems look and feel for Windows and MacOS
|
||||
* and GTK for unix based systems.
|
||||
*/
|
||||
private static void setPlatformDependingTheme() {
|
||||
// default look and feel
|
||||
var laf = UIManager.getCrossPlatformLookAndFeelClassName();
|
||||
|
||||
// runtime os
|
||||
String os = System.getProperty("os.name").toLowerCase();
|
||||
LOGGER.fine("Detected operating system: " + os);
|
||||
|
||||
// set look and feel class name depending on the platform we run on
|
||||
if (os.contains("win") || os.contains("mac")) {
|
||||
LOGGER.info("Detected Windows or MacOS based operating system, using system look and feel");
|
||||
laf = UIManager.getSystemLookAndFeelClassName();
|
||||
} else if (os.contains("nix") || os.contains("nux") || os.contains("aix") || os.contains("sunos")) {
|
||||
LOGGER.info("Detected Unix/Linux based operating system, using GTK look and feel");
|
||||
laf = "com.sun.java.swing.plaf.gtk.GTKLookAndFeel";
|
||||
} else {
|
||||
LOGGER.warning("Unable to detect operating system: " + os);
|
||||
}
|
||||
|
||||
try {
|
||||
LOGGER.info("Setting look and feel to class: " + laf);
|
||||
UIManager.setLookAndFeel(laf);
|
||||
} catch (ClassNotFoundException | UnsupportedLookAndFeelException e) {
|
||||
LOGGER.severe("Look and feel class not found: " + e.getMessage());
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
LOGGER.warning("Could not set look and feel: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the main singleton instance of this program
|
||||
*
|
||||
* @return the singleton instance of this class
|
||||
*/
|
||||
public static Main getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
@ -123,28 +39,54 @@ public final class Main {
|
|||
/**
|
||||
* Attempts to create a new instance of the singleton class Main.
|
||||
* This method throws an exception if the class is already instantiated.
|
||||
*
|
||||
* @throws IllegalStateException if the class is already instantiated
|
||||
* @pre the static instance of this class should be null.
|
||||
* @post the static instance of this class will be set
|
||||
*/
|
||||
public static void instance() {
|
||||
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();
|
||||
}
|
||||
|
||||
public JFrame getWindow() {
|
||||
return this.window.getWindow();
|
||||
/**
|
||||
* Shows the open dialog for selecting a database file. After selection, it then proceeds to prompt login.
|
||||
* Afterward the selected account can be managed.
|
||||
* This method is non-blocking and all work described is performed asynchronously on the AWT Event dispatcher.
|
||||
*/
|
||||
public void showLoginScreen() {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
try {
|
||||
// select db file
|
||||
var path = Loader.load();
|
||||
// read database and login
|
||||
var login = new LoginController(path);
|
||||
|
||||
// when we have logged in set the account viewer as window content
|
||||
login.addAccountSelectionListener(account -> {
|
||||
var profileCont = new AccountController(account);
|
||||
this.window.setContentPane(profileCont.getView());
|
||||
this.window.revalidate();
|
||||
this.window.repaint();
|
||||
});
|
||||
|
||||
// we are not logged in yet, so show the login prompt on the main window
|
||||
this.window.setContentPane(login.getView());
|
||||
this.window.setSize(800, 600);
|
||||
this.window.setVisible(true);
|
||||
|
||||
} catch (IllegalStateException e) {
|
||||
System.out.println("no file selected. goodbye");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the user out of the currently open account.
|
||||
* This will show the login mask and clear the password field or the previous
|
||||
* login attempt.
|
||||
* Logs the user out of the database, hiding the main window.
|
||||
*/
|
||||
public void logout() {
|
||||
this.window.logout();
|
||||
window.setContentPane(new JLabel("you're logged out"));
|
||||
window.setVisible(false);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package me.teridax.jcash.banking;
|
||||
|
||||
import me.teridax.jcash.decode.StringUtils;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public abstract class Account {
|
||||
|
||||
private final int iban;
|
||||
private final int pin;
|
||||
private double balance;
|
||||
|
||||
public Account(int iban, int pin, double balance) {
|
||||
this.iban = iban;
|
||||
this.pin = pin;
|
||||
this.balance = balance;
|
||||
}
|
||||
|
||||
public static Account fromColumns(String[] columns) {
|
||||
Objects.requireNonNull(columns);
|
||||
|
||||
var iban = StringUtils.decodeUniqueIdentificationNumber(columns[0]);
|
||||
var pin = StringUtils.decodeUniqueIdentificationNumber(columns[1]);
|
||||
var balance = StringUtils.decodeCurrency(columns[2]);
|
||||
var type = StringUtils.decodeName(columns[3]);
|
||||
|
||||
try {
|
||||
if (type.equals("Sparkonto")) {
|
||||
var interest = StringUtils.decodePercent(columns[4]);
|
||||
return new SavingsAccount(iban, pin, balance, interest);
|
||||
} else if (type.equals("Girokonto")) {
|
||||
var overdraft = StringUtils.decodeCurrency(columns[5]);
|
||||
return new Girokonto(iban, pin, balance, overdraft);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid account type: " + type);
|
||||
}
|
||||
} catch (IllegalArgumentException | NullPointerException e) {
|
||||
throw new IllegalArgumentException("Account format: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
public int getIban() {
|
||||
return iban;
|
||||
}
|
||||
|
||||
public int getPin() {
|
||||
return pin;
|
||||
}
|
||||
|
||||
public double getBalance() {
|
||||
return balance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(iban);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Account)
|
||||
return iban == ((Account)obj).iban;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return String.format("%s (%s)", iban, getClass().getSimpleName());
|
||||
}
|
||||
|
||||
public void deposit(double amount) {
|
||||
this.balance += amount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("@Account [iban=%d, pin=%d, balance=%.2f]", iban, pin, balance);
|
||||
}
|
||||
|
||||
public void takeoff(double amount) {
|
||||
this.balance = Math.max(0, this.balance - amount);
|
||||
}
|
||||
}
|
|
@ -1,57 +1,20 @@
|
|||
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;
|
||||
import me.teridax.jcash.gui.Utils;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Bank storing a name, blz and a list of owners and their accounts for this bank.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public final class Bank {
|
||||
/**
|
||||
* The name of the bank
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
/**
|
||||
* Bankleitzahl. Identification number of banks in germany
|
||||
*/
|
||||
private final String blz;
|
||||
|
||||
/**
|
||||
* A map of banking accounts associated with their respective owner
|
||||
*/
|
||||
private final Map<Owner, Set<Account>> accounts;
|
||||
|
||||
public Bank(String blz, String name) {
|
||||
Bank(String blz, String name) {
|
||||
this.blz = blz;
|
||||
this.name = name;
|
||||
this.accounts = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given BLZ. If the blz is not valid an exception is thrown.
|
||||
*
|
||||
* @param maybeBlz a string to be tested for being a blz
|
||||
* @return returns the correctly formatted blz
|
||||
* @throws IllegalArgumentException if the supplied argument is not a valid blz
|
||||
*/
|
||||
public static String validateBlz(String maybeBlz) throws IllegalArgumentException {
|
||||
var pattern = Pattern.compile("[\\w-_]+");
|
||||
var matcher = pattern.matcher(maybeBlz);
|
||||
|
||||
if (matcher.find())
|
||||
return matcher.group();
|
||||
|
||||
throw new IllegalArgumentException("not a valid BLZ: " + maybeBlz);
|
||||
}
|
||||
|
||||
public String getBlz() {
|
||||
return blz;
|
||||
}
|
||||
|
@ -60,14 +23,6 @@ public final class Bank {
|
|||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new account and its associated owner to this bank.
|
||||
* If the owner is already present the account is added to the existing owner.
|
||||
* If the account is already present for the owner the old account gets overwritten.
|
||||
*
|
||||
* @param owner the owner of the account
|
||||
* @param account the account of the owner
|
||||
*/
|
||||
public void addAccount(Owner owner, Account account) {
|
||||
var set = this.accounts.getOrDefault(owner, new HashSet<>());
|
||||
set.add(account);
|
||||
|
@ -87,33 +42,29 @@ public final class Bank {
|
|||
return Objects.hash(blz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all accounts owned by the specific owner.
|
||||
*
|
||||
* @param owner the owner for which accounts are to be retrieved
|
||||
* @return all accounts owned by the owner and managed by this bank
|
||||
*/
|
||||
public Account[] getAccountsOfOwner(Owner owner) {
|
||||
return accounts.get(owner).toArray(Account[]::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a profile of the specified international bank account number.
|
||||
*
|
||||
* @param iban the number to create a profile for
|
||||
* @return the profile for the iban account
|
||||
*/
|
||||
public Optional<Profile> getAccount(int iban) {
|
||||
// find the account which has the supplied iban
|
||||
public static String validateBlz(String maybeBlz) {
|
||||
var pattern = Pattern.compile("[\\w\\d-_]+");
|
||||
var matcher = pattern.matcher(maybeBlz);
|
||||
|
||||
if (matcher.find())
|
||||
return matcher.group();
|
||||
|
||||
throw new IllegalArgumentException("not a valid BLZ: " + maybeBlz);
|
||||
}
|
||||
|
||||
public Optional<Profile> getAccount(int iban) {
|
||||
for (var owner : this.accounts.entrySet()) {
|
||||
for (var account : owner.getValue()) {
|
||||
if (iban == account.getIban()) {
|
||||
var tmp = account.getIban();
|
||||
if (tmp == iban) {
|
||||
return Optional.of(new Profile(owner.getKey(), this, account, getAccountsOfOwner(owner.getKey())));
|
||||
}
|
||||
}
|
||||
}
|
||||
Logging.LOGGER.finer("Account not found: " + iban);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
package me.teridax.jcash.banking;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
|
||||
public class BankTest {
|
||||
|
||||
@Test
|
||||
public void testBlzValidation() {
|
||||
// everything is fine
|
||||
assertEquals(Bank.validateBlz("MA2424"), "MA2424");
|
||||
assertEquals(Bank.validateBlz("VR-BANK-567"), "VR-BANK-567");
|
||||
assertEquals(Bank.validateBlz("19087"), "19087");
|
||||
|
||||
// cut stuff away
|
||||
assertEquals(Bank.validateBlz("MA2%asd424"), "MA2");
|
||||
assertEquals(Bank.validateBlz("qg0948 z67 0"), "qg0948");
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testBlzValidationException() {
|
||||
assertNotEquals(Bank.validateBlz("öüäöüäöü"), "öüäöüäöü");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package me.teridax.jcash.banking;
|
||||
|
||||
import me.teridax.jcash.decode.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
public final class BankingManagementSystem {
|
||||
|
||||
private final Set<Bank> banks;
|
||||
|
||||
private BankingManagementSystem() {
|
||||
this.banks = new HashSet<>();
|
||||
}
|
||||
|
||||
private static String[] tail(String[] array, int index) {
|
||||
return Arrays.stream(array).skip(index).toArray(String[]::new);
|
||||
}
|
||||
|
||||
public static BankingManagementSystem loadFromCsv(Path file) {
|
||||
try {
|
||||
var bms = new BankingManagementSystem();
|
||||
var content = Files.readString(file);
|
||||
|
||||
content.lines().skip(1).forEach(line -> {
|
||||
var columns = line.split(";");
|
||||
|
||||
if (columns.length != 14)
|
||||
throw new IllegalArgumentException("invalid column count: " + columns.length);
|
||||
|
||||
var owner = Owner.fromColumns(tail(columns, 8));
|
||||
var account = Account.fromColumns(tail(columns, 2));
|
||||
|
||||
var blz = Bank.validateBlz(columns[1]);
|
||||
var name = StringUtils.decodeName(columns[0]);
|
||||
var bankOfLine = new Bank(blz, name);
|
||||
|
||||
var bankOfSet = bms.banks.stream().filter(b -> b.equals(bankOfLine)).findFirst();
|
||||
if (bankOfSet.isPresent()) {
|
||||
bankOfSet.get().addAccount(owner, account);
|
||||
} else {
|
||||
bankOfLine.addAccount(owner, account);
|
||||
bms.banks.add(bankOfLine);
|
||||
}
|
||||
});
|
||||
|
||||
return bms;
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException("Could not read file " + file, e);
|
||||
} catch (IllegalArgumentException | NullPointerException e) {
|
||||
throw new IllegalArgumentException("Could not parse file " + file, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<Bank> getBank(String blz) {
|
||||
return this.banks.stream().filter(b -> b.getBlz().equals(blz)).findFirst();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package me.teridax.jcash.banking;
|
||||
|
||||
public class Girokonto extends Account {
|
||||
|
||||
private final double overdraft;
|
||||
|
||||
public Girokonto(int iban, int pin, double balance, double overdraft) {
|
||||
super(iban, pin, balance);
|
||||
this.overdraft = overdraft;
|
||||
}
|
||||
|
||||
public double getOverdraft() {
|
||||
return overdraft;
|
||||
}
|
||||
}
|
|
@ -1,27 +1,17 @@
|
|||
package me.teridax.jcash.banking.accounts;
|
||||
package me.teridax.jcash.banking;
|
||||
|
||||
import me.teridax.jcash.Logging;
|
||||
import me.teridax.jcash.decode.StringDecoder;
|
||||
import me.teridax.jcash.decode.StringUtils;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Represents a person owning an account.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public final class Owner {
|
||||
/**
|
||||
* unique identifier
|
||||
*/
|
||||
private final int uid;
|
||||
private final String familyName;
|
||||
private final String name;
|
||||
private final String street;
|
||||
/**
|
||||
* postal code
|
||||
*/
|
||||
private final int zip;
|
||||
private final String city;
|
||||
private int uid;
|
||||
private String familyName;
|
||||
private String name;
|
||||
private String street;
|
||||
private int zip;
|
||||
private String city;
|
||||
|
||||
private Owner(int uid, String familyName, String name, int zip, String city, String street) {
|
||||
this.uid = uid;
|
||||
|
@ -32,29 +22,18 @@ public final class Owner {
|
|||
this.street = street;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of this class parsed from the columns.
|
||||
*
|
||||
* @param columns the fields of this class as strings
|
||||
* @return an instance of this class
|
||||
* @throws IllegalArgumentException if the supplied columns is invalid
|
||||
* @throws NullPointerException if columns is null
|
||||
*/
|
||||
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]);
|
||||
var name = StringDecoder.decodeName(columns[2]);
|
||||
var street = StringDecoder.decodeStreet(columns[3]);
|
||||
var zip = StringDecoder.decodeUniqueIdentificationNumber(columns[4]);
|
||||
var city = StringDecoder.decodeName(columns[5]);
|
||||
var uid = StringUtils.decodeUniqueIdentificationNumber(columns[0]);
|
||||
var familyName = StringUtils.decodeName(columns[1]);
|
||||
var name = StringUtils.decodeName(columns[2]);
|
||||
var street = StringUtils.decodeStreet(columns[3]);
|
||||
var zip = StringUtils.decodeUniqueIdentificationNumber(columns[4]);
|
||||
var city = StringUtils.decodeName(columns[5]);
|
||||
|
||||
return new Owner(uid, familyName, name, zip, city, street);
|
||||
}
|
||||
|
@ -90,9 +69,9 @@ public final class Owner {
|
|||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Owner)
|
||||
if (obj instanceof Owner) {
|
||||
return this.uid == ((Owner) obj).getUid();
|
||||
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package me.teridax.jcash.banking;
|
||||
|
||||
public class Profile {
|
||||
|
||||
private Owner owner;
|
||||
private Bank bank;
|
||||
private Account primaryAccount;
|
||||
private Account[] accounts;
|
||||
|
||||
public Profile(Owner owner, Bank bank, Account account, Account[] accounts) {
|
||||
this.owner = owner;
|
||||
this.bank = bank;
|
||||
this.accounts = accounts;
|
||||
this.primaryAccount = account;
|
||||
}
|
||||
|
||||
public Account getPrimaryAccount() {
|
||||
return primaryAccount;
|
||||
}
|
||||
|
||||
public Owner getOwner() {
|
||||
return owner;
|
||||
}
|
||||
|
||||
public Bank getBank() {
|
||||
return bank;
|
||||
}
|
||||
|
||||
public Account[] getAccounts() {
|
||||
return accounts;
|
||||
}
|
||||
|
||||
public void setPrimaryAccount(String description) {
|
||||
for (Account account : accounts) {
|
||||
if (account.getDescription().equals(description)) {
|
||||
this.primaryAccount = account;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +1,8 @@
|
|||
package me.teridax.jcash.banking.accounts;
|
||||
package me.teridax.jcash.banking;
|
||||
|
||||
/**
|
||||
* Savings account representing a german "Sparkonto".
|
||||
* To the balance a certain interest rate is added.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public final class SavingsAccount extends Account {
|
||||
public class SavingsAccount extends Account {
|
||||
|
||||
/**
|
||||
* interest rate applied to the balance of the super class
|
||||
*/
|
||||
private final double interest;
|
||||
|
||||
public SavingsAccount(int iban, int pin, double balance, double interest) {
|
|
@ -1,10 +1,10 @@
|
|||
package me.teridax.jcash.banking.management;
|
||||
package me.teridax.jcash.banking;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.nio.file.Paths;
|
||||
|
||||
public class BankingManagementSystemTest {
|
||||
public class Tests {
|
||||
|
||||
@Test
|
||||
public void test() {
|
|
@ -1,154 +0,0 @@
|
|||
package me.teridax.jcash.banking.accounts;
|
||||
|
||||
import me.teridax.jcash.Logging;
|
||||
import me.teridax.jcash.decode.StringDecoder;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static me.teridax.jcash.lang.Translator.translate;
|
||||
|
||||
/**
|
||||
* Base class for bank accounts.
|
||||
* Stores the iban, pin and balance.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public abstract class Account {
|
||||
|
||||
/**
|
||||
* International bank account number
|
||||
*/
|
||||
protected final int iban;
|
||||
/**
|
||||
* Personal identification number
|
||||
*/
|
||||
protected final int pin;
|
||||
/**
|
||||
* Balance of this account
|
||||
*/
|
||||
protected double balance;
|
||||
|
||||
public Account(int iban, int pin, double balance) {
|
||||
this.iban = iban;
|
||||
this.pin = pin;
|
||||
this.balance = balance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a row of a fixed amount of columns into an account.
|
||||
* This function will attempt to create an instance of two classes which inherit from Account.
|
||||
*
|
||||
* @param columns array of 6 strings
|
||||
* @return either an instance of {@link SavingsAccount} or an instance of {@link CurrentAccount}
|
||||
* @throws IllegalArgumentException if the account type cannot be determined or the provided data is invalid
|
||||
* @throws NullPointerException if columns is null
|
||||
*/
|
||||
public static Account fromColumns(String[] columns) throws IllegalArgumentException, NullPointerException {
|
||||
Objects.requireNonNull(columns);
|
||||
Logging.LOGGER.finer("Parsing account from columns");
|
||||
|
||||
// deserialize fields
|
||||
var iban = StringDecoder.decodeUniqueIdentificationNumber(columns[0]);
|
||||
var pin = StringDecoder.decodeUniqueIdentificationNumber(columns[1]);
|
||||
var balance = StringDecoder.decodeCurrency(columns[2]);
|
||||
var type = StringDecoder.decodeName(columns[3]);
|
||||
|
||||
// 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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public int getIban() {
|
||||
return iban;
|
||||
}
|
||||
|
||||
public int getPin() {
|
||||
return pin;
|
||||
}
|
||||
|
||||
public double getBalance() {
|
||||
return balance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(iban);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the parameter is an instance of class {@link Account} and both their
|
||||
* ibans are equal.
|
||||
*
|
||||
* @param obj the obj to compare to
|
||||
* @return true if the parameter is an instance of class {@link Account} and their ibans match
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Account)
|
||||
return iban == ((Account) obj).iban;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a description of the account in form a string.
|
||||
* This method is not equal to {@link #toString()} but is intended to be
|
||||
* a method used to retrieve a formatted representation for guis.
|
||||
*
|
||||
* @return a basic description of the account in form a string
|
||||
*/
|
||||
public String getDescription() {
|
||||
return String.format("%s (%s)", iban, translate(getClass().getSimpleName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a non-negative value onto the balance of this account.
|
||||
*
|
||||
* @param amount the amount of value to add
|
||||
* @throws IllegalArgumentException if amount is negative
|
||||
*/
|
||||
public void deposit(double amount) throws IllegalArgumentException {
|
||||
if (amount < 0) {
|
||||
Logging.LOGGER.severe("Cannot deposit negative amount of money: " + amount);
|
||||
throw new IllegalArgumentException("amount must be positive");
|
||||
}
|
||||
|
||||
this.balance += amount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("@Account [iban=%d, pin=%d, balance=%.2f]", iban, pin, balance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takeoff a certain amount of money from this accounts balance.
|
||||
* Saturates the result if the value to subtract is greater than the balance present.
|
||||
*
|
||||
* @param amount the amount of money to subtract from the accounts balance
|
||||
* @throws IllegalArgumentException if amount is greater than the balance present
|
||||
*/
|
||||
public void takeoff(double amount) throws IllegalArgumentException {
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
package me.teridax.jcash.banking.accounts;
|
||||
|
||||
import me.teridax.jcash.Logging;
|
||||
|
||||
/**
|
||||
* Immutable currency account storing only overdraft.
|
||||
* English equivalent to "Girokonto"
|
||||
*/
|
||||
public final class CurrentAccount extends Account {
|
||||
|
||||
/**
|
||||
* Overdraft amount in currency.
|
||||
*/
|
||||
private final double overdraft;
|
||||
|
||||
public CurrentAccount(int iban, int pin, double balance, double overdraft) {
|
||||
super(iban, pin, balance);
|
||||
this.overdraft = overdraft;
|
||||
}
|
||||
|
||||
public double getOverdraft() {
|
||||
return overdraft;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takeoff a certain amount of money from this accounts balance.
|
||||
* Saturates the result if the value to subtract is greater than the balance present.
|
||||
*
|
||||
* @param amount the amount of money to subtract from the accounts balance
|
||||
* @throws IllegalArgumentException if amount is smaller than 0 or the overflow is greater than the overdraft.
|
||||
*/
|
||||
@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);
|
||||
|
||||
if (overflow > overdraft) {
|
||||
Logging.LOGGER.warning("amount to takeoff greater than overdraft");
|
||||
throw new IllegalArgumentException("amount to takeoff greater than overdraft");
|
||||
}
|
||||
}
|
||||
|
||||
this.balance -= amount;
|
||||
}
|
||||
}
|
|
@ -1,162 +0,0 @@
|
|||
package me.teridax.jcash.banking.management;
|
||||
|
||||
import me.teridax.jcash.banking.Bank;
|
||||
import me.teridax.jcash.banking.accounts.Account;
|
||||
import me.teridax.jcash.banking.accounts.Owner;
|
||||
import me.teridax.jcash.decode.StringDecoder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
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
|
||||
* principles.
|
||||
*/
|
||||
public final class BankingManagementSystem {
|
||||
|
||||
/**
|
||||
* Separator used to separate columns of CSV files
|
||||
*/
|
||||
private static final String SEPARATOR = ";";
|
||||
/**
|
||||
* Charsets to try when decoding the source file
|
||||
*/
|
||||
private static final Charset[] ENCODINGS = {
|
||||
StandardCharsets.UTF_8,
|
||||
Charset.forName("windows-1252")
|
||||
};
|
||||
|
||||
/**
|
||||
* A set of banks
|
||||
*/
|
||||
private final Set<Bank> banks;
|
||||
|
||||
private BankingManagementSystem() {
|
||||
this.banks = new HashSet<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method for retrieving the tail of a string array.
|
||||
* This method return all items which follow the n-th index denoted by index.
|
||||
*
|
||||
* @param array the array to take the tail of
|
||||
* @param index the amount trailing indices to skip
|
||||
* @return an array containing the last elements of the supplied array
|
||||
*/
|
||||
private static String[] tail(String[] array, int index) {
|
||||
return Arrays.stream(array).skip(index).toArray(String[]::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an instance of this class from a csv file.
|
||||
* The csv file must comply to the formats of {@link StringDecoder} and must contain
|
||||
* the following columns in this exact order:
|
||||
* <ol>
|
||||
* <li>Bank name</li>
|
||||
* <li>Bank BLZ</li>
|
||||
* <li>Account IBAN</li>
|
||||
* <li>Account PIN</li>
|
||||
* <li>Account Balance</li>
|
||||
* <li>Account type</li>
|
||||
* <li>Account Interest rate</li>
|
||||
* <li>Account Overdraft</li>
|
||||
* <li>Owner uuid</li>
|
||||
* <li>Owner family name</li>
|
||||
* <li>Owner name</li>
|
||||
* <li>Owner street</li>
|
||||
* <li>Owner plz</li>
|
||||
* <li>Owner city</li>
|
||||
* </ol>
|
||||
* The file can contain a header line which gets skipped when reading.
|
||||
* Note that the first line is always skipped and the name of every column is not checked in any way.
|
||||
*
|
||||
* @param file the file to parse
|
||||
* @return a valid BMS
|
||||
* @throws IllegalArgumentException if the file cannot be read or the containing data is invalid
|
||||
*/
|
||||
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 = getSource(file);
|
||||
|
||||
// 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(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));
|
||||
|
||||
var blz = Bank.validateBlz(columns[1]);
|
||||
var name = StringDecoder.decodeName(columns[0]);
|
||||
var bankOfLine = new Bank(blz, name);
|
||||
|
||||
// add banks to bms
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
return bms;
|
||||
|
||||
} catch (IllegalArgumentException | NullPointerException e) {
|
||||
LOGGER.severe("Could not parse file: " + file + " due to: " + e.getMessage());
|
||||
throw new IllegalArgumentException("Could not parse file " + file, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to read the entire file into a string.
|
||||
* This method tires out all encodings in {@link #ENCODINGS}
|
||||
* @param file the file to read
|
||||
* @throws IllegalArgumentException if the file cannot be read
|
||||
* @return the content of the file
|
||||
*/
|
||||
private static String getSource(Path file) throws IllegalArgumentException {
|
||||
Exception lastException = null;
|
||||
for (var encoding : ENCODINGS) {
|
||||
try {
|
||||
return Files.readString(file, encoding);
|
||||
} catch (IOException e) {
|
||||
LOGGER.severe("Could not read file: " + file + " due to: " + e.getMessage());
|
||||
lastException = e;
|
||||
}
|
||||
}
|
||||
assert lastException != null;
|
||||
throw new IllegalArgumentException("Invalid encoding, or IO exception: " + lastException.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a bank with the given blz.
|
||||
*
|
||||
* @param blz the blz to search bank of
|
||||
* @return the bank with this blz or none
|
||||
*/
|
||||
public Optional<Bank> getBank(String blz) {
|
||||
return this.banks.stream().filter(b -> b.getBlz().equals(blz)).findFirst();
|
||||
}
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
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;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Groups an owner and all of its accounts registered at a specific bank together.
|
||||
* The profile is oriented around a primary account of the owner.
|
||||
* This class is meant to be a read only reference for easier runtime processing of
|
||||
* data packed into nested structure of objects.
|
||||
*/
|
||||
public class Profile {
|
||||
|
||||
/**
|
||||
* Owner of the primary account and all other accounts registered at a specific bank
|
||||
*/
|
||||
private final Owner owner;
|
||||
/**
|
||||
* The bank that manages every account referenced by this profile
|
||||
*/
|
||||
private final Bank bank;
|
||||
/**
|
||||
* All other account registered at a specific bank for the specified owner
|
||||
*/
|
||||
private final Account[] accounts;
|
||||
/**
|
||||
* Primary or currently selected account.
|
||||
*/
|
||||
private Account primaryAccount;
|
||||
|
||||
public Profile(Owner owner, Bank bank, Account account, Account[] accounts) {
|
||||
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;
|
||||
this.accounts = accounts;
|
||||
this.primaryAccount = account;
|
||||
}
|
||||
|
||||
public Account getPrimaryAccount() {
|
||||
return primaryAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the primary account of this profile based on a descriptive text.
|
||||
* This method may not change anything if no account can be found with a matching description
|
||||
*
|
||||
* @param description the description to find a matching account for
|
||||
*/
|
||||
public void setPrimaryAccount(String description) {
|
||||
for (Account account : accounts) {
|
||||
if (account.getDescription().equals(description)) {
|
||||
this.primaryAccount = account;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Logging.LOGGER.warning("Account " + description + " not found in associated account list");
|
||||
}
|
||||
|
||||
public Owner getOwner() {
|
||||
return owner;
|
||||
}
|
||||
|
||||
public Bank getBank() {
|
||||
return bank;
|
||||
}
|
||||
|
||||
public Account[] getAccounts() {
|
||||
return accounts;
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
package me.teridax.jcash.decode;
|
||||
|
||||
import me.teridax.jcash.lang.Locales;
|
||||
import org.junit.Test;
|
||||
|
||||
import static junit.framework.TestCase.assertEquals;
|
||||
import static me.teridax.jcash.decode.StringDecoder.*;
|
||||
|
||||
public class DecodeTests {
|
||||
@Test
|
||||
public void testDecodeSuccessfulFunctions() {
|
||||
// make sure a comma separated integer from fractions
|
||||
Locales.setDefaultLocale("de", "DE");
|
||||
|
||||
assertEquals(decodePercent("1,003"), 100.3d, 1e-3d);
|
||||
|
||||
decodeUniqueIdentificationNumber("9578647895");
|
||||
decodeUniqueIdentificationNumber(" 927856347 ");
|
||||
decodeUniqueIdentificationNumber("0");
|
||||
decodeUniqueIdentificationNumber("'9578647895");
|
||||
|
||||
decodeName("Adolf");
|
||||
decodeName("Günther");
|
||||
decodeName("Saßkia");
|
||||
|
||||
decodeStreet("Bahnhofstraße 40/1");
|
||||
decodeStreet("Gülleweg 9");
|
||||
decodeStreet("Echsengaße 67 / 4");
|
||||
|
||||
assertEquals(decodePercent("1,4%"), 1.4, 1e-3d);
|
||||
assertEquals(decodePercent("99"), 9900.0d);
|
||||
assertEquals(decodePercent("1,003%"), 1.003, 1e-5d);
|
||||
assertEquals(decodePercent("1,003"), 100.3, 1e-5d);
|
||||
assertEquals(decodePercent("'1,003"), 100.3, 1e-5d);
|
||||
|
||||
assertEquals(decodeCurrency("1,3€"), 1.3d);
|
||||
assertEquals(decodeCurrency("‘0567€"), 567d);
|
||||
assertEquals(decodeCurrency("145,34"), 145, 34d);
|
||||
assertEquals(decodeCurrency("0,45 €"), 0.45d);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeLocaleDependent() {
|
||||
Locales.setDefaultLocale("de", "DE");
|
||||
assertEquals(decodeCurrency("13.00,45€"), 1300.45);
|
||||
|
||||
Locales.setDefaultLocale("en", "EN");
|
||||
assertEquals(decodeCurrency("13,00.45€"), 1300.45);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testDecodeInvalidFunctions() {
|
||||
decodeUniqueIdentificationNumber("q0948tvb6q047t 740 t74z0tz 784");
|
||||
decodeUniqueIdentificationNumber("-39867.8475");
|
||||
decodeUniqueIdentificationNumber("-398678475");
|
||||
decodeUniqueIdentificationNumber(" ß9qu908t76q34798t6q734vb9843");
|
||||
|
||||
decodeName("John Doe");
|
||||
decodeName("3490qt67v 0b34");
|
||||
decodeName("Alexander9");
|
||||
decodeName("-ga76re78g6$§");
|
||||
|
||||
decodeStreet("Bahnhofstraße -40/1");
|
||||
decodeStreet("Gülleweg 9//567");
|
||||
decodeStreet("23Echsengaße 67 / 4 Hofwetg 9");
|
||||
}
|
||||
}
|
|
@ -1,105 +1,63 @@
|
|||
package me.teridax.jcash.decode;
|
||||
|
||||
import me.teridax.jcash.lang.Locales;
|
||||
import org.junit.Test;
|
||||
|
||||
import javax.swing.text.NumberFormatter;
|
||||
import java.text.NumberFormat;
|
||||
import java.text.ParseException;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static junit.framework.TestCase.assertEquals;
|
||||
|
||||
/**
|
||||
* Utility class for converting various single line strings into a specific data type according
|
||||
* to a format mostly dictated by a locale.
|
||||
*/
|
||||
public class StringDecoder {
|
||||
|
||||
public static NumberFormat getNumberFormat() {
|
||||
return NumberFormat.getInstance(Locales.getDefaultLocale());
|
||||
}
|
||||
public class StringUtils {
|
||||
|
||||
/**
|
||||
* Returns a NumberFormatter for parsing double values in the appropriate locale.
|
||||
* @return the number formatter
|
||||
* Locale to use when converting strings
|
||||
*/
|
||||
public static NumberFormatter getNumberFormatter(double maxValue) {
|
||||
var formatter = new NumberFormatter();
|
||||
formatter.setValueClass(Double.class);
|
||||
formatter.setMinimum(0d);
|
||||
formatter.setMaximum(maxValue);
|
||||
formatter.setAllowsInvalid(true);
|
||||
formatter.setCommitsOnValidEdit(true);
|
||||
|
||||
return formatter;
|
||||
}
|
||||
|
||||
private static final Locale LOCALE = Locale.GERMANY;
|
||||
/**
|
||||
* Returns a NumberFormatter for parsing integer values in the appropriate locale.
|
||||
* @return the number formatter
|
||||
* NumberFormat to use when converting strings
|
||||
*/
|
||||
public static NumberFormatter getIntegerNumberFormatter() {
|
||||
var formatter = new NumberFormatter();
|
||||
formatter.setValueClass(Integer.class);
|
||||
formatter.setMinimum(0d);
|
||||
formatter.setAllowsInvalid(true);
|
||||
formatter.setCommitsOnValidEdit(true);
|
||||
|
||||
return formatter;
|
||||
}
|
||||
public static final NumberFormat LOCAL_NUMBER_FORMAT = NumberFormat.getInstance(LOCALE);
|
||||
|
||||
/**
|
||||
* Attempts to convert the given string into a double value representing a percentage.
|
||||
* The output value will be in the range [0, 100]. Strings formatted without a percentage
|
||||
* symbol will be assumed to be normalized and thus multiplied by 100 to retrieve the result in percent.
|
||||
*
|
||||
* The percentage is stored in the range [0,1] and can linearly be mapped to [0, 100] by multiplying with 100.
|
||||
* @param number the string to convert
|
||||
* @return the double value
|
||||
* @throws IllegalArgumentException when the format is invalid
|
||||
* @throws NullPointerException when the argument is null
|
||||
* @throws NullPointerException when the argument is null
|
||||
*/
|
||||
public static double decodePercent(String number) throws IllegalArgumentException, NullPointerException {
|
||||
Objects.requireNonNull(number);
|
||||
|
||||
// trim and cut out weird leading single quotes for numbers
|
||||
var prepared = number.trim().replaceAll("^\\s*['‘`](?=\\d)", "");
|
||||
// trim the number and cut out optional percent symbols
|
||||
var trimmed = number.trim().replace("%", "");
|
||||
|
||||
var pattern = Pattern.compile("^([^%]+)?(%)?$", Pattern.CASE_INSENSITIVE);
|
||||
var matcher = pattern.matcher(prepared);
|
||||
if (matcher.find()) {
|
||||
// if no percentage symbol is given the number will be multiplied by 100%
|
||||
var scale = 1e2;
|
||||
|
||||
// check if capture group 2 captured a percentage symbol
|
||||
// if to we don't want to apply any scaling to the value
|
||||
if (null != matcher.group(2))
|
||||
scale = 1;
|
||||
|
||||
try {
|
||||
return getNumberFormat().parse(matcher.group(1)).doubleValue() * scale;
|
||||
} catch (ParseException ex) {
|
||||
throw new IllegalArgumentException("Not a valid number: " + number, ex);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("not a valid percentage");
|
||||
try {
|
||||
return LOCAL_NUMBER_FORMAT.parse(trimmed).doubleValue();
|
||||
} catch (ParseException ex) {
|
||||
throw new IllegalArgumentException("Not a valid number: " + number, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to convert the given string into a currency value.
|
||||
*
|
||||
* @param currency the string to convert
|
||||
* @return the double value
|
||||
* @throws IllegalArgumentException when the format is invalid
|
||||
* @throws NullPointerException when the argument is null
|
||||
* @throws NullPointerException when the argument is null
|
||||
*/
|
||||
public static double decodeCurrency(String currency) throws IllegalArgumentException, NullPointerException {
|
||||
Objects.requireNonNull(currency);
|
||||
|
||||
// trim and cut out weird leading single quotes for numbers
|
||||
var prepared = currency.trim().replaceAll("^\\s*['‘`](?=\\d)", "");
|
||||
|
||||
try {
|
||||
return getNumberFormat().parse(prepared).doubleValue();
|
||||
return LOCAL_NUMBER_FORMAT.parse(currency.trim()).doubleValue();
|
||||
} catch (ParseException ex) {
|
||||
throw new IllegalArgumentException("Not a valid currency in german locale: " + currency, ex);
|
||||
}
|
||||
|
@ -108,21 +66,19 @@ public class StringDecoder {
|
|||
/**
|
||||
* Attempts to convert the given string into universally unique number.
|
||||
* This function does not check for duplicates. The number must be a positive integer.
|
||||
*
|
||||
* @param number the string to convert
|
||||
* @return the integer serial number
|
||||
* @throws IllegalArgumentException when the format is invalid
|
||||
* @throws NullPointerException when the argument is null
|
||||
* @throws NullPointerException when the argument is null
|
||||
*/
|
||||
public static int decodeUniqueIdentificationNumber(String number) throws IllegalArgumentException, NullPointerException {
|
||||
Objects.requireNonNull(number);
|
||||
|
||||
// trim and cut out weird leading single quotes for numbers
|
||||
var preparedUID = number.trim().replaceAll("^\\s*['‘`](?=\\d)", "");
|
||||
|
||||
// check if the string is a valid unsigned number
|
||||
try {
|
||||
var serialNumber = getNumberFormat().parse(preparedUID);
|
||||
LOCAL_NUMBER_FORMAT.setParseIntegerOnly(true);
|
||||
var serialNumber = LOCAL_NUMBER_FORMAT.parse(number.trim());
|
||||
LOCAL_NUMBER_FORMAT.setParseIntegerOnly(false);
|
||||
|
||||
if (serialNumber.intValue() < 0)
|
||||
throw new IllegalArgumentException("Not a valid unique identification number: " + number);
|
||||
|
@ -136,18 +92,17 @@ public class StringDecoder {
|
|||
/**
|
||||
* Attempts to convert the given string into a name.
|
||||
* This method performs validation and trimming.
|
||||
*
|
||||
* @param name the string to convert
|
||||
* @return the qualified name
|
||||
* @throws IllegalArgumentException when the format is invalid
|
||||
* @throws NullPointerException when the argument is null
|
||||
* @throws NullPointerException when the argument is null
|
||||
*/
|
||||
public static String decodeName(String name) throws IllegalArgumentException, NullPointerException {
|
||||
Objects.requireNonNull(name);
|
||||
|
||||
var trimmed = name.trim();
|
||||
|
||||
var pattern = Pattern.compile("[^\\d]+", Pattern.CASE_INSENSITIVE);
|
||||
var pattern = Pattern.compile("[\\w-\\s]+", Pattern.CASE_INSENSITIVE);
|
||||
var matcher = pattern.matcher(trimmed);
|
||||
if (matcher.find()) {
|
||||
return matcher.group();
|
||||
|
@ -158,16 +113,15 @@ public class StringDecoder {
|
|||
|
||||
/**
|
||||
* Attempts to convert the given string into a street and an optional house address.
|
||||
*
|
||||
* @param street the string to convert
|
||||
* @return the address name
|
||||
* @throws IllegalArgumentException when the format is invalid
|
||||
* @throws NullPointerException when the argument is null
|
||||
* @throws NullPointerException when the argument is null
|
||||
*/
|
||||
public static String decodeStreet(String street) throws IllegalArgumentException, NullPointerException {
|
||||
Objects.requireNonNull(street);
|
||||
|
||||
var pattern = Pattern.compile("\\S+(\\s+\\d+(\\s*/\\s*\\d+)?)?", Pattern.CASE_INSENSITIVE);
|
||||
var pattern = Pattern.compile("\\S+(\\s+\\d+(/\\d+)?)?", Pattern.CASE_INSENSITIVE);
|
||||
var matcher = pattern.matcher(street);
|
||||
if (matcher.find()) {
|
||||
return matcher.group();
|
||||
|
@ -175,4 +129,44 @@ public class StringDecoder {
|
|||
throw new IllegalArgumentException("not a void address");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecodeSuccessfulFunctions() {
|
||||
decodeUniqueIdentificationNumber("95786978625347895");
|
||||
decodeUniqueIdentificationNumber(" 927856347 ");
|
||||
decodeUniqueIdentificationNumber("0");
|
||||
|
||||
decodeName("Adolf");
|
||||
decodeName("Günther");
|
||||
decodeName("Saßkia");
|
||||
|
||||
decodeStreet("Bahnhofstraße 40/1");
|
||||
decodeStreet("Gülleweg 9");
|
||||
decodeStreet("Echsengaße 67 / 4");
|
||||
|
||||
assertEquals(decodePercent("1,4%"), 1.4d);
|
||||
assertEquals(decodePercent("99"), 99.0d);
|
||||
assertEquals(decodePercent("1,003 %"), 1.003d);
|
||||
|
||||
assertEquals(decodeCurrency("1,3€"), 1.3d);
|
||||
assertEquals(decodeCurrency("145,34"), 145,34d);
|
||||
assertEquals(decodeCurrency("0,45 €"), 0.45d);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testDecodeInvalidFunctions() {
|
||||
decodeUniqueIdentificationNumber("q0948tvb6q047t 740 t74z0tz 784");
|
||||
decodeUniqueIdentificationNumber("-39867.8475");
|
||||
decodeUniqueIdentificationNumber("-398678475");
|
||||
decodeUniqueIdentificationNumber(" ß9qu908t76q34798t6q734vb9843");
|
||||
|
||||
decodeName("John Doe");
|
||||
decodeName("3490qt67v 0b34");
|
||||
decodeName("Alexander9");
|
||||
decodeName("-ga76re78g6$§");
|
||||
|
||||
decodeStreet("Bahnhofstraße -40/1");
|
||||
decodeStreet("Gülleweg 9//567");
|
||||
decodeStreet("23Echsengaße 67 / 4 Hofwetg 9");
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
package me.teridax.jcash.gui;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Objects;
|
||||
|
||||
import static me.teridax.jcash.Logging.LOGGER;
|
||||
|
||||
/**
|
||||
* Static class for providing the capabilities to load images from file.
|
||||
*/
|
||||
public class IconProvider {
|
||||
|
||||
private static final Image DEFAULT_IMAGE = new BufferedImage(256, 256, BufferedImage.TYPE_INT_RGB);
|
||||
|
||||
/**
|
||||
* Fetches the windows icon.
|
||||
* @return the windows icon
|
||||
*/
|
||||
public static Image getWindowIcon() {
|
||||
return loadIcon("res/register.png");
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the specified image from disk.
|
||||
* If the file cannot be made into an image because its corrupted or the file cannot be read,
|
||||
* the default image is returned {@link #DEFAULT_IMAGE}
|
||||
* @param path the path to the image
|
||||
* @return the specified image or {@link #DEFAULT_IMAGE}
|
||||
*/
|
||||
private static Image loadIcon(String path) {
|
||||
try {
|
||||
var is = Objects.requireNonNull(IconProvider.class.getResourceAsStream(path));
|
||||
return ImageIO.read(is);
|
||||
} catch (Exception e) {
|
||||
LOGGER.severe("Unable to load icon " + path + " because: " + e.getMessage());
|
||||
}
|
||||
|
||||
return DEFAULT_IMAGE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the background image used for the login screen
|
||||
* @return login screen background image
|
||||
*/
|
||||
public static Image getBackground() {
|
||||
return loadIcon("res/background.png");
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package me.teridax.jcash.gui;
|
||||
|
||||
|
||||
/**
|
||||
* Exception thrown when some user input is invalid
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class InvalidInputException extends IllegalStateException {
|
||||
|
||||
public InvalidInputException(String message, Exception cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public InvalidInputException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public InvalidInputException(Exception cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
|
@ -1,14 +1,12 @@
|
|||
package me.teridax.jcash.gui;
|
||||
|
||||
import me.teridax.jcash.Logging;
|
||||
import me.teridax.jcash.Main;
|
||||
import me.teridax.jcash.banking.management.BankingManagementSystem;
|
||||
import me.teridax.jcash.banking.BankingManagementSystem;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||
import java.io.File;
|
||||
|
||||
import static javax.swing.JFileChooser.APPROVE_OPTION;
|
||||
import static me.teridax.jcash.lang.Translator.translate;
|
||||
|
||||
/**
|
||||
* Utility class for loading a BMS configuration from a csv file.
|
||||
|
@ -18,15 +16,13 @@ public class Loader {
|
|||
/**
|
||||
* Filter that only allows for files with *.csv extension
|
||||
*/
|
||||
private static final FileNameExtensionFilter FILE_FILTER = new FileNameExtensionFilter(translate("Comma separated value spreadsheet"), "csv", "CSV");
|
||||
private static final FileNameExtensionFilter FILE_FILTER = new FileNameExtensionFilter("Comma separated value spreadsheet", "csv", "CSV");
|
||||
|
||||
private Loader() {
|
||||
}
|
||||
private Loader() {}
|
||||
|
||||
/**
|
||||
* Load a BMS from a csv file. Opens up a dialog which prompts the user to select a single file.
|
||||
* Once the file is selected this function will try to parse the contents to a BMS and return the instance.
|
||||
*
|
||||
* @return a valid BMS instance loaded from a file
|
||||
* @throws IllegalStateException When either no file is selected or the selected files content is invalid
|
||||
*/
|
||||
|
@ -37,8 +33,9 @@ public class Loader {
|
|||
fileChooser.setFileFilter(FILE_FILTER);
|
||||
fileChooser.setDialogType(JFileChooser.OPEN_DIALOG);
|
||||
fileChooser.setAcceptAllFileFilterUsed(false);
|
||||
fileChooser.setCurrentDirectory(new File("/home/teridax/IdeaProjects/JCash/res"));
|
||||
|
||||
if (fileChooser.showDialog(Main.getInstance().getWindow(), translate("Load database")) == APPROVE_OPTION) {
|
||||
if (fileChooser.showDialog(null, "Load database") == APPROVE_OPTION) {
|
||||
// parse file content
|
||||
try {
|
||||
return BankingManagementSystem.loadFromCsv(fileChooser.getSelectedFile().toPath());
|
||||
|
@ -47,7 +44,6 @@ public class Loader {
|
|||
}
|
||||
}
|
||||
|
||||
Logging.LOGGER.warning("no file selected");
|
||||
throw new IllegalStateException("No file selected");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,128 +0,0 @@
|
|||
package me.teridax.jcash.gui;
|
||||
|
||||
import me.teridax.jcash.banking.management.BankingManagementSystem;
|
||||
import me.teridax.jcash.gui.account.AccountController;
|
||||
import me.teridax.jcash.gui.login.LoginController;
|
||||
import me.teridax.jcash.lang.Locales;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.util.Objects;
|
||||
|
||||
import static me.teridax.jcash.Logging.LOGGER;
|
||||
import static me.teridax.jcash.lang.Translator.translate;
|
||||
|
||||
public class MainFrame {
|
||||
|
||||
/**
|
||||
* Constant used to identify the login screen on the cardlayout
|
||||
*/
|
||||
private static final String LOGIN_SCREEN_STRING_IDENT = "LoginScreen";
|
||||
/**
|
||||
* Constant used to identify the profile screen on the cardlayout
|
||||
*/
|
||||
private static final String PROFILE_SCREEN_STRING_IDENT = "ProfileScreen";
|
||||
/**
|
||||
* Version of this application
|
||||
*/
|
||||
private static final String VERSION = "v2.1.0";
|
||||
|
||||
/**
|
||||
* Primary window of this program
|
||||
*/
|
||||
private final JFrame window;
|
||||
/**
|
||||
* Primary layout of this application
|
||||
*/
|
||||
private final CardLayout layout;
|
||||
|
||||
/**
|
||||
* Database containing every bank, account and owner available
|
||||
*/
|
||||
private BankingManagementSystem bms;
|
||||
|
||||
private LoginController loginMask;
|
||||
private AccountController accountController;
|
||||
|
||||
public MainFrame() {
|
||||
// create main window and set defaults
|
||||
this.window = new JFrame();
|
||||
this.window.setTitle(translate("Cashmachine") + getInfoString());
|
||||
this.window.setLocationByPlatform(true);
|
||||
this.window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
this.window.setIconImage(IconProvider.getWindowIcon());
|
||||
|
||||
this.layout = new CardLayout();
|
||||
this.window.getContentPane().setLayout(this.layout);
|
||||
|
||||
initialize();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates and returns a general information string about this application
|
||||
* @return the locale and the current version as string
|
||||
*/
|
||||
private String getInfoString() {
|
||||
return " locale: [" + Locales.getDefaultLocale().toString() + "] " + VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the GUI components of login screen and profile view
|
||||
*/
|
||||
private void initialize() {
|
||||
// create the login mask
|
||||
|
||||
this.loginMask = new LoginController();
|
||||
|
||||
// when we have logged in set the account viewer as window content
|
||||
this.loginMask.addAccountSelectionListener(account -> {
|
||||
LOGGER.finer("account selected: " + Objects.toString(account, "null"));
|
||||
accountController.setProfile(account, bms);
|
||||
layout.show(window.getContentPane(), PROFILE_SCREEN_STRING_IDENT);
|
||||
});
|
||||
|
||||
this.window.getContentPane().add(loginMask.getView(), LOGIN_SCREEN_STRING_IDENT);
|
||||
|
||||
// create the account viewer
|
||||
|
||||
this.accountController = new AccountController();
|
||||
this.window.getContentPane().add(accountController.getView(), PROFILE_SCREEN_STRING_IDENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the BMS of this application to use for the GUI.
|
||||
* This method will show the login screen to the user
|
||||
* @param bms the BMS to use for the GUI
|
||||
*/
|
||||
public void setBms(BankingManagementSystem bms) {
|
||||
this.bms = bms;
|
||||
this.loginMask.setBankingManagementSystem(bms);
|
||||
this.showLoginScreen();
|
||||
this.window.pack();
|
||||
this.window.setResizable(false);
|
||||
this.window.setLocationRelativeTo(null);
|
||||
this.window.setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the open dialog for selecting a database file. After selection, it then proceeds to prompt login.
|
||||
* Afterward the selected account can be managed.
|
||||
* This method is non-blocking and all work described is performed asynchronously on the AWT Event dispatcher.
|
||||
*/
|
||||
private void showLoginScreen() {
|
||||
this.layout.show(this.window.getContentPane(), LOGIN_SCREEN_STRING_IDENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the user out of the database, hiding the main window.
|
||||
*/
|
||||
public void logout() {
|
||||
this.loginMask.logout();
|
||||
this.layout.show(this.window.getContentPane(), LOGIN_SCREEN_STRING_IDENT);
|
||||
}
|
||||
|
||||
public JFrame getWindow() {
|
||||
return this.window;
|
||||
}
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
package me.teridax.jcash.gui;
|
||||
|
||||
import me.teridax.jcash.Main;
|
||||
import me.teridax.jcash.lang.Locales;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.text.Format;
|
||||
import java.text.NumberFormat;
|
||||
|
||||
public class Utils {
|
||||
|
||||
/**
|
||||
* Formats the string so that it will be displayed as a Heading 1 element by JLabels.
|
||||
* This embeds the given string into two html tags.
|
||||
* Note that eny html entities in the string will be formatted as valid HTML entities.
|
||||
* Meaning they won't show up in as plain text.
|
||||
* @param title the title to format.
|
||||
* @return the given string embedded into <pre><html><h1>$string</h1></html></pre>
|
||||
*/
|
||||
public static String addHeading(String title) {
|
||||
return String.format("<html><h1>%s</h1></html>", title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new row of components to the specified target component.
|
||||
* This will add label to the right side of the next row and the specified component to the left.
|
||||
*
|
||||
* @param constraints the constraint to use. Must be non-null
|
||||
* @param target the base component to add a row to
|
||||
* @param comp the component to add to the left side
|
||||
* @param row the row to add the components to
|
||||
* @param name the labels text to add to the left side
|
||||
*/
|
||||
public static void addGridBagRow(GridBagConstraints constraints, JComponent target, JComponent comp, int row, String name) {
|
||||
constraints.gridwidth = 1;
|
||||
constraints.gridx = 1;
|
||||
constraints.gridy = row;
|
||||
constraints.weightx = 0;
|
||||
constraints.fill = GridBagConstraints.HORIZONTAL;
|
||||
target.add(new JLabel(name, SwingConstants.RIGHT), constraints);
|
||||
|
||||
if (comp == null)
|
||||
return;
|
||||
|
||||
constraints.gridx = 2;
|
||||
constraints.gridy = row;
|
||||
constraints.weightx = 1;
|
||||
constraints.fill = GridBagConstraints.HORIZONTAL;
|
||||
target.add(comp, constraints);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new row of components to the specified target component.
|
||||
* This will add label to the right side of the next row and the specified component to the left.
|
||||
*
|
||||
* @param constraints the constraint to use. Must be non-null
|
||||
* @param target the base component to add a row to
|
||||
* @param comp the component to add to the left side
|
||||
* @param row the row to add the components to
|
||||
* @param right the component to place on the left side
|
||||
*/
|
||||
public static void addGridBagRow(GridBagConstraints constraints, JComponent target, JComponent comp, int row, Component right) {
|
||||
constraints.gridwidth = 1;
|
||||
constraints.gridx = 1;
|
||||
constraints.gridy = row;
|
||||
constraints.weightx = 0;
|
||||
constraints.fill = GridBagConstraints.HORIZONTAL;
|
||||
target.add(right, constraints);
|
||||
|
||||
constraints.gridx = 2;
|
||||
constraints.gridy = row;
|
||||
constraints.weightx = 1;
|
||||
constraints.fill = GridBagConstraints.HORIZONTAL;
|
||||
target.add(comp, constraints);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens an error message dialog. This function will block the calling thread until the error message
|
||||
* disposed.
|
||||
* @param message the message to show to the user
|
||||
*/
|
||||
public static void error(String message) {
|
||||
JOptionPane.showMessageDialog(Main.getInstance().getWindow(), message, "Error occurred", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
|
@ -1,90 +1,45 @@
|
|||
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;
|
||||
import me.teridax.jcash.gui.deposit.DepositController;
|
||||
import me.teridax.jcash.gui.takeoff.TakeoffController;
|
||||
import me.teridax.jcash.gui.transfer.TransferController;
|
||||
import me.teridax.jcash.banking.Profile;
|
||||
import me.teridax.jcash.gui.deposit.DepositDialog;
|
||||
import me.teridax.jcash.gui.takeoff.TakeoffDialog;
|
||||
import me.teridax.jcash.gui.transfer.TransferDialog;
|
||||
|
||||
/**
|
||||
* Controller for controlling the gui of an account.
|
||||
*/
|
||||
public class AccountController {
|
||||
private final AccountData data;
|
||||
/**
|
||||
* GUI if an account
|
||||
*/
|
||||
|
||||
private final AccountView view;
|
||||
|
||||
private Profile profile;
|
||||
|
||||
public AccountController() {
|
||||
public AccountController(Profile profile) {
|
||||
this.view = new AccountView();
|
||||
this.data = new AccountData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the profile and BMS used to manage banking.
|
||||
* @param profile the profile used to manage the account
|
||||
* @param bms the BMS used access other banking accounts
|
||||
*/
|
||||
public void setProfile(Profile profile, BankingManagementSystem bms) {
|
||||
this.profile = profile;
|
||||
this.view.setProfile(profile);
|
||||
this.data.setBms(bms);
|
||||
this.createListeners();
|
||||
|
||||
createListeners(profile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create listeners for GUI components
|
||||
*/
|
||||
private void createListeners() {
|
||||
this.view.getAccountSelection().addActionListener(e -> changeAccount());
|
||||
this.view.getLogout().addActionListener(e -> logout());
|
||||
this.view.getDeposit().addActionListener(e -> depositMoney());
|
||||
this.view.getTakeoff().addActionListener(e -> takeoffMoney());
|
||||
this.view.getTransfer().addActionListener(e -> transferMoney());
|
||||
}
|
||||
private void createListeners(Profile profile) {
|
||||
this.view.getAccountSelection().addActionListener(e -> {
|
||||
var description = ((String) this.view.getAccountSelection().getSelectedItem());
|
||||
profile.setPrimaryAccount(description);
|
||||
this.view.setProfile(profile);
|
||||
});
|
||||
|
||||
/**
|
||||
* Open dialog to deposit money
|
||||
*/
|
||||
private void depositMoney() {
|
||||
new DepositController(profile.getPrimaryAccount());
|
||||
this.view.updateAccountVariables(profile);
|
||||
}
|
||||
this.view.getLogout().addActionListener(e -> {
|
||||
Main.getInstance().logout();
|
||||
Main.getInstance().showLoginScreen();
|
||||
});
|
||||
|
||||
/**
|
||||
* Open dialog to transfer money
|
||||
*/
|
||||
private void transferMoney() {
|
||||
new TransferController(profile.getPrimaryAccount(), data.getBms());
|
||||
this.view.updateAccountVariables(profile);
|
||||
}
|
||||
this.view.getDeposit().addActionListener(e -> {
|
||||
new DepositDialog(profile.getPrimaryAccount(), () -> this.view.setProfile(profile));
|
||||
});
|
||||
|
||||
/**
|
||||
* Open dialog to take off money
|
||||
*/
|
||||
private void takeoffMoney() {
|
||||
new TakeoffController(profile.getPrimaryAccount());
|
||||
this.view.updateAccountVariables(profile);
|
||||
}
|
||||
this.view.getTakeoff().addActionListener(e -> {
|
||||
new TakeoffDialog(profile.getPrimaryAccount(), () -> this.view.setProfile(profile));
|
||||
});
|
||||
|
||||
private void logout() {
|
||||
Logging.LOGGER.fine("Logging out of account");
|
||||
Main.getInstance().logout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the selected account.
|
||||
*/
|
||||
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);
|
||||
this.view.getTransfer().addActionListener(e -> {
|
||||
new TransferDialog(profile.getPrimaryAccount(), () -> this.view.setProfile(profile));
|
||||
});
|
||||
}
|
||||
|
||||
public AccountView getView() {
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
package me.teridax.jcash.gui.account;
|
||||
|
||||
import me.teridax.jcash.banking.management.BankingManagementSystem;
|
||||
|
||||
/**
|
||||
* Data storage class for account management
|
||||
*/
|
||||
public class AccountData {
|
||||
|
||||
private BankingManagementSystem bms;
|
||||
|
||||
public BankingManagementSystem getBms() {
|
||||
return bms;
|
||||
}
|
||||
|
||||
public void setBms(BankingManagementSystem bms) {
|
||||
this.bms = bms;
|
||||
}
|
||||
}
|
|
@ -1,20 +1,10 @@
|
|||
package me.teridax.jcash.gui.account;
|
||||
|
||||
import me.teridax.jcash.Logging;
|
||||
import me.teridax.jcash.banking.accounts.Account;
|
||||
import me.teridax.jcash.banking.accounts.CurrentAccount;
|
||||
import me.teridax.jcash.banking.accounts.SavingsAccount;
|
||||
import me.teridax.jcash.banking.management.Profile;
|
||||
import me.teridax.jcash.decode.StringDecoder;
|
||||
import me.teridax.jcash.banking.*;
|
||||
import me.teridax.jcash.decode.StringUtils;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
|
||||
import static javax.swing.SwingConstants.RIGHT;
|
||||
import static me.teridax.jcash.gui.Utils.addGridBagRow;
|
||||
import static me.teridax.jcash.lang.Translator.translate;
|
||||
|
||||
public class AccountView extends JPanel {
|
||||
|
||||
|
@ -26,8 +16,13 @@ public class AccountView extends JPanel {
|
|||
private JTextField type;
|
||||
private JTextField typeSpecialProperty;
|
||||
private JTextField balance;
|
||||
|
||||
private JLabel typeSpecialLabel;
|
||||
|
||||
private JComboBox<String> accountSelection;
|
||||
|
||||
private JPanel content;
|
||||
|
||||
private JButton logout;
|
||||
private JButton transfer;
|
||||
private JButton deposit;
|
||||
|
@ -37,47 +32,62 @@ public class AccountView extends JPanel {
|
|||
createComponents();
|
||||
createLayout();
|
||||
|
||||
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
||||
setBorder(BorderFactory.createEmptyBorder(8,8,8,8));
|
||||
}
|
||||
|
||||
/**
|
||||
* The profile to manage via the GUI.
|
||||
* @param profile the profile to manage
|
||||
*/
|
||||
public void setProfile(Profile profile) {
|
||||
this.updateAccountVariables(profile);
|
||||
var bank = profile.getBank();
|
||||
var account = profile.getPrimaryAccount();
|
||||
var owner = profile.getOwner();
|
||||
|
||||
this.blz.setText(bank.getBlz());
|
||||
this.bankName.setText(bank.getName());
|
||||
|
||||
this.iban.setText(String.valueOf(account.getIban()));
|
||||
this.name.setText(owner.getName() + " " + owner.getFamilyName());
|
||||
this.address.setText(owner.getStreet() + " " + owner.getCity());
|
||||
|
||||
this.balance.setText(StringUtils.LOCAL_NUMBER_FORMAT.format(account.getBalance()) + " €");
|
||||
|
||||
this.type.setText(account.getClass().getSimpleName());
|
||||
if (account instanceof Girokonto) {
|
||||
this.typeSpecialLabel.setText("Overdraft");
|
||||
this.typeSpecialProperty.setText( StringUtils.LOCAL_NUMBER_FORMAT.format(((Girokonto) account).getOverdraft()) + " €");
|
||||
} else if (account instanceof SavingsAccount) {
|
||||
this.typeSpecialLabel.setText("Interest rate");
|
||||
this.typeSpecialProperty.setText( ((SavingsAccount) account).getInterest() + " %" );
|
||||
}
|
||||
|
||||
this.accountSelection.removeAllItems();
|
||||
|
||||
var accounts = profile.getAccounts();
|
||||
Arrays.stream(accounts).sorted(Comparator.comparingInt(Account::getIban)).forEach(a -> this.accountSelection.addItem(a.getDescription()));
|
||||
|
||||
this.accountSelection.setSelectedItem(profile.getPrimaryAccount().getDescription());
|
||||
for (var otherAccount : profile.getAccounts()) {
|
||||
this.accountSelection.addItem(otherAccount.getDescription());
|
||||
}
|
||||
this.accountSelection.setSelectedItem(account.getDescription());
|
||||
}
|
||||
|
||||
private void createLayout() {
|
||||
var content = new JPanel(new GridBagLayout());
|
||||
|
||||
this.setLayout(new BorderLayout(12, 12));
|
||||
this.add(new JScrollPane(content), BorderLayout.CENTER);
|
||||
setLayout(new BorderLayout(16, 16));
|
||||
add(new JScrollPane(content), BorderLayout.CENTER);
|
||||
|
||||
content.setLayout(new GridBagLayout());
|
||||
var constraints = new GridBagConstraints();
|
||||
|
||||
constraints.gridwidth = 4;
|
||||
constraints.insets = new Insets(12, 12, 12, 12);
|
||||
constraints.insets = new Insets(12,12,12,12);
|
||||
|
||||
var accountSelectionPanel = new JPanel(new BorderLayout(12, 0));
|
||||
accountSelectionPanel.add(iban, BorderLayout.CENTER);
|
||||
accountSelectionPanel.add(accountSelection, BorderLayout.EAST);
|
||||
|
||||
addGridBagRow(constraints, content, accountSelectionPanel, 1, translate("IBAN"));
|
||||
addGridBagRow(constraints, content, name, 2, translate("Name/Family-name"));
|
||||
addGridBagRow(constraints, content, address, 3, translate("Address"));
|
||||
addGridBagRow(constraints, content, bankName, 4, translate("Bank"));
|
||||
addGridBagRow(constraints, content, blz, 5, translate("BLZ"));
|
||||
addGridBagRow(constraints, content, type, 6, translate("Account"));
|
||||
addGridBagRow(constraints, content, typeSpecialProperty, 7, typeSpecialLabel);
|
||||
addGridBagRow(constraints, content, balance, 8, translate("Balance"));
|
||||
addInputRow(constraints, content, accountSelectionPanel, 1, new JLabel("IBAN", SwingConstants.RIGHT));
|
||||
addInputRow(constraints, content, name, 2, new JLabel("Name/Family-name", SwingConstants.RIGHT));
|
||||
addInputRow(constraints, content, address, 3, new JLabel("Address", SwingConstants.RIGHT));
|
||||
addInputRow(constraints, content, bankName, 4, new JLabel("Bank", SwingConstants.RIGHT));
|
||||
addInputRow(constraints, content, blz, 5, new JLabel("BLZ", SwingConstants.RIGHT));
|
||||
addInputRow(constraints, content, type, 6, new JLabel("Account", SwingConstants.RIGHT));
|
||||
addInputRow(constraints, content, typeSpecialProperty, 7, typeSpecialLabel);
|
||||
addInputRow(constraints, content, balance, 8, new JLabel("Balance", SwingConstants.RIGHT));
|
||||
|
||||
var buttonPanel = Box.createHorizontalBox();
|
||||
buttonPanel.add(Box.createHorizontalStrut(4));
|
||||
|
@ -111,14 +121,31 @@ public class AccountView extends JPanel {
|
|||
this.balance.setEditable(false);
|
||||
this.typeSpecialProperty.setEditable(false);
|
||||
|
||||
this.typeSpecialLabel = new JLabel("", RIGHT);
|
||||
this.typeSpecialLabel = new JLabel("", SwingConstants.RIGHT);
|
||||
|
||||
this.accountSelection = new JComboBox<>();
|
||||
|
||||
this.logout = new JButton(translate("Logout"));
|
||||
this.transfer = new JButton(translate("Transfer"));
|
||||
this.deposit = new JButton(translate("Deposit"));
|
||||
this.takeoff = new JButton(translate("Takeoff"));
|
||||
this.content = new JPanel();
|
||||
|
||||
this.logout = new JButton("Logout");
|
||||
this.transfer = new JButton("Transfer");
|
||||
this.deposit = new JButton("Deposit");
|
||||
this.takeoff = new JButton("Takeoff");
|
||||
}
|
||||
|
||||
private void addInputRow(GridBagConstraints constraints, JComponent target, JComponent comp, int row, JLabel label) {
|
||||
constraints.gridwidth = 1;
|
||||
constraints.gridx = 1;
|
||||
constraints.gridy = row;
|
||||
constraints.weightx = 0;
|
||||
constraints.fill = GridBagConstraints.HORIZONTAL;
|
||||
target.add(label, constraints);
|
||||
|
||||
constraints.gridx = 2;
|
||||
constraints.gridy = row;
|
||||
constraints.weightx = 1;
|
||||
constraints.fill = GridBagConstraints.HORIZONTAL;
|
||||
target.add(comp, constraints);
|
||||
}
|
||||
|
||||
public JComboBox<String> getAccountSelection() {
|
||||
|
@ -140,40 +167,4 @@ public class AccountView extends JPanel {
|
|||
public JButton getTakeoff() {
|
||||
return takeoff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the accessible class fields of the primary account
|
||||
* into the text fields. Also updates the combo box for
|
||||
* all associated accounts.
|
||||
* @param profile the profile to update from
|
||||
*/
|
||||
public void updateAccountVariables(Profile profile) {
|
||||
Logging.LOGGER.finer("Updating account view");
|
||||
// temporarily extract data
|
||||
var bank = profile.getBank();
|
||||
var account = profile.getPrimaryAccount();
|
||||
var owner = profile.getOwner();
|
||||
|
||||
this.blz.setText(bank.getBlz());
|
||||
this.bankName.setText(bank.getName());
|
||||
|
||||
this.iban.setText(String.valueOf(account.getIban()));
|
||||
this.name.setText(owner.getName() + " " + owner.getFamilyName());
|
||||
this.address.setText(owner.getStreet() + " " + owner.getCity());
|
||||
|
||||
this.balance.setText(StringDecoder.getNumberFormat().format(account.getBalance()) + " €");
|
||||
|
||||
// update account type specific fields
|
||||
|
||||
this.type.setText(translate(account.getClass().getSimpleName()));
|
||||
if (account instanceof CurrentAccount) {
|
||||
this.typeSpecialLabel.setText(translate("Overdraft"));
|
||||
this.typeSpecialProperty.setText(StringDecoder.getNumberFormat().format(((CurrentAccount) account).getOverdraft()) + " €");
|
||||
} else if (account instanceof SavingsAccount) {
|
||||
this.typeSpecialLabel.setText(translate("Interest rate"));
|
||||
this.typeSpecialProperty.setText(((SavingsAccount) account).getInterest() + " %");
|
||||
} else {
|
||||
Logging.LOGGER.severe("Type of new primary account cannot be determined: " + account.getClass().getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
package me.teridax.jcash.gui.deposit;
|
||||
|
||||
import me.teridax.jcash.Logging;
|
||||
import me.teridax.jcash.banking.accounts.Account;
|
||||
import me.teridax.jcash.gui.InvalidInputException;
|
||||
import me.teridax.jcash.gui.Utils;
|
||||
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import java.text.ParseException;
|
||||
|
||||
/**
|
||||
* Class for controlling the deposit operation via a dialog.
|
||||
*/
|
||||
public class DepositController {
|
||||
|
||||
private final DepositView view;
|
||||
/**
|
||||
* Account to deposit money to.
|
||||
*/
|
||||
private final Account account;
|
||||
|
||||
public DepositController(Account account) {
|
||||
this.account = account;
|
||||
|
||||
this.view = new DepositView(account.getBalance());
|
||||
|
||||
this.view.getDeposit().addActionListener(e -> depositMoney());
|
||||
this.view.getCancel().addActionListener(e -> view.dispose());
|
||||
this.view.getValue().getDocument().addDocumentListener(new DocumentListener() {
|
||||
|
||||
/**
|
||||
* Validate the amount to deposit and update display
|
||||
* variables.
|
||||
*/
|
||||
private void validateInputState() {
|
||||
var balance = account.getBalance();
|
||||
try {
|
||||
view.getValue().commitEdit();
|
||||
var amount = view.getAmount();
|
||||
view.setCommittedValue(amount, balance + amount);
|
||||
view.getDeposit().setEnabled(true);
|
||||
} catch (InvalidInputException | ParseException ex) {
|
||||
view.setCommittedValue(0, balance);
|
||||
view.getDeposit().setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertUpdate(DocumentEvent documentEvent) {
|
||||
validateInputState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUpdate(DocumentEvent documentEvent) {
|
||||
validateInputState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedUpdate(DocumentEvent documentEvent) {
|
||||
validateInputState();
|
||||
}
|
||||
});
|
||||
this.view.showDialog();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deposit the last valid value to the account.
|
||||
* This method may display error dialogs when no money can be deposited.
|
||||
*/
|
||||
private void depositMoney() {
|
||||
try {
|
||||
var amount = view.getAmount();
|
||||
Logging.LOGGER.fine("Depositing money of account: " + account.getIban() + " amount: " + amount);
|
||||
account.deposit(amount);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
Logging.LOGGER.severe("Cannot deposit money of account: " + account.getIban() + " because: " + ex.getMessage());
|
||||
Utils.error(ex.getMessage());
|
||||
} catch (InvalidInputException ex) {
|
||||
Utils.error(ex.getMessage());
|
||||
}
|
||||
view.dispose();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package me.teridax.jcash.gui.deposit;
|
||||
|
||||
import me.teridax.jcash.banking.Account;
|
||||
|
||||
public class DepositDialog {
|
||||
|
||||
public DepositDialog(Account account, Runnable onDeposit) {
|
||||
var view = new DepositView(account);
|
||||
view.getDeposit().addActionListener(e -> {
|
||||
account.deposit(view.getAmount());
|
||||
onDeposit.run();
|
||||
view.dispose();
|
||||
});
|
||||
view.getCancel().addActionListener(e -> view.dispose());
|
||||
view.showDialog();
|
||||
}
|
||||
}
|
|
@ -1,52 +1,29 @@
|
|||
package me.teridax.jcash.gui.deposit;
|
||||
|
||||
import me.teridax.jcash.Logging;
|
||||
import me.teridax.jcash.decode.StringDecoder;
|
||||
import me.teridax.jcash.gui.IconProvider;
|
||||
import me.teridax.jcash.gui.InvalidInputException;
|
||||
import me.teridax.jcash.banking.Account;
|
||||
import me.teridax.jcash.decode.StringUtils;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.text.NumberFormat;
|
||||
import java.text.ParseException;
|
||||
|
||||
import static me.teridax.jcash.lang.Translator.translate;
|
||||
|
||||
/**
|
||||
* View class for displaying a dialog prompting the user to
|
||||
* enter a valid amount to deposit at their account
|
||||
*/
|
||||
public class DepositView {
|
||||
|
||||
/**
|
||||
* Window to use
|
||||
*/
|
||||
private JDialog dialog;
|
||||
private JButton cancel;
|
||||
/**
|
||||
* Button for applying the deposit operation
|
||||
*/
|
||||
private JButton deposit;
|
||||
/**
|
||||
* Displays the validated value to deposit
|
||||
*/
|
||||
private JLabel enteredValue;
|
||||
/**
|
||||
* Displays the account balance after the deposit operation
|
||||
*/
|
||||
private JLabel balanceAfterDeposit;
|
||||
private JFormattedTextField value;
|
||||
|
||||
public DepositView(double maxValue) {
|
||||
createComponents(maxValue);
|
||||
public DepositView(Account account) {
|
||||
createComponents(account);
|
||||
layoutComponents();
|
||||
}
|
||||
|
||||
public void showDialog() {
|
||||
dialog.setIconImage(IconProvider.getWindowIcon());
|
||||
dialog.setModalityType(Dialog.ModalityType.APPLICATION_MODAL);
|
||||
dialog.setTitle(translate("Deposit money"));
|
||||
dialog.setTitle("Deposit money");
|
||||
dialog.pack();
|
||||
dialog.setSize(dialog.getWidth() * 2, dialog.getHeight());
|
||||
dialog.setResizable(false);
|
||||
dialog.setLocationRelativeTo(null);
|
||||
dialog.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
||||
|
@ -60,103 +37,70 @@ public class DepositView {
|
|||
|
||||
c.gridx = 0;
|
||||
c.gridy = 0;
|
||||
c.weightx = 1;
|
||||
c.weighty = 1;
|
||||
c.gridwidth = 3;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
c.anchor = GridBagConstraints.CENTER;
|
||||
c.insets = new Insets(4, 4, 4, 4);
|
||||
dialog.getContentPane().add(new JLabel("Deposit money"), c);
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = 1;
|
||||
c.gridwidth = 1;
|
||||
c.fill = GridBagConstraints.NONE;
|
||||
c.anchor = GridBagConstraints.LAST_LINE_END;
|
||||
c.weightx = 0;
|
||||
c.insets = new Insets(6, 6, 6, 6);
|
||||
dialog.getContentPane().add(new JLabel(translate("Value"), SwingConstants.RIGHT), c);
|
||||
dialog.getContentPane().add(new JLabel("Value", SwingConstants.RIGHT), c);
|
||||
|
||||
c.gridx = 1;
|
||||
c.gridy = 0;
|
||||
c.gridy = 1;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
c.anchor = GridBagConstraints.LAST_LINE_END;
|
||||
c.weightx = 0.5;
|
||||
dialog.getContentPane().add(value, c);
|
||||
|
||||
c.gridx = 2;
|
||||
c.gridy = 0;
|
||||
c.gridy = 1;
|
||||
c.fill = GridBagConstraints.NONE;
|
||||
c.anchor = GridBagConstraints.LINE_START;
|
||||
c.weightx = 0;
|
||||
dialog.getContentPane().add(new JLabel("€"), c);
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = 1;
|
||||
c.gridwidth = 1;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
c.anchor = GridBagConstraints.LAST_LINE_END;
|
||||
dialog.getContentPane().add(new JLabel(translate("Value to deposit:"), SwingConstants.RIGHT), c);
|
||||
|
||||
c.gridx = 1;
|
||||
c.gridy = 1;
|
||||
c.gridwidth = 2;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
c.anchor = GridBagConstraints.LAST_LINE_END;
|
||||
dialog.getContentPane().add(enteredValue, c);
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = 2;
|
||||
c.gridwidth = 1;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
c.anchor = GridBagConstraints.LAST_LINE_END;
|
||||
dialog.getContentPane().add(new JLabel(translate("Balance after deposit:"), SwingConstants.RIGHT), c);
|
||||
|
||||
c.gridx = 1;
|
||||
c.gridy = 2;
|
||||
c.gridwidth = 2;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
c.anchor = GridBagConstraints.LAST_LINE_END;
|
||||
dialog.getContentPane().add(balanceAfterDeposit, c);
|
||||
|
||||
var buttonPanel = new JPanel(new FlowLayout(FlowLayout.TRAILING));
|
||||
buttonPanel.add(cancel);
|
||||
buttonPanel.add(deposit);
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = 3;
|
||||
c.gridy = 2;
|
||||
c.gridwidth = 3;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
c.anchor = GridBagConstraints.LAST_LINE_END;
|
||||
c.insets = new Insets(10, 10, 10, 10);
|
||||
dialog.getContentPane().add(buttonPanel, c);
|
||||
}
|
||||
|
||||
private void createComponents(double maxValue) {
|
||||
private void createComponents(Account account) {
|
||||
this.dialog = new JDialog();
|
||||
|
||||
this.cancel = new JButton(translate("Cancel"));
|
||||
this.deposit = new JButton(translate("Deposit"));
|
||||
this.value = new JFormattedTextField(StringDecoder.getNumberFormatter(Double.MAX_VALUE));
|
||||
this.enteredValue = new JLabel();
|
||||
this.balanceAfterDeposit = new JLabel(StringDecoder.getNumberFormat().format(maxValue));
|
||||
|
||||
this.deposit.setEnabled(false);
|
||||
this.cancel = new JButton("Cancel");
|
||||
this.deposit = new JButton("Deposit");
|
||||
this.value = new JFormattedTextField(StringUtils.LOCAL_NUMBER_FORMAT);
|
||||
|
||||
this.dialog.setContentPane(new JPanel(new GridBagLayout()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of money that should be deposited
|
||||
* This value derives from the input of the user.
|
||||
* @return the value to deposit
|
||||
* @throws InvalidInputException if the user entered something invalid
|
||||
*/
|
||||
public double getAmount() throws InvalidInputException {
|
||||
public double getAmount() {
|
||||
if (value.getText().isBlank())
|
||||
throw new InvalidInputException("currency value is blank or has been invalid whilst entered");
|
||||
return 0;
|
||||
|
||||
try {
|
||||
return StringDecoder.getNumberFormat().parse(value.getText()).doubleValue();
|
||||
return NumberFormat.getNumberInstance().parse(value.getText()).doubleValue();
|
||||
} catch (ParseException e) {
|
||||
Logging.LOGGER.severe("Amount text field contains invalid value: " + value);
|
||||
throw new InvalidInputException(e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public JFormattedTextField getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public JButton getCancel() {
|
||||
return cancel;
|
||||
}
|
||||
|
@ -168,14 +112,4 @@ public class DepositView {
|
|||
public void dispose() {
|
||||
this.dialog.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the supplied amount to the preview GUI fields.
|
||||
* @param amount the value to display for value to deposit
|
||||
* @param after the value to display for balance after deposit
|
||||
*/
|
||||
public void setCommittedValue(double amount, double after) {
|
||||
enteredValue.setText(StringDecoder.getNumberFormat().format(amount));
|
||||
balanceAfterDeposit.setText(StringDecoder.getNumberFormat().format(after));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,8 @@
|
|||
package me.teridax.jcash.gui.login;
|
||||
|
||||
import me.teridax.jcash.banking.management.Profile;
|
||||
import me.teridax.jcash.banking.Profile;
|
||||
|
||||
/**
|
||||
* Listens for changes in a selected account.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface AccountSelectionListener {
|
||||
|
||||
/**
|
||||
* Run when a new account is selected.
|
||||
* The selected account is set as the primary account of the profile
|
||||
*
|
||||
* @param account the profile for the selected account
|
||||
*/
|
||||
void onAccountSelected(Profile account);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
package me.teridax.jcash.gui.login;
|
||||
|
||||
import me.teridax.jcash.Logging;
|
||||
import me.teridax.jcash.banking.management.BankingManagementSystem;
|
||||
import me.teridax.jcash.gui.Utils;
|
||||
import me.teridax.jcash.banking.BankingManagementSystem;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.util.Optional;
|
||||
|
||||
|
@ -14,17 +13,13 @@ public class LoginController {
|
|||
|
||||
private AccountSelectionListener listener;
|
||||
|
||||
public LoginController() {
|
||||
public LoginController(BankingManagementSystem bms) {
|
||||
this.view = new LoginView();
|
||||
this.data = new LoginData();
|
||||
this.data = new LoginData(bms);
|
||||
|
||||
addActionListeners();
|
||||
}
|
||||
|
||||
public void setBankingManagementSystem(BankingManagementSystem bms) {
|
||||
this.data.setBms(bms);
|
||||
}
|
||||
|
||||
private void addActionListeners() {
|
||||
this.view.getLogin().addActionListener(this::login);
|
||||
}
|
||||
|
@ -34,7 +29,6 @@ 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();
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +38,6 @@ 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();
|
||||
}
|
||||
}
|
||||
|
@ -53,13 +46,12 @@ public class LoginController {
|
|||
var blz = this.view.getBlz().getText();
|
||||
var iban = this.getIban();
|
||||
if (iban.isEmpty()) {
|
||||
Utils.error("invalid IBAN entered");
|
||||
JOptionPane.showMessageDialog(null, "invalid IBAN", "Faulty login attempt", JOptionPane.ERROR_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
var pin = this.getPin();
|
||||
if (pin.isEmpty()) {
|
||||
Utils.error("invalid PIN entered");
|
||||
JOptionPane.showMessageDialog(null, "invalid pin", "Faulty login attempt", JOptionPane.ERROR_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -67,8 +59,8 @@ public class LoginController {
|
|||
if (account.isPresent()) {
|
||||
this.listener.onAccountSelected(account.get());
|
||||
} else {
|
||||
Logging.LOGGER.warning("invalid login credentials: " + iban + " / " + pin);
|
||||
}
|
||||
JOptionPane.showMessageDialog(null, "invalid login credentials", "Faulty login attempt", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
public void addAccountSelectionListener(AccountSelectionListener listener) {
|
||||
|
@ -78,12 +70,4 @@ public class LoginController {
|
|||
public LoginView getView() {
|
||||
return view;
|
||||
}
|
||||
|
||||
public LoginData getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void logout() {
|
||||
this.view.getPin().setText("");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,52 +1,24 @@
|
|||
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;
|
||||
import me.teridax.jcash.gui.Utils;
|
||||
import me.teridax.jcash.banking.BankingManagementSystem;
|
||||
import me.teridax.jcash.banking.Profile;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Wrapper class for a {@link BankingManagementSystem}
|
||||
*/
|
||||
public class LoginData {
|
||||
|
||||
private BankingManagementSystem bms;
|
||||
private final BankingManagementSystem bms;
|
||||
|
||||
/**
|
||||
* authenticate the specified account with the provided pin.
|
||||
*
|
||||
* @param blz the bank identifier
|
||||
* @param iban the account identifier
|
||||
* @param pin the pin for the account to authenticate with
|
||||
* @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()) {
|
||||
Utils.error("Unknown BLZ: " + blz);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
var profile = optionalBank.get().getAccount(iban);
|
||||
if (profile.isEmpty()) {
|
||||
Utils.error("Unknown account: " + iban);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
var account = profile.filter(value -> value.getPrimaryAccount().getPin() == pin);
|
||||
if (account.isEmpty()) {
|
||||
Utils.error("PIN does not match");
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
public void setBms(BankingManagementSystem bms) {
|
||||
public LoginData(BankingManagementSystem bms) {
|
||||
this.bms = bms;
|
||||
}
|
||||
|
||||
public Optional<Profile> authenticateAccount(String blz, int iban, int pin) {
|
||||
var optionalBank = bms.getBank(blz);
|
||||
if (optionalBank.isEmpty())
|
||||
return Optional.empty();
|
||||
|
||||
var profile = optionalBank.get().getAccount(iban);
|
||||
return profile.filter(value -> value.getPrimaryAccount().getPin() == pin);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,120 +1,75 @@
|
|||
package me.teridax.jcash.gui.login;
|
||||
|
||||
import me.teridax.jcash.gui.IconProvider;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.text.*;
|
||||
import javax.swing.text.NumberFormatter;
|
||||
import java.awt.*;
|
||||
import java.text.NumberFormat;
|
||||
|
||||
import static me.teridax.jcash.gui.Utils.addGridBagRow;
|
||||
import static me.teridax.jcash.gui.Utils.addHeading;
|
||||
import static me.teridax.jcash.lang.Translator.translate;
|
||||
|
||||
/**
|
||||
* GUI class for login into an account
|
||||
*/
|
||||
public class LoginView extends JPanel {
|
||||
|
||||
/**
|
||||
* Maximum number of decimal digits that can be stored lossless by a 32-bit signed integer.
|
||||
* N = log10(2^32-1) = 9,632959861146281
|
||||
*/
|
||||
private static final int MAX_PIN_DECIMAL_DIGITS = 9;
|
||||
/**
|
||||
* Number of pixels the banner image should be in width
|
||||
*/
|
||||
public static final int BANNER_WIDTH = 400;
|
||||
|
||||
private JFormattedTextField blz;
|
||||
private JFormattedTextField iban;
|
||||
private JPasswordField pin;
|
||||
private JButton login;
|
||||
private final JFormattedTextField blz;
|
||||
private final JFormattedTextField iban;
|
||||
private final JPasswordField pin;
|
||||
private final JButton login;
|
||||
|
||||
public LoginView() {
|
||||
createComponents();
|
||||
layoutComponents();
|
||||
}
|
||||
this.blz = new JFormattedTextField("MA2424");
|
||||
this.iban = new JFormattedTextField(getNumberFormat());
|
||||
this.iban.setText("4711");
|
||||
this.pin = new JPasswordField("1234");
|
||||
this.login = new JButton("Login");
|
||||
|
||||
private void layoutComponents() {
|
||||
var content = new JLabel();
|
||||
content.setIcon(new ImageIcon(IconProvider.getBackground()));
|
||||
content.setLayout(new BorderLayout());
|
||||
|
||||
var loginPane = new JPanel(new GridBagLayout());
|
||||
loginPane.setOpaque(true);
|
||||
content.add(loginPane, BorderLayout.CENTER);
|
||||
content.add(Box.createHorizontalStrut(BANNER_WIDTH), BorderLayout.WEST);
|
||||
|
||||
this.setLayout(new BorderLayout(32, 32));
|
||||
this.add(new JScrollPane(content), BorderLayout.CENTER);
|
||||
setLayout(new BorderLayout(16, 16));
|
||||
var content = new JPanel();
|
||||
this.setBorder(BorderFactory.createEmptyBorder(8,8,8,8));
|
||||
add(new JScrollPane(content), BorderLayout.CENTER);
|
||||
add(new JLabel("Bankautomat"), BorderLayout.NORTH);
|
||||
|
||||
var layout = new GridBagLayout();
|
||||
var constraints = new GridBagConstraints();
|
||||
content.setLayout(layout);
|
||||
|
||||
constraints.gridwidth = 4;
|
||||
constraints.insets = new Insets(12, 12, 12, 12);
|
||||
constraints.insets = new Insets(12,12,12,12);
|
||||
|
||||
addGridBagRow(constraints, loginPane, new JLabel(addHeading(translate("Cashmachine"))), 0, "");
|
||||
addGridBagRow(constraints, loginPane, blz, 1, translate("BLZ"));
|
||||
addGridBagRow(constraints, loginPane, iban, 2, translate("IBAN"));
|
||||
addGridBagRow(constraints, loginPane, pin, 3, translate("PIN"));
|
||||
addInputRow(constraints, content, blz, 1, "BLZ");
|
||||
addInputRow(constraints, content, iban, 2, "Kontonummer");
|
||||
addInputRow(constraints, content, pin, 3, "Passwort");
|
||||
|
||||
constraints.gridy = 4;
|
||||
constraints.anchor = GridBagConstraints.PAGE_END;
|
||||
constraints.weightx = 0;
|
||||
constraints.fill = GridBagConstraints.NONE;
|
||||
constraints.insets = new Insets(0, 0, 0, 12);
|
||||
loginPane.add(login, constraints);
|
||||
constraints.insets = new Insets(12,12,12,12);
|
||||
content.add(login, constraints);
|
||||
}
|
||||
|
||||
private void createComponents() {
|
||||
this.blz = new JFormattedTextField();
|
||||
this.iban = new JFormattedTextField();
|
||||
this.pin = new JPasswordField();
|
||||
this.login = new JButton(translate("Login"));
|
||||
private NumberFormatter getNumberFormat() {
|
||||
var format = NumberFormat.getIntegerInstance();
|
||||
format.setGroupingUsed(false);
|
||||
|
||||
// customize login button
|
||||
// this may not work with every swing look and feel
|
||||
this.login.setFont(new Font("Circus", Font.PLAIN, 28));
|
||||
this.login.setBackground(Color.CYAN);
|
||||
var formatter = new NumberFormatter(format);
|
||||
formatter.setValueClass(Integer.class);
|
||||
formatter.setMinimum(0);
|
||||
formatter.setMaximum(Integer.MAX_VALUE);
|
||||
formatter.setAllowsInvalid(false);
|
||||
|
||||
restrictPasswordToDigits();
|
||||
restrictIbanInput();
|
||||
return formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a document filter onto {@link #pin} that filters out everything that is not a digit.
|
||||
* The filter also restricts the amount of digits that can be entered to {@link #MAX_PIN_DECIMAL_DIGITS}
|
||||
*/
|
||||
private void restrictPasswordToDigits() {
|
||||
((AbstractDocument) this.pin.getDocument()).setDocumentFilter(new DocumentFilter() {
|
||||
@Override
|
||||
public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs)
|
||||
throws BadLocationException {
|
||||
String newText = fb.getDocument().getText(0, fb.getDocument().getLength()) + text;
|
||||
private void addInputRow(GridBagConstraints constraints, JComponent target, JComponent comp, int row, String name) {
|
||||
constraints.gridwidth = 1;
|
||||
constraints.gridx = 1;
|
||||
constraints.gridy = row;
|
||||
constraints.weightx = 0;
|
||||
constraints.fill = GridBagConstraints.HORIZONTAL;
|
||||
target.add(new JLabel(name, SwingConstants.RIGHT), constraints);
|
||||
|
||||
if (newText.matches(String.format("\\d{1,%s}", MAX_PIN_DECIMAL_DIGITS))) {
|
||||
super.replace(fb, offset, length, text, attrs);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a document filter onto {@link #iban} that filters out everything that is not a digit.
|
||||
* The filter also restricts the amount of digits that can be entered to {@link #MAX_PIN_DECIMAL_DIGITS}
|
||||
*/
|
||||
private void restrictIbanInput() {
|
||||
((AbstractDocument) this.iban.getDocument()).setDocumentFilter(new DocumentFilter() {
|
||||
@Override
|
||||
public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs)
|
||||
throws BadLocationException {
|
||||
String newText = fb.getDocument().getText(0, fb.getDocument().getLength()) + text;
|
||||
|
||||
if (newText.matches(String.format("\\d{1,%s}", MAX_PIN_DECIMAL_DIGITS))) {
|
||||
super.replace(fb, offset, length, text, attrs);
|
||||
}
|
||||
}
|
||||
});
|
||||
constraints.gridx = 2;
|
||||
constraints.gridy = row;
|
||||
constraints.weightx = 1;
|
||||
constraints.fill = GridBagConstraints.HORIZONTAL;
|
||||
target.add(comp, constraints);
|
||||
}
|
||||
|
||||
public JTextField getBlz() {
|
||||
|
|
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 1.7 MiB |
|
@ -1,4 +0,0 @@
|
|||
https://pixabay.com/vectors/register-cash-register-modern-23666/
|
||||
![register](https://cdn.pixabay.com/photo/2012/04/01/17/34/register-23666_960_720.png)
|
||||
Font file
|
||||
https://www.dafont.com/de/circus.font?text=Login
|
Binary file not shown.
Before Width: | Height: | Size: 31 KiB |
|
@ -1,86 +0,0 @@
|
|||
package me.teridax.jcash.gui.takeoff;
|
||||
|
||||
import me.teridax.jcash.Logging;
|
||||
import me.teridax.jcash.banking.accounts.Account;
|
||||
import me.teridax.jcash.banking.accounts.CurrentAccount;
|
||||
import me.teridax.jcash.gui.InvalidInputException;
|
||||
import me.teridax.jcash.gui.Utils;
|
||||
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import java.text.ParseException;
|
||||
|
||||
/**
|
||||
* Controller class for handling bank account take off.
|
||||
*/
|
||||
public class TakeoffController {
|
||||
|
||||
/**
|
||||
* Account to take off
|
||||
*/
|
||||
private final Account account;
|
||||
/**
|
||||
* GUI object
|
||||
*/
|
||||
private final TakeoffView view;
|
||||
|
||||
public TakeoffController(Account account) {
|
||||
this.account = account;
|
||||
|
||||
// add overdraft on top of the maximum amount
|
||||
// a user is allowed to take off
|
||||
var overdraft = 0.0;
|
||||
if (account instanceof CurrentAccount) {
|
||||
overdraft += ((CurrentAccount) account).getOverdraft();
|
||||
}
|
||||
|
||||
TakeoffData data = new TakeoffData(account.getBalance());
|
||||
this.view = new TakeoffView(data.getMaxValue() + overdraft);
|
||||
|
||||
this.view.getTakeoff().addActionListener(e -> takeOff());
|
||||
this.view.getCancel().addActionListener(e -> view.dispose());
|
||||
this.view.getValue().getDocument().addDocumentListener(new DocumentListener() {
|
||||
private void validateInputState() {
|
||||
var balance = account.getBalance();
|
||||
try {
|
||||
view.getValue().commitEdit();
|
||||
var amount = view.getAmount();
|
||||
view.setCommittedValue(amount, balance - amount);
|
||||
view.getTakeoff().setEnabled(true);
|
||||
} catch (InvalidInputException | ParseException ex) {
|
||||
view.setCommittedValue(0, balance);
|
||||
view.getTakeoff().setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertUpdate(DocumentEvent documentEvent) {
|
||||
validateInputState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUpdate(DocumentEvent documentEvent) {
|
||||
validateInputState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedUpdate(DocumentEvent documentEvent) {
|
||||
validateInputState();
|
||||
}
|
||||
});
|
||||
this.view.showDialog();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to take off some money from an account.
|
||||
*/
|
||||
private void takeOff() {
|
||||
try {
|
||||
account.takeoff(view.getAmount());
|
||||
} catch (IllegalArgumentException | InvalidInputException ex) {
|
||||
Logging.LOGGER.severe("Could not take off money: " + ex.getMessage());
|
||||
Utils.error("Reason: " + ex.getMessage());
|
||||
}
|
||||
view.dispose();
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
package me.teridax.jcash.gui.takeoff;
|
||||
|
||||
/**
|
||||
* Data class for taking off value from a certain account
|
||||
*/
|
||||
public class TakeoffData {
|
||||
|
||||
/**
|
||||
* Maximum value a user is allowed to take off
|
||||
*/
|
||||
private final double maxValue;
|
||||
|
||||
public TakeoffData(double maxValue) {
|
||||
this.maxValue = maxValue;
|
||||
}
|
||||
|
||||
public double getMaxValue() {
|
||||
return maxValue;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package me.teridax.jcash.gui.takeoff;
|
||||
|
||||
import me.teridax.jcash.banking.Account;
|
||||
|
||||
public class TakeoffDialog {
|
||||
|
||||
public TakeoffDialog(Account account, Runnable onTakeoff) {
|
||||
var view = new TakeoffView(account);
|
||||
view.getTakeoff().addActionListener(e -> {
|
||||
account.takeoff(view.getAmount());
|
||||
onTakeoff.run();
|
||||
view.dispose();
|
||||
});
|
||||
view.getCancel().addActionListener(e -> view.dispose());
|
||||
view.showDialog();
|
||||
}
|
||||
}
|
|
@ -1,42 +1,32 @@
|
|||
package me.teridax.jcash.gui.takeoff;
|
||||
|
||||
import me.teridax.jcash.Logging;
|
||||
import me.teridax.jcash.decode.StringDecoder;
|
||||
import me.teridax.jcash.gui.IconProvider;
|
||||
import me.teridax.jcash.gui.InvalidInputException;
|
||||
import me.teridax.jcash.banking.Account;
|
||||
import me.teridax.jcash.decode.StringUtils;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.text.NumberFormat;
|
||||
import java.text.ParseException;
|
||||
|
||||
import static me.teridax.jcash.lang.Translator.translate;
|
||||
import static javax.swing.JOptionPane.ERROR_MESSAGE;
|
||||
import static javax.swing.JOptionPane.showMessageDialog;
|
||||
|
||||
/**
|
||||
* Dialog for taking off money of an account.
|
||||
*/
|
||||
public class TakeoffView {
|
||||
|
||||
private JDialog dialog;
|
||||
private JButton cancel;
|
||||
private JButton takeoff;
|
||||
private JLabel enteredValue;
|
||||
private JLabel balanceAfterDeposit;
|
||||
private JFormattedTextField value;
|
||||
|
||||
public TakeoffView(double maxValue) {
|
||||
createComponents(maxValue);
|
||||
public TakeoffView(Account account) {
|
||||
createComponents(account);
|
||||
layoutComponents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes this dialog visible.
|
||||
*/
|
||||
public void showDialog() {
|
||||
dialog.setIconImage(IconProvider.getWindowIcon());
|
||||
dialog.setModalityType(Dialog.ModalityType.APPLICATION_MODAL);
|
||||
dialog.setTitle(translate("Takeoff money"));
|
||||
dialog.setTitle("Takeoff money");
|
||||
dialog.pack();
|
||||
dialog.setSize(dialog.getWidth() * 2, dialog.getHeight());
|
||||
dialog.setResizable(false);
|
||||
dialog.setLocationRelativeTo(null);
|
||||
dialog.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
||||
|
@ -48,61 +38,42 @@ public class TakeoffView {
|
|||
|
||||
c.gridx = 0;
|
||||
c.gridy = 0;
|
||||
c.weightx = 1;
|
||||
c.weighty = 1;
|
||||
c.gridwidth = 3;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
c.anchor = GridBagConstraints.CENTER;
|
||||
c.insets = new Insets(4, 4, 4, 4);
|
||||
dialog.getContentPane().add(new JLabel("Takeoff money"), c);
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = 1;
|
||||
c.gridwidth = 1;
|
||||
c.fill = GridBagConstraints.NONE;
|
||||
c.anchor = GridBagConstraints.LAST_LINE_END;
|
||||
c.weightx = 0;
|
||||
c.insets = new Insets(6,6,6,6);
|
||||
dialog.getContentPane().add(new JLabel(translate("Value"), SwingConstants.RIGHT), c);
|
||||
dialog.getContentPane().add(new JLabel("Value", SwingConstants.RIGHT), c);
|
||||
|
||||
c.gridx = 1;
|
||||
c.gridy = 0;
|
||||
c.gridy = 1;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
c.anchor = GridBagConstraints.LAST_LINE_END;
|
||||
c.weightx = 0.5;
|
||||
dialog.getContentPane().add(value, c);
|
||||
|
||||
c.gridx = 2;
|
||||
c.gridy = 0;
|
||||
c.gridy = 1;
|
||||
c.fill = GridBagConstraints.NONE;
|
||||
c.anchor = GridBagConstraints.LINE_START;
|
||||
c.weightx = 0;
|
||||
dialog.getContentPane().add(new JLabel("€"), c);
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = 1;
|
||||
c.gridwidth = 1;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
c.anchor = GridBagConstraints.LAST_LINE_END;
|
||||
dialog.getContentPane().add(new JLabel(translate("Value to takeoff:"), SwingConstants.RIGHT), c);
|
||||
|
||||
c.gridx = 1;
|
||||
c.gridy = 1;
|
||||
c.gridwidth = 2;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
c.anchor = GridBagConstraints.LAST_LINE_END;
|
||||
dialog.getContentPane().add(enteredValue, c);
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = 2;
|
||||
c.gridwidth = 1;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
c.anchor = GridBagConstraints.LAST_LINE_END;
|
||||
dialog.getContentPane().add(new JLabel(translate("Balance after takeoff:"), SwingConstants.RIGHT), c);
|
||||
|
||||
c.gridx = 1;
|
||||
c.gridy = 2;
|
||||
c.gridwidth = 2;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
c.anchor = GridBagConstraints.LAST_LINE_END;
|
||||
dialog.getContentPane().add(balanceAfterDeposit, c);
|
||||
|
||||
var buttonPanel = new JPanel(new FlowLayout(FlowLayout.TRAILING));
|
||||
buttonPanel.add(cancel);
|
||||
buttonPanel.add(takeoff);
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = 3;
|
||||
c.gridy = 2;
|
||||
c.gridwidth = 3;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
c.anchor = GridBagConstraints.LAST_LINE_END;
|
||||
|
@ -110,50 +81,29 @@ public class TakeoffView {
|
|||
dialog.getContentPane().add(buttonPanel, c);
|
||||
}
|
||||
|
||||
/**
|
||||
* The createComponents function creates the components of the dialog.
|
||||
* @param maxValue Set the maximum value of the jformattedtextfield
|
||||
*/
|
||||
private void createComponents(double maxValue) {
|
||||
private void createComponents(Account account) {
|
||||
this.dialog = new JDialog();
|
||||
|
||||
this.cancel = new JButton(translate("Cancel"));
|
||||
this.takeoff = new JButton(translate("Takeoff"));
|
||||
this.value = new JFormattedTextField(StringDecoder.getNumberFormatter(maxValue));
|
||||
this.enteredValue = new JLabel();
|
||||
this.balanceAfterDeposit = new JLabel(StringDecoder.getNumberFormat().format(maxValue));
|
||||
this.cancel = new JButton("Cancel");
|
||||
this.takeoff = new JButton("Takeoff");
|
||||
this.value = new JFormattedTextField(StringUtils.LOCAL_NUMBER_FORMAT);
|
||||
|
||||
this.dialog.setContentPane(new JPanel(new GridBagLayout()));
|
||||
}
|
||||
|
||||
/**
|
||||
* The getAmount function is used to get the amount of currency that has been entered into the text field.
|
||||
* @return A double value, which is the parsed amount from the text field
|
||||
*/
|
||||
public double getAmount() throws InvalidInputException {
|
||||
if (value.getText().isBlank())
|
||||
throw new InvalidInputException("currency value is blank or has been invalid whilst entered");
|
||||
public double getAmount() {
|
||||
if (value.getText().isBlank()) {
|
||||
showMessageDialog(null, "invalid amount", "currency must not be blank", ERROR_MESSAGE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
return StringDecoder.getNumberFormat().parse(value.getText()).doubleValue();
|
||||
return NumberFormat.getNumberInstance().parse(value.getText()).doubleValue();
|
||||
} catch (ParseException e) {
|
||||
Logging.LOGGER.severe("Amount text field contains invalid value: " + value);
|
||||
throw new InvalidInputException(e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The setCommittedValue function sets the text of the enteredValue and balanceAfterDeposit TextFields to
|
||||
* a String representation of amount and after, respectively.
|
||||
* @param amount Set the text of enteredvalue
|
||||
* @param after Set the balance after deposit
|
||||
*/
|
||||
public void setCommittedValue(double amount, double after) {
|
||||
enteredValue.setText(StringDecoder.getNumberFormat().format(amount));
|
||||
balanceAfterDeposit.setText(StringDecoder.getNumberFormat().format(after));
|
||||
}
|
||||
|
||||
public JButton getCancel() {
|
||||
return cancel;
|
||||
}
|
||||
|
@ -162,10 +112,6 @@ public class TakeoffView {
|
|||
return takeoff;
|
||||
}
|
||||
|
||||
public JFormattedTextField getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
this.dialog.dispose();
|
||||
}
|
||||
|
|
|
@ -1,111 +0,0 @@
|
|||
package me.teridax.jcash.gui.transfer;
|
||||
|
||||
import me.teridax.jcash.Logging;
|
||||
import me.teridax.jcash.Main;
|
||||
import me.teridax.jcash.banking.accounts.Account;
|
||||
import me.teridax.jcash.banking.accounts.CurrentAccount;
|
||||
import me.teridax.jcash.banking.management.BankingManagementSystem;
|
||||
import me.teridax.jcash.gui.InvalidInputException;
|
||||
import me.teridax.jcash.gui.Utils;
|
||||
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import java.text.ParseException;
|
||||
|
||||
/**
|
||||
* Dialog class for transferring some value from one account to another
|
||||
*/
|
||||
public class TransferController {
|
||||
|
||||
private final Account account;
|
||||
private final TransferData transferData;
|
||||
private final TransferView view;
|
||||
|
||||
public TransferController(Account account, BankingManagementSystem bms) {
|
||||
this.account = account;
|
||||
|
||||
var overdraft = 0.0;
|
||||
if (account instanceof CurrentAccount) {
|
||||
overdraft += ((CurrentAccount) account).getOverdraft();
|
||||
}
|
||||
|
||||
this.view = new TransferView(account.getBalance() + overdraft);
|
||||
this.transferData = new TransferData(bms);
|
||||
this.view.getTransfer().addActionListener(e -> transfer());
|
||||
this.view.getCancel().addActionListener(e -> view.dispose());
|
||||
|
||||
// validates the users input
|
||||
var validator = new DocumentListener() {
|
||||
@Override
|
||||
public void insertUpdate(DocumentEvent documentEvent) {
|
||||
validateInputState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUpdate(DocumentEvent documentEvent) {
|
||||
validateInputState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedUpdate(DocumentEvent documentEvent) {
|
||||
validateInputState();
|
||||
}
|
||||
};
|
||||
|
||||
this.view.getValue().getDocument().addDocumentListener(validator);
|
||||
this.view.getIbanTextField().getDocument().addDocumentListener(validator);
|
||||
this.view.getBlzTextField().getDocument().addDocumentListener(validator);
|
||||
|
||||
this.view.showDialog();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the target bank account is valid.
|
||||
* @return true if the target bank account is valid false otherwise
|
||||
*/
|
||||
private boolean validateTargetAccount() {
|
||||
if (transferData.validateBLZ(this.view.getBlz())) {
|
||||
return transferData.validateIBAN(this.view.getBlz(), view.getIban());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the entered value to transfer is valid.
|
||||
* This method will also commit the valid value back to the text field.
|
||||
* @return true if the value to transfer is valid, false otherwise
|
||||
*/
|
||||
private boolean validateTransferValue() {
|
||||
var balance = account.getBalance();
|
||||
try {
|
||||
view.getValue().commitEdit();
|
||||
var amount = view.getAmount();
|
||||
view.setCommittedValue(amount, balance - amount);
|
||||
return true;
|
||||
} catch (InvalidInputException | ParseException ex) {
|
||||
view.setCommittedValue(0, balance);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void validateInputState() {
|
||||
var valid = validateTargetAccount() && validateTransferValue();
|
||||
view.getTransfer().setEnabled(valid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to transfer the balance from one account to another
|
||||
* This method will close the dialog.
|
||||
*/
|
||||
private void transfer() {
|
||||
try {
|
||||
var amount = view.getAmount();
|
||||
this.account.takeoff(amount);
|
||||
this.transferData.transferValue(amount, view.getBlz(), view.getIban());
|
||||
} catch (IllegalArgumentException | InvalidInputException ex) {
|
||||
Logging.LOGGER.severe("Could not transfer: " + ex.getMessage());
|
||||
Utils.error("Reason: " + ex.getMessage());
|
||||
}
|
||||
this.view.dispose();
|
||||
}
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
package me.teridax.jcash.gui.transfer;
|
||||
|
||||
import me.teridax.jcash.Logging;
|
||||
import me.teridax.jcash.banking.management.BankingManagementSystem;
|
||||
import me.teridax.jcash.decode.StringDecoder;
|
||||
|
||||
public class TransferData {
|
||||
|
||||
private final BankingManagementSystem bms;
|
||||
|
||||
public TransferData(BankingManagementSystem bms) {
|
||||
this.bms = bms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfers a certain amount of money to the specified account of the specified bank
|
||||
*
|
||||
* @param amount the amount to transfer
|
||||
* @param blz the bank that manages the account
|
||||
* @param ibanString the internal bank account number to transfer money to
|
||||
* @throws IllegalArgumentException if the bank or the account do not exist
|
||||
*/
|
||||
public void transferValue(double amount, String blz, String ibanString) throws IllegalArgumentException {
|
||||
// get bank to transfer to
|
||||
var bank = bms.getBank(blz);
|
||||
if (bank.isEmpty()) {
|
||||
Logging.LOGGER.warning("Bank not found: " + blz);
|
||||
throw new IllegalArgumentException("Bank not found: " + blz);
|
||||
}
|
||||
|
||||
// validate iban of target account
|
||||
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);
|
||||
}
|
||||
|
||||
// get account to transfer value to
|
||||
var account = bank.get().getAccount(iban);
|
||||
if (account.isEmpty()) {
|
||||
Logging.LOGGER.warning("Account not found: " + iban);
|
||||
throw new IllegalArgumentException("Account not found: " + iban);
|
||||
}
|
||||
|
||||
account.get().getPrimaryAccount().deposit(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given BLZ. If no bank with the given BLZ can be found
|
||||
* this method returns false. Otherwise, true is returned.
|
||||
* @param blz the BLZ to validate
|
||||
* @return true if the BLZ is valid and false otherwise
|
||||
*/
|
||||
public boolean validateBLZ(String blz) {
|
||||
return bms.getBank(blz).isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given IBAN for the given BLZ. This method assumes the BLZ to be valid.
|
||||
* If this is not the case, this function will throw an exception.
|
||||
* Returns true if an account with the given IBAN was found for bank of the given BLZ.
|
||||
* @param blz bank to search in
|
||||
* @param ibanString account to search for
|
||||
* @return true if the account was found false otherwise
|
||||
*/
|
||||
public boolean validateIBAN(String blz, String ibanString) {
|
||||
var bank = bms.getBank(blz);
|
||||
try {
|
||||
var iban = StringDecoder.decodeUniqueIdentificationNumber(ibanString);
|
||||
return bank.map(value -> value.getAccount(iban).isPresent()).orElse(false);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package me.teridax.jcash.gui.transfer;
|
||||
|
||||
import me.teridax.jcash.banking.Account;
|
||||
|
||||
public class TransferDialog {
|
||||
|
||||
public TransferDialog(Account account, Runnable onDeposit) {
|
||||
var view = new TransferView(account);
|
||||
view.getTransfer().addActionListener(e -> {
|
||||
account.takeoff(view.getAmount());
|
||||
onDeposit.run();
|
||||
view.dispose();
|
||||
});
|
||||
view.getCancel().addActionListener(e -> view.dispose());
|
||||
view.showDialog();
|
||||
}
|
||||
}
|
|
@ -1,34 +1,16 @@
|
|||
package me.teridax.jcash.gui.transfer;
|
||||
|
||||
import me.teridax.jcash.Logging;
|
||||
import me.teridax.jcash.decode.StringDecoder;
|
||||
import me.teridax.jcash.gui.IconProvider;
|
||||
import me.teridax.jcash.gui.InvalidInputException;
|
||||
import me.teridax.jcash.banking.Account;
|
||||
import me.teridax.jcash.decode.StringUtils;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.text.NumberFormat;
|
||||
import java.text.ParseException;
|
||||
|
||||
import static me.teridax.jcash.lang.Translator.translate;
|
||||
import static javax.swing.JOptionPane.ERROR_MESSAGE;
|
||||
import static javax.swing.JOptionPane.showMessageDialog;
|
||||
|
||||
/**
|
||||
* JDialog for displaying the GUI for a transfer dialog
|
||||
* with the following crude layout:
|
||||
* <pre>
|
||||
┌───────────────────────────────────────────────┐
|
||||
│ ┌─────────────┐ ┌────────────────┐ │
|
||||
│ BLZ │ │ IBAN │ │ │
|
||||
│ └─────────────┘ └────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────┐ │
|
||||
│ VALUE │ │ │
|
||||
│ └─────────────┘ │
|
||||
│ ┌───────┐ ┌────────┐ │
|
||||
│ │Cancel │ │Transfer│ │
|
||||
│ └───────┘ └────────┘ │
|
||||
└───────────────────────────────────────────────┘
|
||||
* </pre>
|
||||
*/
|
||||
public class TransferView {
|
||||
|
||||
private JDialog dialog;
|
||||
|
@ -37,21 +19,15 @@ public class TransferView {
|
|||
private JFormattedTextField iban;
|
||||
private JFormattedTextField blz;
|
||||
private JFormattedTextField value;
|
||||
private JLabel balanceAfterTransfer;
|
||||
private JLabel enteredValue;
|
||||
|
||||
public TransferView(double maxValue) {
|
||||
createComponents(maxValue);
|
||||
public TransferView(Account account) {
|
||||
createComponents(account);
|
||||
layoutComponents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes this dialog visible to the user
|
||||
*/
|
||||
public void showDialog() {
|
||||
dialog.setIconImage(IconProvider.getWindowIcon());
|
||||
dialog.setModalityType(Dialog.ModalityType.APPLICATION_MODAL);
|
||||
dialog.setTitle(translate("Transfer money"));
|
||||
dialog.setTitle("Transfer money");
|
||||
dialog.pack();
|
||||
dialog.setSize(dialog.getWidth() * 2, dialog.getHeight());
|
||||
dialog.setResizable(false);
|
||||
|
@ -60,142 +36,100 @@ public class TransferView {
|
|||
dialog.setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout all components of this dialog.
|
||||
*/
|
||||
private void layoutComponents() {
|
||||
var c = new GridBagConstraints();
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = 0;
|
||||
c.weightx = 1;
|
||||
c.weighty = 1;
|
||||
c.gridwidth = 3;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
c.anchor = GridBagConstraints.CENTER;
|
||||
c.insets = new Insets(4, 4, 4, 4);
|
||||
dialog.getContentPane().add(new JLabel("Transfer money"), c);
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = 1;
|
||||
c.gridwidth = 1;
|
||||
c.fill = GridBagConstraints.NONE;
|
||||
c.anchor = GridBagConstraints.LAST_LINE_END;
|
||||
c.weightx = 0;
|
||||
c.insets = new Insets(6,6,6,6);
|
||||
dialog.getContentPane().add(new JLabel(translate("BLZ"), SwingConstants.RIGHT), c);
|
||||
dialog.getContentPane().add(new JLabel("BLZ", SwingConstants.RIGHT), c);
|
||||
|
||||
c.gridx = 1;
|
||||
c.gridy = 0;
|
||||
c.gridy = 1;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
c.anchor = GridBagConstraints.LAST_LINE_END;
|
||||
c.weightx = 0.5;
|
||||
dialog.getContentPane().add(blz, c);
|
||||
|
||||
c.gridx = 2;
|
||||
c.gridy = 0;
|
||||
c.gridy = 1;
|
||||
c.fill = GridBagConstraints.NONE;
|
||||
c.anchor = GridBagConstraints.LAST_LINE_END;
|
||||
c.weightx = 0;
|
||||
dialog.getContentPane().add(new JLabel(translate("IBAN"), SwingConstants.RIGHT), c);
|
||||
dialog.getContentPane().add(new JLabel("IBAN", SwingConstants.RIGHT), c);
|
||||
|
||||
c.gridx = 3;
|
||||
c.gridy = 0;
|
||||
c.gridy = 1;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
c.anchor = GridBagConstraints.LAST_LINE_END;
|
||||
c.weightx = 1;
|
||||
dialog.getContentPane().add(iban, c);
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = 1;
|
||||
c.gridy = 2;
|
||||
c.fill = GridBagConstraints.NONE;
|
||||
c.anchor = GridBagConstraints.LAST_LINE_END;
|
||||
c.weightx = 0;
|
||||
dialog.getContentPane().add(new JLabel(translate("Betrag"), SwingConstants.RIGHT), c);
|
||||
dialog.getContentPane().add(new JLabel("Betrag", SwingConstants.RIGHT), c);
|
||||
|
||||
c.gridx = 1;
|
||||
c.gridy = 1;
|
||||
c.gridy = 2;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
c.anchor = GridBagConstraints.LAST_LINE_END;
|
||||
c.weightx = 0.5;
|
||||
dialog.getContentPane().add(value, c);
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = 2;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
c.anchor = GridBagConstraints.LAST_LINE_END;
|
||||
c.weightx = 0.5;
|
||||
dialog.getContentPane().add(new JLabel(translate("Value to transfer:"), JLabel.RIGHT), c);
|
||||
|
||||
c.gridx = 1;
|
||||
c.gridy = 2;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
c.anchor = GridBagConstraints.LAST_LINE_END;
|
||||
c.weightx = 0.5;
|
||||
dialog.getContentPane().add(enteredValue, c);
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = 3;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
c.anchor = GridBagConstraints.LAST_LINE_END;
|
||||
c.weightx = 0.5;
|
||||
dialog.getContentPane().add(new JLabel(translate("Balance after transfer:"), JLabel.RIGHT), c);
|
||||
|
||||
c.gridx = 1;
|
||||
c.gridy = 3;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
c.anchor = GridBagConstraints.LAST_LINE_END;
|
||||
c.weightx = 0.5;
|
||||
dialog.getContentPane().add(balanceAfterTransfer, c);
|
||||
|
||||
var buttonPanel = new JPanel(new FlowLayout(FlowLayout.TRAILING));
|
||||
buttonPanel.add(cancel);
|
||||
buttonPanel.add(transfer);
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = 4;
|
||||
c.gridy = 3;
|
||||
c.gridwidth = 4;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
c.anchor = GridBagConstraints.LAST_LINE_END;
|
||||
c.insets = new Insets(10, 10, 10, 10);
|
||||
dialog.getContentPane().add(buttonPanel, c);
|
||||
}
|
||||
|
||||
private void createComponents(double maxValue) {
|
||||
private void createComponents(Account account) {
|
||||
this.dialog = new JDialog();
|
||||
|
||||
this.cancel = new JButton(translate("Cancel"));
|
||||
this.transfer = new JButton(translate("Transfer"));
|
||||
this.transfer.setEnabled(false);
|
||||
this.value = new JFormattedTextField(StringDecoder.getNumberFormatter(maxValue));
|
||||
this.cancel = new JButton("Cancel");
|
||||
this.transfer = new JButton("Transfer");
|
||||
this.value = new JFormattedTextField(StringUtils.LOCAL_NUMBER_FORMAT);
|
||||
this.iban = new JFormattedTextField();
|
||||
this.blz = new JFormattedTextField();
|
||||
this.enteredValue = new JLabel();
|
||||
this.balanceAfterTransfer = new JLabel(StringDecoder.getNumberFormat().format(maxValue));
|
||||
|
||||
this.dialog.setContentPane(new JPanel(new GridBagLayout()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the entered amount parsed into a double value.
|
||||
* @return the amount parsed into a double
|
||||
* @throws InvalidInputException if the text in {@link #value} is not a valid double value.
|
||||
*/
|
||||
public double getAmount() throws InvalidInputException {
|
||||
if (value.getText().isBlank())
|
||||
throw new InvalidInputException("currency value is blank or has been invalid whilst entered");
|
||||
public double getAmount() {
|
||||
if (value.getText().isBlank()) {
|
||||
showMessageDialog(null, "invalid amount", "currency must not be blank", ERROR_MESSAGE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
return StringDecoder.getNumberFormat().parse(value.getText()).doubleValue();
|
||||
return NumberFormat.getNumberInstance().parse(value.getText()).doubleValue();
|
||||
} catch (ParseException e) {
|
||||
Logging.LOGGER.severe("Amount text field contains invalid value: " + value);
|
||||
throw new InvalidInputException(e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the values to display in the dialog labels as overview information.
|
||||
* @param amount the amount to transfer
|
||||
* @param after balance after the transfer
|
||||
*/
|
||||
public void setCommittedValue(double amount, double after) {
|
||||
enteredValue.setText(StringDecoder.getNumberFormat().format(amount));
|
||||
balanceAfterTransfer.setText(StringDecoder.getNumberFormat().format(after));
|
||||
}
|
||||
|
||||
public JFormattedTextField getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public JButton getCancel() {
|
||||
return cancel;
|
||||
}
|
||||
|
@ -204,22 +138,6 @@ public class TransferView {
|
|||
return transfer;
|
||||
}
|
||||
|
||||
public String getIban() {
|
||||
return iban.getText();
|
||||
}
|
||||
|
||||
public String getBlz() {
|
||||
return blz.getText();
|
||||
}
|
||||
|
||||
public JFormattedTextField getIbanTextField() {
|
||||
return iban;
|
||||
}
|
||||
|
||||
public JFormattedTextField getBlzTextField() {
|
||||
return blz;
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
this.dialog.dispose();
|
||||
}
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
package me.teridax.jcash.lang;
|
||||
|
||||
import me.teridax.jcash.Logging;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Class for storing static information about the locale used by the application instance at runtime.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class Locales {
|
||||
|
||||
/**
|
||||
* Default locale initialized to the fallback locale used by this application.
|
||||
*/
|
||||
private static Locale defaultLocale = new Locale("en", "EN");
|
||||
|
||||
/**
|
||||
* Sets the default locale to use for the application instance.
|
||||
* This will instruct the translator to use the default locale as well as
|
||||
* Java swing components.
|
||||
*
|
||||
* @param language the locale to use for language
|
||||
* @param country the locale to use for country
|
||||
*/
|
||||
public static void setDefaultLocale(String language, String country) {
|
||||
var locale = language + "_" + country;
|
||||
|
||||
if (Translator.setTranslationLocale(locale)) {
|
||||
defaultLocale = new Locale(language, country);
|
||||
// apply locale to JVM
|
||||
Locale.setDefault(defaultLocale);
|
||||
}
|
||||
Logging.LOGGER.info("Using locale: " + locale);
|
||||
}
|
||||
|
||||
public static Locale getDefaultLocale() {
|
||||
return defaultLocale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to automatically detect the default locale.
|
||||
* This will prefer the users locale over the systems locale.
|
||||
* If both fail, the JVMs default locale will be used.
|
||||
*/
|
||||
public static void autodetectDefaultLocale() {
|
||||
var country = System.getProperty("user.country");
|
||||
var language = System.getProperty("user.language");
|
||||
|
||||
var jvmLocale = Locale.getDefault();
|
||||
|
||||
if (null == country)
|
||||
country = jvmLocale.getCountry();
|
||||
|
||||
if (null == language)
|
||||
language = jvmLocale.getLanguage();
|
||||
|
||||
setDefaultLocale(language, country);
|
||||
}
|
||||
}
|
|
@ -1,130 +0,0 @@
|
|||
package me.teridax.jcash.lang;
|
||||
|
||||
import me.teridax.jcash.Logging;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Static translator class.
|
||||
* This is a very simple translator able to translate base tokens from english into a variety of
|
||||
* configured languages.
|
||||
*/
|
||||
public final class Translator {
|
||||
|
||||
/**
|
||||
* List of all supported locales in the format: language_country. Examples: en_EN, fr_FR
|
||||
* The index inside the list is directly related to the index of the translation array inside the list translations.
|
||||
*/
|
||||
private static final List<String> languages = new ArrayList<>();
|
||||
/**
|
||||
* Mapping of a default english phrase of the code en_EN which is associated with a list of possible translations.
|
||||
* Index 0 of the translation is equivalent to the key itself since locale 0 is always en_EN.
|
||||
*/
|
||||
private static final Map<String, String[]> translations = new HashMap<>();
|
||||
/**
|
||||
* Precomputed index of the locale used to statically translate a phrase
|
||||
*/
|
||||
private static int localeTranslationIndex = 0;
|
||||
|
||||
static {
|
||||
// read language file and parse
|
||||
try (var stream = Objects.requireNonNull(Translator.class.getResourceAsStream("languages.csv"))) {
|
||||
var text = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
|
||||
// parse each line
|
||||
text.lines().forEach(line -> {
|
||||
// read header i.e. the locales identifier
|
||||
if (languages.isEmpty()) {
|
||||
// split string into columns by comma and trim each column
|
||||
languages.addAll(Arrays.stream(line.split(",")).map(String::trim).collect(Collectors.toList()));
|
||||
|
||||
// check if default locale is present
|
||||
if (!languages.contains("en_EN")) {
|
||||
Logging.LOGGER.severe("Missing default en_EN locale");
|
||||
throw new IllegalArgumentException("Missing en_EN locale");
|
||||
}
|
||||
|
||||
Logging.LOGGER.info("Read locales: " + Arrays.deepToString(languages.toArray()));
|
||||
return;
|
||||
}
|
||||
|
||||
var translation = Arrays.stream(line.split(",")).map(String::trim).toArray(String[]::new);
|
||||
|
||||
// check if all translations are present
|
||||
// it may happen at a locale does not provide a translation
|
||||
if (translation.length != languages.size())
|
||||
Logging.LOGGER.warning("invalid translations: " + translation.length + " " + languages.size());
|
||||
|
||||
translations.put(translation[0], translation);
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the given phrase into the corresponding phrase of the selected locale of the translator.
|
||||
* If no translation is found or no locale is defined for the translator this function will return the given phrase.
|
||||
*
|
||||
* @param phrase the text to translate
|
||||
* @return the translated phrase, or the phrase itself in case no translation can be found
|
||||
*/
|
||||
public static String translate(String phrase) {
|
||||
try {
|
||||
return translations.get(phrase)[localeTranslationIndex];
|
||||
|
||||
} catch (ArrayIndexOutOfBoundsException e) {
|
||||
Logging.LOGGER.severe("Locale does not exist with index: " + localeTranslationIndex);
|
||||
} catch (NullPointerException e) {
|
||||
Logging.LOGGER.severe("No translation found for phrase: " + phrase);
|
||||
}
|
||||
return phrase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all available locales for this translator
|
||||
*
|
||||
* @return an array of all available locales for this translator
|
||||
*/
|
||||
public static String[] availableLocales() {
|
||||
return languages.toArray(String[]::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the given locale string to an index indicating which array location to choose when fetching a result from
|
||||
* the translation map.
|
||||
*
|
||||
* @param locale the locale string
|
||||
* @return a matching index of the locale
|
||||
* @throws IllegalArgumentException if the given locale is not part of the available locales
|
||||
*/
|
||||
private static int mapLocaleToIndex(String locale) throws IllegalArgumentException {
|
||||
for (int i = 0; i < languages.size(); i++) {
|
||||
if (languages.get(i).equals(locale)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Locale does not exist: " + locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default locale to use when translating.
|
||||
* The locale must have the format language_COUNTRY like en_EN
|
||||
*
|
||||
* @param locale the locale to use when translating
|
||||
* @return if the specified locale can be used by the translator
|
||||
*/
|
||||
public static boolean setTranslationLocale(String locale) {
|
||||
try {
|
||||
localeTranslationIndex = Translator.mapLocaleToIndex(locale);
|
||||
return true;
|
||||
} catch (IllegalArgumentException ex) {
|
||||
Logging.LOGGER.severe("unable to set locale for translation: " + locale + " because: " + ex.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
package me.teridax.jcash.lang;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static junit.framework.TestCase.assertEquals;
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
|
||||
public class TranslatorTest {
|
||||
|
||||
@Test
|
||||
public void testLoading() {
|
||||
// test if all locales are read
|
||||
assertTrue(Arrays.deepEquals(Translator.availableLocales(), new String[]{"en_EN", "de_DE", "es_ES", "fr_FR", "zh_Hans"}));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTranslationBasic() {
|
||||
// test if basic translation works
|
||||
Translator.setTranslationLocale("de_DE");
|
||||
assertEquals(Translator.translate("Overdraft"), "Überziehungsbetrag");
|
||||
assertEquals(Translator.translate("Load database"), "Datenbank auswählen");
|
||||
assertEquals(Translator.translate("currency must not be blank"), "Betrag darf nicht leer sein");
|
||||
assertEquals(Translator.translate("Deposit money"), "Geld einzahlen");
|
||||
|
||||
Translator.setTranslationLocale("es_ES");
|
||||
assertEquals(Translator.translate("Account"), "Cuenta");
|
||||
assertEquals(Translator.translate("Faulty login attempt"), "Solicitud de autenticación no válida");
|
||||
assertEquals(Translator.translate("Transfer money"), "Transferencia de dinero");
|
||||
|
||||
// test if translation with no available translation works as expected
|
||||
assertEquals(Translator.translate("Guppi guppi guppi"), "Guppi guppi guppi");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidLocales() {
|
||||
// test if app crashes when invalid locale is loaded
|
||||
Translator.setTranslationLocale("ar_TD");
|
||||
}
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
en_EN,de_DE,es_ES,fr_FR,zh_Hans
|
||||
Bank,Bank,Banco,Banque,银行
|
||||
BLZ,BLZ,BLZ,BLZ,BLZ
|
||||
PIN,PIN,PIN,PIN,密码
|
||||
Balance,Kontostand,Saldo,Solde,余额
|
||||
Account type,Kontoart,Tipo de cuenta,Type de compte,账户类型
|
||||
Interest,Zinsen,Interés,Intérêts,利息
|
||||
Overdraft,Überziehungskredit,Descubierto,Découvert,透支
|
||||
Customer number,Kundennummer,Número de cliente,Numéro de client,客户编号
|
||||
Name,Name,Nombre,Nom du client,姓名
|
||||
Name,Name,Nombre,Nom et prénom,姓名
|
||||
Street,Straße,Calle,Rue,街道
|
||||
PLZ,PLZ,PLZ,PLZ,PLZ
|
||||
City,Ort,Ciudad,Ville,城市
|
||||
Password,Kennwort,Contraseña,Mot de passe,密码
|
||||
Login,Anmeldung,Inicio de sesión,Connexion,登录
|
||||
CurrentAccount,Girokonto,Cuenta corriente,Compte courant,活期账户
|
||||
SavingsAccount,Sparkonto,CuentaAhorro,Compte d'épargne,储蓄账户
|
||||
Address,Adresse,Dirección,Adresse,地址
|
||||
Logout,Abmelden,Cerrar sesión,Déconnexion,注销
|
||||
Transfer,Überweisung,Transferir,Virement,转账
|
||||
Deposit,Einzahlen,Ingresar,Dépôt,存款
|
||||
Take off,Abheben,Retirar,Enlever,取款
|
||||
Value,Wert,Valor,Valeur,价值
|
||||
Cancel,Abbrechen,Cancelar,Annuler,取消
|
||||
Load database,Datenbank laden,Cargar base de datos,Charger la base de données,加载数据库
|
||||
Invalid account,Ungültiges Konto,Cuenta no válida,Compte non valide,无效账户
|
||||
Could not transfer,Konnte nicht übertragen werden,No se ha podido transferir,Impossible de transférer,无法转账
|
||||
Transfer,Überweisung,Transferencia,Transférer,转账
|
||||
invalid amount,Ungültiger Betrag,Importe no válido,montant non valide,无效金额
|
||||
currency must not be blank,Währung darf nicht leer sein,La divisa no debe estar en blanco,la devise ne doit pas être vide,货币不得为空
|
||||
Transfer money,Geld überweisen,Transferencia de dinero,Transférer de l'argent,转账金额
|
||||
Could not take off money,Konnte Geld nicht abheben,No se ha podido retirar el dinero,Impossible de retirer de l'argent,无法取款
|
||||
Takeoff money,Geld abheben,Retirar dinero,Retirer de l'argent,取款
|
||||
Takeoff,Abheben,Despegue,Décoller,起飞
|
||||
Currency must not be blank,Die Währung darf nicht leer sein,La divisa no debe estar en blanco,La monnaie ne doit pas être en blanc,货币不得为空
|
||||
Cashmachine,Geldautomat,Cajero,Cashmachine,提款机
|
||||
Invalid IBAN,Ungültige IBAN,IBAN no válido,IBAN non valide,IBAN 无效
|
||||
Faulty login attempt,Fehlerhafter Anmeldeversuch,Intento de acceso erróneo,Tentative de connexion erronée,尝试登录失败
|
||||
Invalid PIN,Ungültige PIN,PIN no válido,PIN invalide,密码无效
|
||||
Invalid login credentials,Ungültige Anmeldedaten,Credenciales de inicio de sesión no válidas,Identifiants de connexion invalides,登录凭证无效
|
||||
Deposit money,Geld einzahlen,Depositar dinero,Dépôt d'argent,存款
|
||||
Interest rate,Zinssatz,Tipo de interés,Taux d'intérêt,利率
|
||||
Name/Family-name,Name/Familienname,Nombre y apellidos,Nom/Prénom de famille,姓名
|
||||
Address,Anschrift,Dirección,Adresse,地址
|
||||
Account,Konto,Cuenta,Compte,账户
|
||||
Closing JCash,JCash wird geschlossen,Cerrar JCash,Clôture de JCash,关闭 JCash
|
||||
you're logged out,Sie sind abgemeldet,has cerrado sesión,Vous êtes déconnecté,您已退出登录
|
||||
Comma separated value spreadsheet,Kommagetrennte Werte-Tabelle,Hoja de cálculo de valores separados por comas,Tableur de valeurs séparées par des virgules,逗号分隔值电子表格
|
||||
Value to transfer:,Zu übertragender Wert:,Valor a transferir:,Valeur à transférer :,要转账的金额:
|
||||
Balance after transfer:,Kontostand nach Überweisung:,Saldo después de la transferencia:,Solde après transfert :,转账后的余额
|
||||
Balance after takeoff:,Kontostand nach dem Abheben:,Saldo después de retirar:,Solde après décollage :,取出后的余额
|
||||
Value to deposit:,Einzuzahlender Wert:,Valor a ingresar:,Valeur à déposer :,存款价值
|
||||
Value to takeoff:,Auszuhalender Wert:,Valor a despegar:,Valeur à l'enlèvement :,起飞价值
|
|
Loading…
Reference in New Issue