Compare commits

...

30 Commits

Author SHA1 Message Date
Sven Vogel 571c516fc8 added information about submission grade 2023-08-07 16:12:48 +00:00
Sven Vogel 24c069b98e added support for windows-1252 2023-07-23 22:47:13 +02:00
Sven Vogel 787b806f9a added more javadoc 2023-07-23 20:15:41 +02:00
Sven Vogel 2fd1fc2b22 added some javadoc 2023-07-23 15:45:06 +02:00
Sven Vogel 88deb75ed7 added mainframe class 2023-07-23 14:56:41 +02:00
Sven Vogel 183b25e784 update to v2.0.0 2023-07-22 17:26:51 +02:00
Sven Vogel e2882d8c5e updated translations 2023-07-22 17:08:38 +02:00
Sven Vogel b6ec9ba52c various small fixes 2023-07-22 16:37:50 +02:00
Sven Vogel c88212b7e1 customized login button 2023-07-22 15:35:03 +02:00
Sven Vogel d0557a2a6d logout button now brings one back to the login screen 2023-07-22 14:31:00 +02:00
Sven Vogel f1f743d233 fixed banking dialogs input validation 2023-07-22 13:58:52 +02:00
Sven Vogel 8f17a6aa49 formatted source code 2023-07-19 19:38:36 +02:00
Sven Vogel 6f221c681c increased dialog width 2023-07-19 19:33:22 +02:00
Sven Vogel 04c18fe95e added GUI utility class 2023-07-19 19:25:38 +02:00
Sven Vogel 7fdb408900 finishing touches 2023-07-19 14:17:52 +02:00
Sven Vogel 9895ae03b9 added icon 2023-07-18 22:08:03 +02:00
Sven Vogel 97bafa3fa5 minor fixes 2023-07-18 21:24:49 +02:00
Sven Vogel 64f0b7d0c8 setting look and feel automatically 2023-07-17 18:22:24 +02:00
Sven Vogel d8bdad548d removed unused import 2023-07-16 23:29:50 +02:00
Sven Vogel e8da31486f added more logger documentation 2023-07-16 23:28:32 +02:00
Sven Vogel 1e0c3ff550 added translator test 2023-07-16 23:25:39 +02:00
Sven Vogel 1cf8d5ced7 Merge remote-tracking branch 'origin/main' 2023-07-16 23:15:35 +02:00
Sven Vogel 83f92f1ef3 added translation capabilities 2023-07-16 23:15:26 +02:00
Sven Vogel 7ee3f5e040 changed logger file name due to incompatibility to windows 2023-07-16 00:27:03 +02:00
Sven Vogel ad279a6b15 added one more test for percentage decoding 2023-07-12 21:52:06 +02:00
Sven Vogel 22a15c3bb9 changed handling of percentage parsing 2023-07-12 21:48:19 +02:00
Sven Vogel 8bf68f5d66 now handling single qoutes before numbers when decoding UIDs and currencies 2023-07-12 17:42:35 +02:00
Sven Vogel 01f22b94a2 changed used overdraft 2023-07-11 22:12:13 +02:00
Sven Vogel 2a0d97c834 Changes:
- added logger
- restructured internal dialogs
2023-07-11 20:43:40 +02:00
Sven Vogel 0840db44c2 updated regex and added error capturing for transactions 2023-07-11 17:15:19 +02:00
42 changed files with 1983 additions and 477 deletions

View File

@ -4,6 +4,8 @@
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.

View File

@ -0,0 +1,97 @@
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);
}
}
}

View File

@ -1,10 +1,19 @@
package me.teridax.jcash;
import me.teridax.jcash.gui.IconProvider;
import me.teridax.jcash.gui.Loader;
import me.teridax.jcash.gui.account.AccountController;
import me.teridax.jcash.gui.login.LoginController;
import me.teridax.jcash.gui.MainFrame;
import me.teridax.jcash.gui.Utils;
import me.teridax.jcash.lang.Locales;
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 {
@ -14,26 +23,97 @@ public final class Main {
private static Main instance;
/**
* Primary window of this program
* Primary class for controlling GUI of this application
*/
private final JFrame window;
private final MainFrame window;
private Main() {
// 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);
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);
}
}
public static void main(String[] args) {
initializeSystemLogger(Level.FINE);
setPlatformDependingTheme();
loadExtraFont();
Locales.autodetectDefaultLocale();
// create main instance and show the login screen
instance();
getInstance().showLoginScreen();
getInstance().loadDatabase();
}
/**
* 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() {
@ -43,55 +123,28 @@ 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();
}
/**
* 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, login.getData().getBms());
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");
System.exit(0);
}
});
public JFrame getWindow() {
return this.window.getWindow();
}
/**
* Logs the user out of the database, hiding the main window.
* 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.
*/
public void logout() {
window.setContentPane(new JLabel("you're logged out"));
window.setVisible(false);
this.window.logout();
}
}

View File

@ -1,8 +1,10 @@
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;
@ -33,6 +35,23 @@ public final class Bank {
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;
}
@ -45,7 +64,8 @@ public final class Bank {
* 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 owner the owner of the account
* @param account the account of the owner
*/
public void addAccount(Owner owner, Account account) {
@ -69,6 +89,7 @@ public final class Bank {
/**
* 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
*/
@ -76,24 +97,9 @@ public final class Bank {
return accounts.get(owner).toArray(Account[]::new);
}
/**
* 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);
}
/**
* 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
*/
@ -107,6 +113,7 @@ public final class Bank {
}
}
}
Logging.LOGGER.finer("Account not found: " + iban);
return Optional.empty();
}
}

View File

@ -1,9 +1,12 @@
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.
@ -14,15 +17,15 @@ public abstract class Account {
/**
* International bank account number
*/
private final int iban;
protected final int iban;
/**
* Personal identification number
*/
private final int pin;
protected final int pin;
/**
* Balance of this account
*/
private double balance;
protected double balance;
public Account(int iban, int pin, double balance) {
this.iban = iban;
@ -33,13 +36,15 @@ public abstract class Account {
/**
* 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
* @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]);
@ -50,15 +55,20 @@ public abstract class Account {
// try to detect the specific runtime class to deserialize
try {
if (type.equals("Sparkonto")) {
Logging.LOGGER.fine("Account detected as Sparkonto");
var interest = StringDecoder.decodePercent(columns[4]);
return new SavingsAccount(iban, pin, balance, interest);
} else if (type.equals("Girokonto")) {
Logging.LOGGER.fine("Account detected as Girokonto");
var overdraft = StringDecoder.decodeCurrency(columns[5]);
return new CurrentAccount(iban, pin, balance, overdraft);
} else
} else {
Logging.LOGGER.severe("Account type could not be detected");
throw new IllegalArgumentException("Invalid account type: " + type);
}
} catch (IllegalArgumentException | NullPointerException e) {
Logging.LOGGER.severe("Account field could not be decoded: " + e.getMessage());
throw new IllegalArgumentException("Account format: ", e);
}
}
@ -83,13 +93,14 @@ public abstract class Account {
/**
* 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 iban == ((Account) obj).iban;
return false;
}
@ -98,20 +109,24 @@ public abstract class Account {
* 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, getClass().getSimpleName());
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)
if (amount < 0) {
Logging.LOGGER.severe("Cannot deposit negative amount of money: " + amount);
throw new IllegalArgumentException("amount must be positive");
}
this.balance += amount;
}
@ -124,9 +139,16 @@ public abstract class Account {
/**
* 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) {
this.balance = Math.max(0, this.balance - amount);
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;
}
}

View File

@ -1,5 +1,7 @@
package me.teridax.jcash.banking.accounts;
import me.teridax.jcash.Logging;
/**
* Immutable currency account storing only overdraft.
* English equivalent to "Girokonto"
@ -19,4 +21,29 @@ public final class CurrentAccount extends Account {
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;
}
}

View File

@ -1,5 +1,6 @@
package me.teridax.jcash.banking.accounts;
import me.teridax.jcash.Logging;
import me.teridax.jcash.decode.StringDecoder;
import java.util.Objects;
@ -33,17 +34,20 @@ public final class Owner {
/**
* 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
* @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]);

View File

@ -6,10 +6,14 @@ 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
@ -17,6 +21,18 @@ import java.util.*;
*/
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
*/
@ -29,6 +45,7 @@ public final class BankingManagementSystem {
/**
* 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
@ -59,25 +76,30 @@ public final class BankingManagementSystem {
* </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
* @throws IllegalArgumentException if the file cannot be read or the containing data is invalid
* @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 = Files.readString(file);
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(";");
var columns = line.split(SEPARATOR);
// one line must contain exactly 14 columns
if (columns.length != 14)
throw new IllegalArgumentException("invalid column count: " + columns.length);
LOGGER.finer("reading members from line: " + line);
// read basic fields
var owner = Owner.fromColumns(tail(columns, 8));
var account = Account.fromColumns(tail(columns, 2));
@ -90,8 +112,10 @@ public final class BankingManagementSystem {
var bankOfSet = bms.banks.stream().filter(b -> b.equals(bankOfLine)).findFirst();
if (bankOfSet.isPresent()) {
LOGGER.fine("bank from current line is already present in management system");
bankOfSet.get().addAccount(owner, account);
} else {
LOGGER.fine("bank from current line is new for management system");
bankOfLine.addAccount(owner, account);
bms.banks.add(bankOfLine);
}
@ -99,15 +123,36 @@ public final class BankingManagementSystem {
return bms;
} catch (IOException e) {
throw new IllegalArgumentException("Could not read file " + file, e);
} catch (IllegalArgumentException | NullPointerException e) {
LOGGER.severe("Could not parse file: " + file + " due to: " + e.getMessage());
throw new IllegalArgumentException("Could not parse file " + file, e);
}
}
/**
* 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
*/

View File

@ -1,9 +1,12 @@
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.
@ -20,16 +23,21 @@ public class Profile {
* The bank that manages every account referenced by this profile
*/
private final Bank bank;
/**
* Primary or currently selected account.
*/
private Account primaryAccount;
/**
* 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;
@ -40,6 +48,22 @@ public class Profile {
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;
}
@ -51,18 +75,4 @@ public class Profile {
public Account[] getAccounts() {
return accounts;
}
/**
* 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;
}
}
}
}

View File

@ -0,0 +1,67 @@
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");
}
}

View File

@ -1,63 +1,105 @@
package me.teridax.jcash.decode;
import org.junit.Test;
import me.teridax.jcash.lang.Locales;
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());
}
/**
* Locale to use when converting strings
* Returns a NumberFormatter for parsing double values in the appropriate locale.
* @return the number formatter
*/
private static final Locale LOCALE = Locale.GERMANY;
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;
}
/**
* NumberFormat to use when converting strings
* Returns a NumberFormatter for parsing integer values in the appropriate locale.
* @return the number formatter
*/
public static final NumberFormat LOCAL_NUMBER_FORMAT = NumberFormat.getInstance(LOCALE);
public static NumberFormatter getIntegerNumberFormatter() {
var formatter = new NumberFormatter();
formatter.setValueClass(Integer.class);
formatter.setMinimum(0d);
formatter.setAllowsInvalid(true);
formatter.setCommitsOnValidEdit(true);
return formatter;
}
/**
* Attempts to convert the given string into a double value representing a percentage.
* The percentage is stored in the range [0,1] and can linearly be mapped to [0, 100] by multiplying with 100.
* 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.
*
* @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 the number and cut out optional percent symbols
var trimmed = number.trim().replace("%", "");
// trim and cut out weird leading single quotes for numbers
var prepared = number.trim().replaceAll("^\\s*['`](?=\\d)", "");
try {
return LOCAL_NUMBER_FORMAT.parse(trimmed).doubleValue();
} catch (ParseException ex) {
throw new IllegalArgumentException("Not a valid number: " + number, ex);
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");
}
}
/**
* 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 LOCAL_NUMBER_FORMAT.parse(currency.trim()).doubleValue();
return getNumberFormat().parse(prepared).doubleValue();
} catch (ParseException ex) {
throw new IllegalArgumentException("Not a valid currency in german locale: " + currency, ex);
}
@ -66,19 +108,21 @@ 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 {
LOCAL_NUMBER_FORMAT.setParseIntegerOnly(true);
var serialNumber = LOCAL_NUMBER_FORMAT.parse(number.trim());
LOCAL_NUMBER_FORMAT.setParseIntegerOnly(false);
var serialNumber = getNumberFormat().parse(preparedUID);
if (serialNumber.intValue() < 0)
throw new IllegalArgumentException("Not a valid unique identification number: " + number);
@ -92,17 +136,18 @@ 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("[\\w-\\s]+", Pattern.CASE_INSENSITIVE);
var pattern = Pattern.compile("[^\\d]+", Pattern.CASE_INSENSITIVE);
var matcher = pattern.matcher(trimmed);
if (matcher.find()) {
return matcher.group();
@ -113,15 +158,16 @@ 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+(/\\d+)?)?", Pattern.CASE_INSENSITIVE);
var pattern = Pattern.compile("\\S+(\\s+\\d+(\\s*/\\s*\\d+)?)?", Pattern.CASE_INSENSITIVE);
var matcher = pattern.matcher(street);
if (matcher.find()) {
return matcher.group();
@ -129,44 +175,4 @@ 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");
}
}

View File

@ -0,0 +1,50 @@
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");
}
}

View File

@ -0,0 +1,21 @@
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);
}
}

View File

@ -1,11 +1,14 @@
package me.teridax.jcash.gui;
import me.teridax.jcash.Logging;
import me.teridax.jcash.Main;
import me.teridax.jcash.banking.management.BankingManagementSystem;
import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
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.
@ -15,13 +18,15 @@ public class Loader {
/**
* Filter that only allows for files with *.csv extension
*/
private static final FileNameExtensionFilter FILE_FILTER = new FileNameExtensionFilter("Comma separated value spreadsheet", "csv", "CSV");
private static final FileNameExtensionFilter FILE_FILTER = new FileNameExtensionFilter(translate("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
*/
@ -33,7 +38,7 @@ public class Loader {
fileChooser.setDialogType(JFileChooser.OPEN_DIALOG);
fileChooser.setAcceptAllFileFilterUsed(false);
if (fileChooser.showDialog(null, "Load database") == APPROVE_OPTION) {
if (fileChooser.showDialog(Main.getInstance().getWindow(), translate("Load database")) == APPROVE_OPTION) {
// parse file content
try {
return BankingManagementSystem.loadFromCsv(fileChooser.getSelectedFile().toPath());
@ -42,6 +47,7 @@ public class Loader {
}
}
Logging.LOGGER.warning("no file selected");
throw new IllegalStateException("No file selected");
}
}

View File

@ -0,0 +1,128 @@
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;
}
}

View File

@ -0,0 +1,86 @@
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>&lt;html>&lt;h1>$string&lt;/h1>&lt;/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);
}
}

View File

@ -1,11 +1,12 @@
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.DepositDialog;
import me.teridax.jcash.gui.takeoff.TakeoffDialog;
import me.teridax.jcash.gui.transfer.TransferDialog;
import me.teridax.jcash.gui.deposit.DepositController;
import me.teridax.jcash.gui.takeoff.TakeoffController;
import me.teridax.jcash.gui.transfer.TransferController;
/**
* Controller for controlling the gui of an account.
@ -17,31 +18,73 @@ public class AccountController {
*/
private final AccountView view;
public AccountController(Profile profile, BankingManagementSystem bms) {
this.view = new AccountView();
this.view.setProfile(profile);
this.data = new AccountData(bms);
private Profile profile;
createListeners(profile);
public AccountController() {
this.view = new AccountView();
this.data = new AccountData();
}
private void createListeners(Profile profile) {
this.view.getAccountSelection().addActionListener(e -> {
var description = ((String) this.view.getAccountSelection().getSelectedItem());
profile.setPrimaryAccount(description);
this.view.setProfile(profile);
});
/**
* 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();
}
this.view.getLogout().addActionListener(e -> {
Main.getInstance().logout();
Main.getInstance().showLoginScreen();
});
/**
* 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());
}
this.view.getDeposit().addActionListener(e -> new DepositDialog(profile.getPrimaryAccount(), () -> this.view.setProfile(profile)));
/**
* Open dialog to deposit money
*/
private void depositMoney() {
new DepositController(profile.getPrimaryAccount());
this.view.updateAccountVariables(profile);
}
this.view.getTakeoff().addActionListener(e -> new TakeoffDialog(profile.getPrimaryAccount(), () -> this.view.setProfile(profile)));
/**
* Open dialog to transfer money
*/
private void transferMoney() {
new TransferController(profile.getPrimaryAccount(), data.getBms());
this.view.updateAccountVariables(profile);
}
this.view.getTransfer().addActionListener(e -> new TransferDialog(profile.getPrimaryAccount(), data.getBms(), () -> this.view.setProfile(profile)));
/**
* Open dialog to take off money
*/
private void takeoffMoney() {
new TakeoffController(profile.getPrimaryAccount());
this.view.updateAccountVariables(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);
}
public AccountView getView() {

View File

@ -2,15 +2,18 @@ package me.teridax.jcash.gui.account;
import me.teridax.jcash.banking.management.BankingManagementSystem;
/**
* Data storage class for account management
*/
public class AccountData {
private final BankingManagementSystem bms;
public AccountData(BankingManagementSystem bms) {
this.bms = bms;
}
private BankingManagementSystem bms;
public BankingManagementSystem getBms() {
return bms;
}
public void setBms(BankingManagementSystem bms) {
this.bms = bms;
}
}

View File

@ -1,5 +1,7 @@
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;
@ -7,8 +9,12 @@ import me.teridax.jcash.decode.StringDecoder;
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 {
@ -31,63 +37,47 @@ 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) {
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.LOCAL_NUMBER_FORMAT.format(account.getBalance()) + "");
this.type.setText(account.getClass().getSimpleName());
if (account instanceof CurrentAccount) {
this.typeSpecialLabel.setText("Overdraft");
this.typeSpecialProperty.setText( StringDecoder.LOCAL_NUMBER_FORMAT.format(((CurrentAccount) account).getOverdraft()) + "");
} else if (account instanceof SavingsAccount) {
this.typeSpecialLabel.setText("Interest rate");
this.typeSpecialProperty.setText( ((SavingsAccount) account).getInterest() + " %" );
}
this.updateAccountVariables(profile);
this.accountSelection.removeAllItems();
for (var otherAccount : profile.getAccounts()) {
this.accountSelection.addItem(otherAccount.getDescription());
}
this.accountSelection.setSelectedItem(account.getDescription());
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());
}
private void createLayout() {
var content = new JPanel(new GridBagLayout());
this.setLayout(new BorderLayout(16, 16));
this.setLayout(new BorderLayout(12, 12));
this.add(new JScrollPane(content), BorderLayout.CENTER);
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);
addInputRow(constraints, content, accountSelectionPanel, 1, new JLabel("IBAN", RIGHT));
addInputRow(constraints, content, name, 2, new JLabel("Name/Family-name", RIGHT));
addInputRow(constraints, content, address, 3, new JLabel("Address", RIGHT));
addInputRow(constraints, content, bankName, 4, new JLabel("Bank", RIGHT));
addInputRow(constraints, content, blz, 5, new JLabel("BLZ", RIGHT));
addInputRow(constraints, content, type, 6, new JLabel("Account", RIGHT));
addInputRow(constraints, content, typeSpecialProperty, 7, typeSpecialLabel);
addInputRow(constraints, content, balance, 8, new JLabel("Balance", RIGHT));
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"));
var buttonPanel = Box.createHorizontalBox();
buttonPanel.add(Box.createHorizontalStrut(4));
@ -125,34 +115,10 @@ public class AccountView extends JPanel {
this.accountSelection = new JComboBox<>();
this.logout = new JButton("Logout");
this.transfer = new JButton("Transfer");
this.deposit = new JButton("Deposit");
this.takeoff = new JButton("Takeoff");
}
/**
* 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 label the label to add to the left side
*/
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);
this.logout = new JButton(translate("Logout"));
this.transfer = new JButton(translate("Transfer"));
this.deposit = new JButton(translate("Deposit"));
this.takeoff = new JButton(translate("Takeoff"));
}
public JComboBox<String> getAccountSelection() {
@ -174,4 +140,40 @@ 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());
}
}
}

View File

@ -0,0 +1,84 @@
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();
}
}

View File

@ -1,17 +0,0 @@
package me.teridax.jcash.gui.deposit;
import me.teridax.jcash.banking.accounts.Account;
public class DepositDialog {
public DepositDialog(Account account, Runnable onDeposit) {
var view = new DepositView();
view.getDeposit().addActionListener(e -> {
account.deposit(view.getAmount());
onDeposit.run();
view.dispose();
});
view.getCancel().addActionListener(e -> view.dispose());
view.showDialog();
}
}

View File

@ -1,28 +1,52 @@
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 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() {
createComponents();
public DepositView(double maxValue) {
createComponents(maxValue);
layoutComponents();
}
public void showDialog() {
dialog.setIconImage(IconProvider.getWindowIcon());
dialog.setModalityType(Dialog.ModalityType.APPLICATION_MODAL);
dialog.setTitle("Deposit money");
dialog.setTitle(translate("Deposit money"));
dialog.pack();
dialog.setSize(dialog.getWidth() * 2, dialog.getHeight());
dialog.setResizable(false);
dialog.setLocationRelativeTo(null);
dialog.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
@ -36,70 +60,103 @@ 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;
dialog.getContentPane().add(new JLabel("Value", SwingConstants.RIGHT), c);
c.insets = new Insets(6, 6, 6, 6);
dialog.getContentPane().add(new JLabel(translate("Value"), SwingConstants.RIGHT), c);
c.gridx = 1;
c.gridy = 1;
c.gridy = 0;
c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.LAST_LINE_END;
c.weightx = 0.5;
dialog.getContentPane().add(value, c);
c.gridx = 2;
c.gridy = 1;
c.gridy = 0;
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 = 2;
c.gridy = 3;
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() {
private void createComponents(double maxValue) {
this.dialog = new JDialog();
this.cancel = new JButton("Cancel");
this.deposit = new JButton("Deposit");
this.value = new JFormattedTextField(StringDecoder.LOCAL_NUMBER_FORMAT);
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.dialog.setContentPane(new JPanel(new GridBagLayout()));
}
public double getAmount() {
/**
* 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 {
if (value.getText().isBlank())
return 0;
throw new InvalidInputException("currency value is blank or has been invalid whilst entered");
try {
return NumberFormat.getNumberInstance().parse(value.getText()).doubleValue();
return StringDecoder.getNumberFormat().parse(value.getText()).doubleValue();
} catch (ParseException e) {
throw new RuntimeException(e);
Logging.LOGGER.severe("Amount text field contains invalid value: " + value);
throw new InvalidInputException(e);
}
}
public JFormattedTextField getValue() {
return value;
}
public JButton getCancel() {
return cancel;
}
@ -111,4 +168,14 @@ 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));
}
}

View File

@ -5,11 +5,13 @@ import me.teridax.jcash.banking.management.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);

View File

@ -1,13 +1,12 @@
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 java.awt.event.ActionEvent;
import java.util.Optional;
import static javax.swing.JOptionPane.ERROR_MESSAGE;
import static javax.swing.JOptionPane.showMessageDialog;
public class LoginController {
private final LoginView view;
@ -15,13 +14,17 @@ public class LoginController {
private AccountSelectionListener listener;
public LoginController(BankingManagementSystem bms) {
public LoginController() {
this.view = new LoginView();
this.data = new LoginData(bms);
this.data = new LoginData();
addActionListeners();
}
public void setBankingManagementSystem(BankingManagementSystem bms) {
this.data.setBms(bms);
}
private void addActionListeners() {
this.view.getLogin().addActionListener(this::login);
}
@ -31,6 +34,7 @@ public class LoginController {
var iban = this.view.getIban().getText();
return Optional.of(Integer.parseUnsignedInt(iban));
} catch (NumberFormatException e) {
Logging.LOGGER.warning("IBAN text field contains invalid value: " + this.view.getIban().getText());
return Optional.empty();
}
}
@ -40,6 +44,7 @@ public class LoginController {
var iban = this.view.getPin().getPassword();
return Optional.of(Integer.parseUnsignedInt(new String(iban)));
} catch (NumberFormatException e) {
Logging.LOGGER.severe("PIN text field contains invalid value: " + String.valueOf(view.getPin().getPassword()));
return Optional.empty();
}
}
@ -48,13 +53,13 @@ public class LoginController {
var blz = this.view.getBlz().getText();
var iban = this.getIban();
if (iban.isEmpty()) {
showMessageDialog(null, "invalid IBAN", "Faulty login attempt", ERROR_MESSAGE);
Utils.error("invalid IBAN entered");
return;
}
var pin = this.getPin();
if (pin.isEmpty()) {
showMessageDialog(null, "invalid pin", "Faulty login attempt", ERROR_MESSAGE);
Utils.error("invalid PIN entered");
return;
}
@ -62,8 +67,8 @@ public class LoginController {
if (account.isPresent()) {
this.listener.onAccountSelected(account.get());
} else {
showMessageDialog(null, "invalid login credentials", "Faulty login attempt", ERROR_MESSAGE);
}
Logging.LOGGER.warning("invalid login credentials: " + iban + " / " + pin);
}
}
public void addAccountSelectionListener(AccountSelectionListener listener) {
@ -77,4 +82,8 @@ public class LoginController {
public LoginData getData() {
return data;
}
public void logout() {
this.view.getPin().setText("");
}
}

View File

@ -1,7 +1,9 @@
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 java.util.Optional;
@ -10,29 +12,41 @@ import java.util.Optional;
*/
public class LoginData {
private final BankingManagementSystem bms;
public LoginData(BankingManagementSystem bms) {
this.bms = bms;
}
private BankingManagementSystem bms;
/**
* authenticate the specified account with the provided pin.
* @param blz the bank identifier
*
* @param blz the bank identifier
* @param iban the account identifier
* @param pin the pin for the account to authenticate with
* @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())
if (optionalBank.isEmpty()) {
Utils.error("Unknown BLZ: " + blz);
return Optional.empty();
}
var profile = optionalBank.get().getAccount(iban);
return profile.filter(value -> value.getPrimaryAccount().getPin() == pin);
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 BankingManagementSystem getBms() {
return bms;
public void setBms(BankingManagementSystem bms) {
this.bms = bms;
}
}

View File

@ -1,12 +1,30 @@
package me.teridax.jcash.gui.login;
import javax.swing.*;
import javax.swing.text.NumberFormatter;
import java.awt.*;
import java.text.NumberFormat;
import me.teridax.jcash.gui.IconProvider;
import javax.swing.*;
import javax.swing.text.*;
import java.awt.*;
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;
@ -18,63 +36,85 @@ public class LoginView extends JPanel {
}
private void layoutComponents() {
var content = new JPanel(new GridBagLayout());
var content = new JLabel();
content.setIcon(new ImageIcon(IconProvider.getBackground()));
content.setLayout(new BorderLayout());
this.setBorder(BorderFactory.createEmptyBorder(8,8,8,8));
this.setLayout(new BorderLayout(16, 16));
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);
this.add(new JLabel("Bankautomat"), BorderLayout.NORTH);
var constraints = new GridBagConstraints();
constraints.gridwidth = 4;
constraints.insets = new Insets(12,12,12,12);
constraints.insets = new Insets(12, 12, 12, 12);
addInputRow(constraints, content, blz, 1, "BLZ");
addInputRow(constraints, content, iban, 2, "IBAN");
addInputRow(constraints, content, pin, 3, "PIN");
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"));
constraints.gridy = 4;
constraints.anchor = GridBagConstraints.PAGE_END;
constraints.weightx = 0;
constraints.fill = GridBagConstraints.NONE;
constraints.insets = new Insets(12,12,12,12);
content.add(login, constraints);
constraints.insets = new Insets(0, 0, 0, 12);
loginPane.add(login, constraints);
}
private void createComponents() {
this.blz = new JFormattedTextField();
this.iban = new JFormattedTextField(getNumberFormat());
this.iban = new JFormattedTextField();
this.pin = new JPasswordField();
this.login = new JButton("Login");
this.login = new JButton(translate("Login"));
// 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);
restrictPasswordToDigits();
restrictIbanInput();
}
private NumberFormatter getNumberFormat() {
var format = NumberFormat.getIntegerInstance();
format.setGroupingUsed(false);
/**
* 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;
var formatter = new NumberFormatter(format);
formatter.setValueClass(Integer.class);
formatter.setMinimum(0);
formatter.setMaximum(Integer.MAX_VALUE);
formatter.setAllowsInvalid(false);
return formatter;
if (newText.matches(String.format("\\d{1,%s}", MAX_PIN_DECIMAL_DIGITS))) {
super.replace(fb, offset, length, text, attrs);
}
}
});
}
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);
/**
* 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;
constraints.gridx = 2;
constraints.gridy = row;
constraints.weightx = 1;
constraints.fill = GridBagConstraints.HORIZONTAL;
target.add(comp, constraints);
if (newText.matches(String.format("\\d{1,%s}", MAX_PIN_DECIMAL_DIGITS))) {
super.replace(fb, offset, length, text, attrs);
}
}
});
}
public JTextField getBlz() {

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@ -0,0 +1,4 @@
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.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -0,0 +1,86 @@
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();
}
}

View File

@ -0,0 +1,20 @@
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;
}
}

View File

@ -1,17 +0,0 @@
package me.teridax.jcash.gui.takeoff;
import me.teridax.jcash.banking.accounts.Account;
public class TakeoffDialog {
public TakeoffDialog(Account account, Runnable onTakeoff) {
var view = new TakeoffView();
view.getTakeoff().addActionListener(e -> {
account.takeoff(view.getAmount());
onTakeoff.run();
view.dispose();
});
view.getCancel().addActionListener(e -> view.dispose());
view.showDialog();
}
}

View File

@ -1,31 +1,42 @@
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 javax.swing.*;
import java.awt.*;
import java.text.NumberFormat;
import java.text.ParseException;
import static javax.swing.JOptionPane.ERROR_MESSAGE;
import static javax.swing.JOptionPane.showMessageDialog;
import static me.teridax.jcash.lang.Translator.translate;
/**
* 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() {
createComponents();
public TakeoffView(double maxValue) {
createComponents(maxValue);
layoutComponents();
}
/**
* Makes this dialog visible.
*/
public void showDialog() {
dialog.setIconImage(IconProvider.getWindowIcon());
dialog.setModalityType(Dialog.ModalityType.APPLICATION_MODAL);
dialog.setTitle("Takeoff money");
dialog.setTitle(translate("Takeoff money"));
dialog.pack();
dialog.setSize(dialog.getWidth() * 2, dialog.getHeight());
dialog.setResizable(false);
dialog.setLocationRelativeTo(null);
dialog.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
@ -37,42 +48,61 @@ 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;
dialog.getContentPane().add(new JLabel("Value", SwingConstants.RIGHT), c);
c.insets = new Insets(6,6,6,6);
dialog.getContentPane().add(new JLabel(translate("Value"), SwingConstants.RIGHT), c);
c.gridx = 1;
c.gridy = 1;
c.gridy = 0;
c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.LAST_LINE_END;
c.weightx = 0.5;
dialog.getContentPane().add(value, c);
c.gridx = 2;
c.gridy = 1;
c.gridy = 0;
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 = 2;
c.gridy = 3;
c.gridwidth = 3;
c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.LAST_LINE_END;
@ -80,29 +110,50 @@ public class TakeoffView {
dialog.getContentPane().add(buttonPanel, c);
}
private void createComponents() {
/**
* The createComponents function creates the components of the dialog.
* @param maxValue Set the maximum value of the jformattedtextfield
*/
private void createComponents(double maxValue) {
this.dialog = new JDialog();
this.cancel = new JButton("Cancel");
this.takeoff = new JButton("Takeoff");
this.value = new JFormattedTextField(StringDecoder.LOCAL_NUMBER_FORMAT);
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.dialog.setContentPane(new JPanel(new GridBagLayout()));
}
public double getAmount() {
if (value.getText().isBlank()) {
showMessageDialog(null, "invalid amount", "currency must not be blank", ERROR_MESSAGE);
return 0;
}
/**
* 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");
try {
return NumberFormat.getNumberInstance().parse(value.getText()).doubleValue();
return StringDecoder.getNumberFormat().parse(value.getText()).doubleValue();
} catch (ParseException e) {
throw new RuntimeException(e);
Logging.LOGGER.severe("Amount text field contains invalid value: " + value);
throw new InvalidInputException(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;
}
@ -111,6 +162,10 @@ public class TakeoffView {
return takeoff;
}
public JFormattedTextField getValue() {
return value;
}
public void dispose() {
this.dialog.dispose();
}

View File

@ -0,0 +1,111 @@
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();
}
}

View File

@ -1,5 +1,6 @@
package me.teridax.jcash.gui.transfer;
import me.teridax.jcash.Logging;
import me.teridax.jcash.banking.management.BankingManagementSystem;
import me.teridax.jcash.decode.StringDecoder;
@ -13,27 +14,64 @@ public class TransferData {
/**
* 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 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())
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);
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())
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;
}
}
}

View File

@ -1,29 +0,0 @@
package me.teridax.jcash.gui.transfer;
import me.teridax.jcash.banking.accounts.Account;
import me.teridax.jcash.banking.management.BankingManagementSystem;
import static javax.swing.JOptionPane.ERROR_MESSAGE;
import static javax.swing.JOptionPane.showMessageDialog;
public class TransferDialog {
public TransferDialog(Account account, BankingManagementSystem bms, Runnable onDeposit) {
var view = new TransferView();
var data = new TransferData(bms);
view.getTransfer().addActionListener(e -> {
try {
var amount = view.getAmount();
data.transferValue(amount, view.getBlz(), view.getIban());
account.takeoff(amount);
onDeposit.run();
view.dispose();
} catch (IllegalArgumentException ex) {
showMessageDialog(null, "invalid account", "Could not transfer", ERROR_MESSAGE);
}
});
view.getCancel().addActionListener(e -> view.dispose());
view.showDialog();
}
}

View File

@ -1,15 +1,34 @@
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 javax.swing.*;
import java.awt.*;
import java.text.NumberFormat;
import java.text.ParseException;
import static javax.swing.JOptionPane.ERROR_MESSAGE;
import static javax.swing.JOptionPane.showMessageDialog;
import static me.teridax.jcash.lang.Translator.translate;
/**
* 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;
@ -18,15 +37,21 @@ public class TransferView {
private JFormattedTextField iban;
private JFormattedTextField blz;
private JFormattedTextField value;
private JLabel balanceAfterTransfer;
private JLabel enteredValue;
public TransferView() {
createComponents();
public TransferView(double maxValue) {
createComponents(maxValue);
layoutComponents();
}
/**
* Makes this dialog visible to the user
*/
public void showDialog() {
dialog.setIconImage(IconProvider.getWindowIcon());
dialog.setModalityType(Dialog.ModalityType.APPLICATION_MODAL);
dialog.setTitle("Transfer money");
dialog.setTitle(translate("Transfer money"));
dialog.pack();
dialog.setSize(dialog.getWidth() * 2, dialog.getHeight());
dialog.setResizable(false);
@ -35,100 +60,142 @@ 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;
dialog.getContentPane().add(new JLabel("BLZ", SwingConstants.RIGHT), c);
c.insets = new Insets(6,6,6,6);
dialog.getContentPane().add(new JLabel(translate("BLZ"), SwingConstants.RIGHT), c);
c.gridx = 1;
c.gridy = 1;
c.gridy = 0;
c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.LAST_LINE_END;
c.weightx = 0.5;
dialog.getContentPane().add(blz, c);
c.gridx = 2;
c.gridy = 1;
c.gridy = 0;
c.fill = GridBagConstraints.NONE;
c.anchor = GridBagConstraints.LAST_LINE_END;
c.weightx = 0;
dialog.getContentPane().add(new JLabel("IBAN", SwingConstants.RIGHT), c);
dialog.getContentPane().add(new JLabel(translate("IBAN"), SwingConstants.RIGHT), c);
c.gridx = 3;
c.gridy = 1;
c.gridy = 0;
c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.LAST_LINE_END;
c.weightx = 1;
dialog.getContentPane().add(iban, c);
c.gridx = 0;
c.gridy = 2;
c.gridy = 1;
c.fill = GridBagConstraints.NONE;
c.anchor = GridBagConstraints.LAST_LINE_END;
c.weightx = 0;
dialog.getContentPane().add(new JLabel("Betrag", SwingConstants.RIGHT), c);
dialog.getContentPane().add(new JLabel(translate("Betrag"), SwingConstants.RIGHT), c);
c.gridx = 1;
c.gridy = 1;
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(value, c);
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 = 3;
c.gridy = 4;
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() {
private void createComponents(double maxValue) {
this.dialog = new JDialog();
this.cancel = new JButton("Cancel");
this.transfer = new JButton("Transfer");
this.value = new JFormattedTextField(StringDecoder.LOCAL_NUMBER_FORMAT);
this.cancel = new JButton(translate("Cancel"));
this.transfer = new JButton(translate("Transfer"));
this.transfer.setEnabled(false);
this.value = new JFormattedTextField(StringDecoder.getNumberFormatter(maxValue));
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()));
}
public double getAmount() {
if (value.getText().isBlank()) {
showMessageDialog(null, "invalid amount", "currency must not be blank", ERROR_MESSAGE);
return 0;
}
/**
* 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");
try {
return NumberFormat.getNumberInstance().parse(value.getText()).doubleValue();
return StringDecoder.getNumberFormat().parse(value.getText()).doubleValue();
} catch (ParseException e) {
throw new RuntimeException(e);
Logging.LOGGER.severe("Amount text field contains invalid value: " + value);
throw new InvalidInputException(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;
}
@ -145,6 +212,14 @@ public class TransferView {
return blz.getText();
}
public JFormattedTextField getIbanTextField() {
return iban;
}
public JFormattedTextField getBlzTextField() {
return blz;
}
public void dispose() {
this.dialog.dispose();
}

View File

@ -0,0 +1,60 @@
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);
}
}

View File

@ -0,0 +1,130 @@
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;
}
}
}

View File

@ -0,0 +1,41 @@
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");
}
}

View File

@ -0,0 +1,54 @@
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 :,起飞价值
1 en_EN de_DE es_ES fr_FR zh_Hans
2 Bank Bank Banco Banque 银行
3 BLZ BLZ BLZ BLZ BLZ
4 PIN PIN PIN PIN 密码
5 Balance Kontostand Saldo Solde 余额
6 Account type Kontoart Tipo de cuenta Type de compte 账户类型
7 Interest Zinsen Interés Intérêts 利息
8 Overdraft Überziehungskredit Descubierto Découvert 透支
9 Customer number Kundennummer Número de cliente Numéro de client 客户编号
10 Name Name Nombre Nom du client 姓名
11 Name Name Nombre Nom et prénom 姓名
12 Street Straße Calle Rue 街道
13 PLZ PLZ PLZ PLZ PLZ
14 City Ort Ciudad Ville 城市
15 Password Kennwort Contraseña Mot de passe 密码
16 Login Anmeldung Inicio de sesión Connexion 登录
17 CurrentAccount Girokonto Cuenta corriente Compte courant 活期账户
18 SavingsAccount Sparkonto CuentaAhorro Compte d'épargne 储蓄账户
19 Address Adresse Dirección Adresse 地址
20 Logout Abmelden Cerrar sesión Déconnexion 注销
21 Transfer Überweisung Transferir Virement 转账
22 Deposit Einzahlen Ingresar Dépôt 存款
23 Take off Abheben Retirar Enlever 取款
24 Value Wert Valor Valeur 价值
25 Cancel Abbrechen Cancelar Annuler 取消
26 Load database Datenbank laden Cargar base de datos Charger la base de données 加载数据库
27 Invalid account Ungültiges Konto Cuenta no válida Compte non valide 无效账户
28 Could not transfer Konnte nicht übertragen werden No se ha podido transferir Impossible de transférer 无法转账
29 Transfer Überweisung Transferencia Transférer 转账
30 invalid amount Ungültiger Betrag Importe no válido montant non valide 无效金额
31 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 货币不得为空
32 Transfer money Geld überweisen Transferencia de dinero Transférer de l'argent 转账金额
33 Could not take off money Konnte Geld nicht abheben No se ha podido retirar el dinero Impossible de retirer de l'argent 无法取款
34 Takeoff money Geld abheben Retirar dinero Retirer de l'argent 取款
35 Takeoff Abheben Despegue Décoller 起飞
36 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 货币不得为空
37 Cashmachine Geldautomat Cajero Cashmachine 提款机
38 Invalid IBAN Ungültige IBAN IBAN no válido IBAN non valide IBAN 无效
39 Faulty login attempt Fehlerhafter Anmeldeversuch Intento de acceso erróneo Tentative de connexion erronée 尝试登录失败
40 Invalid PIN Ungültige PIN PIN no válido PIN invalide 密码无效
41 Invalid login credentials Ungültige Anmeldedaten Credenciales de inicio de sesión no válidas Identifiants de connexion invalides 登录凭证无效
42 Deposit money Geld einzahlen Depositar dinero Dépôt d'argent 存款
43 Interest rate Zinssatz Tipo de interés Taux d'intérêt 利率
44 Name/Family-name Name/Familienname Nombre y apellidos Nom/Prénom de famille 姓名
45 Address Anschrift Dirección Adresse 地址
46 Account Konto Cuenta Compte 账户
47 Closing JCash JCash wird geschlossen Cerrar JCash Clôture de JCash 关闭 JCash
48 you're logged out Sie sind abgemeldet has cerrado sesión Vous êtes déconnecté 您已退出登录
49 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 逗号分隔值电子表格
50 Value to transfer: Zu übertragender Wert: Valor a transferir: Valeur à transférer : 要转账的金额:
51 Balance after transfer: Kontostand nach Überweisung: Saldo después de la transferencia: Solde après transfert : 转账后的余额
52 Balance after takeoff: Kontostand nach dem Abheben: Saldo después de retirar: Solde après décollage : 取出后的余额
53 Value to deposit: Einzuzahlender Wert: Valor a ingresar: Valeur à déposer : 存款价值
54 Value to takeoff: Auszuhalender Wert: Valor a despegar: Valeur à l'enlèvement : 起飞价值