Compare commits

..

No commits in common. "main" and "javadoc" have entirely different histories.

47 changed files with 525 additions and 2122 deletions

View File

@ -1,34 +1,3 @@
## JCash # JCash
## About Draft program for the Java class of semester 2. The goal was to simulate basic cash machine that can read customer data from a .csv file and let the user view the data with curde forms of authentication.
Draft program for the Java class of semester 2. The goal was to simulate basic cash machine that can read customer data from a `.csv` file and let the user view the data with crude forms of authentication.
> This project was graded with 100,0 out of 100,0 points
## Overview
The program can read `.csv` file from disk and allows the user login to an account specified.
After login, the user can deposit, takeoff or transfer money from an account.
The application does not write any changes back into the file.
## Images
![preview.png](https://git.teridax.de/dhbw/JCash/raw/branch/main/meta/Preview.png)
## Repository structure
The `meta/` folder only contains metadata required for the README. Its not a build relevant folder.
Sample database files can be found under `res/`.
## CSV Database format
The first row is always ignored as this is generally contains the columns header.
Reference example:
| Bank | BLZ | Kontonummer | PIN | Kontostand | Kontoart | Zins | Überziehungsbetrag | Kundennummer | Name | Vorname | Straße |
| ---------------------- | -------- | ------------- | ------ | ------------ | ----------- | ------ | --------------------- | -------------- | --------- | --------- | ------------------------------ |
| VR Bank Rhein-Neckar | MA2424 | 4711 | 1234 | 45,90€ | Girokonto | | 1000€ | 8745364 | Gunther | Peter | Katzenköttel an der Strulle |
| Berliner Bank | 19087 | 987456 | 4578 | 9,05€ | Sparkonto | 3% | | 39845762 | Korb | Jauch | Unter der Brücke |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

View File

@ -1,97 +0,0 @@
package me.teridax.jcash;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.logging.*;
import static java.nio.file.Path.of;
/**
* Utility class for providing a global logger for the entire application instance at runtime
*/
public final class Logging {
/**
* Publicly available logger instance.
*/
public static final Logger LOGGER = Logger.getLogger(Logging.class.getName());
/**
* Folder the log files are stored in, relative to the file containing the class
*/
private static final String LOG_FOLDER_NAME = "logs/";
/**
* Format for the date time used for log files
*/
private static final String DATE_TIME_FORMAT = "yyyy-MM-dd_HH-mm-ss";
/**
* Initialize the global system logger.
* Adds a file logging handler
*/
static void initializeSystemLogger(Level level) {
LogManager.getLogManager().reset();
createConsoleLogger(level);
createFileLogger(level);
LOGGER.setLevel(level);
}
/**
* Add a console log handler writing to the applications stderr
*
* @param level the level to set the handler to
*/
private static void createConsoleLogger(Level level) {
ConsoleHandler ch = new ConsoleHandler();
ch.setLevel(level);
LOGGER.addHandler(ch);
}
/**
* Create a log handler writing to a file.
* The file will be located in a folder directly besides the application
* with the name LOG_FOLDER_NAME. The name will follow the format DATE_TIME_FORMAT plus "log" as
* file extension.
*
* @param level the log level to set the handler to
*/
private static void createFileLogger(Level level) {
// setup log file name
LocalDateTime now = LocalDateTime.now();
String dateTime = DateTimeFormatter.ofPattern(DATE_TIME_FORMAT).format(now);
String logFileName = LOG_FOLDER_NAME + dateTime + ".log";
// setup the folder for the logs
initializeLogFolder();
try {
FileHandler fh = new FileHandler(logFileName);
fh.setLevel(level);
LOGGER.addHandler(fh);
} catch (Exception e) {
LOGGER.warning("Unable to initialize logging for file: " + logFileName);
}
}
/**
* Checks if the folder containing log files is present.
* If the folder does not exist, the function will create a new folder.
*/
private static void initializeLogFolder() {
Path folderPath = of(LOG_FOLDER_NAME);
if (Files.isDirectory(folderPath))
return;
try {
Files.createDirectory(folderPath);
} catch (IOException e) {
LOGGER.warning("Unable to create directory: " + folderPath);
}
}
}

View File

@ -1,19 +1,10 @@
package me.teridax.jcash; package me.teridax.jcash;
import me.teridax.jcash.gui.IconProvider;
import me.teridax.jcash.gui.Loader; import me.teridax.jcash.gui.Loader;
import me.teridax.jcash.gui.MainFrame; import me.teridax.jcash.gui.account.AccountController;
import me.teridax.jcash.gui.Utils; import me.teridax.jcash.gui.login.LoginController;
import me.teridax.jcash.lang.Locales;
import javax.swing.*; 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 { public final class Main {
@ -23,99 +14,24 @@ public final class Main {
private static Main instance; private static Main instance;
/** /**
* Primary class for controlling GUI of this application * Primary window of this program
*/ */
private final MainFrame window; private final JFrame window;
private Main() { private Main() {
this.window = new MainFrame(); // create main window and set defaults
} this.window = new JFrame();
this.window.setTitle("Bankautomat");
/** this.window.setLocationByPlatform(true);
* Prompts the user a dialog to select a file to load the database from. this.window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
* 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) { public static void main(String[] args) {
initializeSystemLogger(Level.FINE);
setPlatformDependingTheme();
loadExtraFont();
Locales.autodetectDefaultLocale();
// create main instance and show the login screen // create main instance and show the login screen
instance(); instance();
getInstance().loadDatabase(); getInstance().showLoginScreen();
} }
/**
* Loads the extra font file used on the login button
*/
private static void loadExtraFont() {
try {
var font = Font.createFont(Font.TRUETYPE_FONT, Objects.requireNonNull(IconProvider.class.getResourceAsStream("res/Circus.ttf")));
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
ge.registerFont(font);
} catch (IOException | FontFormatException | NullPointerException e) {
LOGGER.warning("Could not load font file: " + e.getMessage());
}
}
/**
* Set the look and feel via the ui manager.
* This function will select a look and feel so that the application will
* look most like a native application.
* It will select the systems look and feel for Windows and MacOS
* and GTK for unix based systems.
*/
private static void setPlatformDependingTheme() {
// default look and feel
var laf = UIManager.getCrossPlatformLookAndFeelClassName();
// runtime os
String os = System.getProperty("os.name").toLowerCase();
LOGGER.fine("Detected operating system: " + os);
// set look and feel class name depending on the platform we run on
if (os.contains("win") || os.contains("mac")) {
LOGGER.info("Detected Windows or MacOS based operating system, using system look and feel");
laf = UIManager.getSystemLookAndFeelClassName();
} else if (os.contains("nix") || os.contains("nux") || os.contains("aix") || os.contains("sunos")) {
LOGGER.info("Detected Unix/Linux based operating system, using GTK look and feel");
laf = "com.sun.java.swing.plaf.gtk.GTKLookAndFeel";
} else {
LOGGER.warning("Unable to detect operating system: " + os);
}
try {
LOGGER.info("Setting look and feel to class: " + laf);
UIManager.setLookAndFeel(laf);
} catch (ClassNotFoundException | UnsupportedLookAndFeelException e) {
LOGGER.severe("Look and feel class not found: " + e.getMessage());
} catch (InstantiationException | IllegalAccessException e) {
LOGGER.warning("Could not set look and feel: " + e.getMessage());
}
}
/**
* Get the main singleton instance of this program
*
* @return the singleton instance of this class
*/
public static Main getInstance() { public static Main getInstance() {
return instance; return instance;
} }
@ -123,28 +39,55 @@ public final class Main {
/** /**
* Attempts to create a new instance of the singleton class Main. * Attempts to create a new instance of the singleton class Main.
* This method throws an exception if the class is already instantiated. * This method throws an exception if the class is already instantiated.
*
* @throws IllegalStateException 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() { public static void instance() {
if (null != instance) if (null != instance)
throw new IllegalStateException(Main.class.getName() + " is already initialized"); throw new IllegalStateException(Main.class.getName() + " is already initialized");
LOGGER.fine("Creating singleton instance of class " + Main.class.getName());
Main.instance = new Main(); Main.instance = new Main();
} }
public JFrame getWindow() { /**
return this.window.getWindow(); * Shows the open dialog for selecting a database file. After selection, it then proceeds to prompt login.
* Afterward the selected account can be managed.
* This method is non-blocking and all work described is performed asynchronously on the AWT Event dispatcher.
*/
public void showLoginScreen() {
SwingUtilities.invokeLater(() -> {
try {
// select db file
var path = Loader.load();
// read database and login
var login = new LoginController(path);
// when we have logged in set the account viewer as window content
login.addAccountSelectionListener(account -> {
var profileCont = new AccountController(account, 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);
}
});
} }
/** /**
* Logs the user out of the currently open account. * Logs the user out of the database, hiding the main window.
* This will show the login mask and clear the password field or the previous
* login attempt.
*/ */
public void logout() { public void logout() {
this.window.logout(); window.setContentPane(new JLabel("you're logged out"));
window.setVisible(false);
} }
} }

View File

@ -1,12 +1,9 @@
package me.teridax.jcash.banking.accounts; package me.teridax.jcash.banking;
import me.teridax.jcash.Logging;
import me.teridax.jcash.decode.StringDecoder; import me.teridax.jcash.decode.StringDecoder;
import java.util.Objects; import java.util.Objects;
import static me.teridax.jcash.lang.Translator.translate;
/** /**
* Base class for bank accounts. * Base class for bank accounts.
* Stores the iban, pin and balance. * Stores the iban, pin and balance.
@ -17,15 +14,15 @@ public abstract class Account {
/** /**
* International bank account number * International bank account number
*/ */
protected final int iban; private final int iban;
/** /**
* Personal identification number * Personal identification number
*/ */
protected final int pin; private final int pin;
/** /**
* Balance of this account * Balance of this account
*/ */
protected double balance; private double balance;
public Account(int iban, int pin, double balance) { public Account(int iban, int pin, double balance) {
this.iban = iban; this.iban = iban;
@ -36,15 +33,13 @@ public abstract class Account {
/** /**
* Parses a row of a fixed amount of columns into an 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. * This function will attempt to create an instance of two classes which inherit from Account.
*
* @param columns array of 6 strings * @param columns array of 6 strings
* @return either an instance of {@link SavingsAccount} or an instance of {@link CurrentAccount} * @return either an instance of {@link me.teridax.jcash.banking.SavingsAccount} or an instance of {@link me.teridax.jcash.banking.CurrentAccount}
* @throws IllegalArgumentException if the account type cannot be determined or the provided data is invalid * @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 { public static Account fromColumns(String[] columns) throws IllegalArgumentException, NullPointerException {
Objects.requireNonNull(columns); Objects.requireNonNull(columns);
Logging.LOGGER.finer("Parsing account from columns");
// deserialize fields // deserialize fields
var iban = StringDecoder.decodeUniqueIdentificationNumber(columns[0]); var iban = StringDecoder.decodeUniqueIdentificationNumber(columns[0]);
@ -55,20 +50,15 @@ public abstract class Account {
// try to detect the specific runtime class to deserialize // try to detect the specific runtime class to deserialize
try { try {
if (type.equals("Sparkonto")) { if (type.equals("Sparkonto")) {
Logging.LOGGER.fine("Account detected as Sparkonto");
var interest = StringDecoder.decodePercent(columns[4]); var interest = StringDecoder.decodePercent(columns[4]);
return new SavingsAccount(iban, pin, balance, interest); return new SavingsAccount(iban, pin, balance, interest);
} else if (type.equals("Girokonto")) { } else if (type.equals("Girokonto")) {
Logging.LOGGER.fine("Account detected as Girokonto");
var overdraft = StringDecoder.decodeCurrency(columns[5]); var overdraft = StringDecoder.decodeCurrency(columns[5]);
return new CurrentAccount(iban, pin, balance, overdraft); 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); throw new IllegalArgumentException("Invalid account type: " + type);
} }
} catch (IllegalArgumentException | NullPointerException e) { } catch (IllegalArgumentException | NullPointerException e) {
Logging.LOGGER.severe("Account field could not be decoded: " + e.getMessage());
throw new IllegalArgumentException("Account format: ", e); throw new IllegalArgumentException("Account format: ", e);
} }
} }
@ -90,17 +80,10 @@ public abstract class Account {
return Objects.hash(iban); return Objects.hash(iban);
} }
/**
* Returns true if the parameter is an instance of class {@link Account} and both their
* ibans are equal.
*
* @param obj the obj to compare to
* @return true if the parameter is an instance of class {@link Account} and their ibans match
*/
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (obj instanceof Account) if (obj instanceof Account)
return iban == ((Account) obj).iban; return iban == ((Account)obj).iban;
return false; return false;
} }
@ -109,25 +92,13 @@ public abstract class Account {
* Returns a description of the account in form a string. * Returns a description of the account in form a string.
* This method is not equal to {@link #toString()} but is intended to be * This method is not equal to {@link #toString()} but is intended to be
* a method used to retrieve a formatted representation for guis. * a method used to retrieve a formatted representation for guis.
*
* @return a basic description of the account in form a string * @return a basic description of the account in form a string
*/ */
public String getDescription() { public String getDescription() {
return String.format("%s (%s)", iban, translate(getClass().getSimpleName())); return String.format("%s (%s)", iban, getClass().getSimpleName());
}
/**
* Add a non-negative value onto the balance of this account.
*
* @param amount the amount of value to add
* @throws IllegalArgumentException if amount is negative
*/
public void deposit(double amount) throws IllegalArgumentException {
if (amount < 0) {
Logging.LOGGER.severe("Cannot deposit negative amount of money: " + amount);
throw new IllegalArgumentException("amount must be positive");
} }
public void deposit(double amount) {
this.balance += amount; this.balance += amount;
} }
@ -139,16 +110,9 @@ public abstract class Account {
/** /**
* Takeoff a certain amount of money from this accounts balance. * Takeoff a certain amount of money from this accounts balance.
* Saturates the result if the value to subtract is greater than the balance present. * 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 * @param amount the amount of money to subtract from the accounts balance
* @throws IllegalArgumentException if amount is greater than the balance present
*/ */
public void takeoff(double amount) throws IllegalArgumentException { public void takeoff(double amount) {
if (amount > this.balance) { this.balance = Math.max(0, this.balance - amount);
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,11 +1,5 @@
package me.teridax.jcash.banking; 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.*;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -29,29 +23,12 @@ public final class Bank {
*/ */
private final Map<Owner, Set<Account>> accounts; private final Map<Owner, Set<Account>> accounts;
public Bank(String blz, String name) { Bank(String blz, String name) {
this.blz = blz; this.blz = blz;
this.name = name; this.name = name;
this.accounts = new HashMap<>(); 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() { public String getBlz() {
return blz; return blz;
} }
@ -64,7 +41,6 @@ public final class Bank {
* Add a new account and its associated owner to this 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 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. * 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 * @param account the account of the owner
*/ */
@ -89,7 +65,6 @@ public final class Bank {
/** /**
* Retrieve all accounts owned by the specific owner. * Retrieve all accounts owned by the specific owner.
*
* @param owner the owner for which accounts are to be retrieved * @param owner the owner for which accounts are to be retrieved
* @return all accounts owned by the owner and managed by this bank * @return all accounts owned by the owner and managed by this bank
*/ */
@ -97,9 +72,24 @@ public final class Bank {
return accounts.get(owner).toArray(Account[]::new); 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. * Return a profile of the specified international bank account number.
*
* @param iban the number to create a profile for * @param iban the number to create a profile for
* @return the profile for the iban account * @return the profile for the iban account
*/ */
@ -113,7 +103,6 @@ public final class Bank {
} }
} }
} }
Logging.LOGGER.finer("Account not found: " + iban);
return Optional.empty(); return Optional.empty();
} }
} }

View File

@ -1,26 +0,0 @@
package me.teridax.jcash.banking;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
public class BankTest {
@Test
public void testBlzValidation() {
// everything is fine
assertEquals(Bank.validateBlz("MA2424"), "MA2424");
assertEquals(Bank.validateBlz("VR-BANK-567"), "VR-BANK-567");
assertEquals(Bank.validateBlz("19087"), "19087");
// cut stuff away
assertEquals(Bank.validateBlz("MA2%asd424"), "MA2");
assertEquals(Bank.validateBlz("qg0948 z67 0"), "qg0948");
}
@Test(expected = IllegalArgumentException.class)
public void testBlzValidationException() {
assertNotEquals(Bank.validateBlz("öüäöüäöü"), "öüäöüäöü");
}
}

View File

@ -1,19 +1,12 @@
package me.teridax.jcash.banking.management; package me.teridax.jcash.banking;
import me.teridax.jcash.banking.Bank;
import me.teridax.jcash.banking.accounts.Account;
import me.teridax.jcash.banking.accounts.Owner;
import me.teridax.jcash.decode.StringDecoder; import me.teridax.jcash.decode.StringDecoder;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; 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. * 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 * This class serves a read only database which can only modify runtime data without any respect to CRUD or the ACID
@ -21,18 +14,6 @@ import static me.teridax.jcash.Logging.LOGGER;
*/ */
public final class BankingManagementSystem { 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 * A set of banks
*/ */
@ -45,7 +26,6 @@ public final class BankingManagementSystem {
/** /**
* Utility method for retrieving the tail of a string array. * Utility method for retrieving the tail of a string array.
* This method return all items which follow the n-th index denoted by index. * This method return all items which follow the n-th index denoted by index.
*
* @param array the array to take the tail of * @param array the array to take the tail of
* @param index the amount trailing indices to skip * @param index the amount trailing indices to skip
* @return an array containing the last elements of the supplied array * @return an array containing the last elements of the supplied array
@ -76,30 +56,25 @@ public final class BankingManagementSystem {
* </ol> * </ol>
* The file can contain a header line which gets skipped when reading. * 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. * 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 * @param file the file to parse
* @return a valid BMS
* @throws IllegalArgumentException if the file cannot be read or the containing data is invalid * @throws IllegalArgumentException if the file cannot be read or the containing data is invalid
* @return a valid BMS
*/ */
public static BankingManagementSystem loadFromCsv(Path file) throws IllegalArgumentException { public static BankingManagementSystem loadFromCsv(Path file) throws IllegalArgumentException {
LOGGER.fine("parsing banking management system from file: " + Objects.toString(file, "null"));
try { try {
var bms = new BankingManagementSystem(); var bms = new BankingManagementSystem();
var content = getSource(file); var content = Files.readString(file);
// read line by line // read line by line
// and skip the first line // and skip the first line
content.lines().skip(1).forEach(line -> { content.lines().skip(1).forEach(line -> {
LOGGER.finest("splitting lines by separator: " + SEPARATOR);
// split the line into columns // split the line into columns
var columns = line.split(SEPARATOR); var columns = line.split(";");
// one line must contain exactly 14 columns // one line must contain exactly 14 columns
if (columns.length != 14) if (columns.length != 14)
throw new IllegalArgumentException("invalid column count: " + columns.length); throw new IllegalArgumentException("invalid column count: " + columns.length);
LOGGER.finer("reading members from line: " + line);
// read basic fields // read basic fields
var owner = Owner.fromColumns(tail(columns, 8)); var owner = Owner.fromColumns(tail(columns, 8));
var account = Account.fromColumns(tail(columns, 2)); var account = Account.fromColumns(tail(columns, 2));
@ -112,10 +87,8 @@ public final class BankingManagementSystem {
var bankOfSet = bms.banks.stream().filter(b -> b.equals(bankOfLine)).findFirst(); var bankOfSet = bms.banks.stream().filter(b -> b.equals(bankOfLine)).findFirst();
if (bankOfSet.isPresent()) { if (bankOfSet.isPresent()) {
LOGGER.fine("bank from current line is already present in management system");
bankOfSet.get().addAccount(owner, account); bankOfSet.get().addAccount(owner, account);
} else { } else {
LOGGER.fine("bank from current line is new for management system");
bankOfLine.addAccount(owner, account); bankOfLine.addAccount(owner, account);
bms.banks.add(bankOfLine); bms.banks.add(bankOfLine);
} }
@ -123,36 +96,15 @@ public final class BankingManagementSystem {
return bms; return bms;
} catch (IOException e) {
throw new IllegalArgumentException("Could not read file " + file, e);
} catch (IllegalArgumentException | NullPointerException 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); 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. * Return a bank with the given blz.
*
* @param blz the blz to search bank of * @param blz the blz to search bank of
* @return the bank with this blz or none * @return the bank with this blz or none
*/ */

View File

@ -0,0 +1,22 @@
package me.teridax.jcash.banking;
/**
* Immutable currency account storing only overdraft.
* English equivalent to "Girokonto"
*/
public final class CurrentAccount extends Account {
/**
* Overdraft amount in currency.
*/
private final double overdraft;
public CurrentAccount(int iban, int pin, double balance, double overdraft) {
super(iban, pin, balance);
this.overdraft = overdraft;
}
public double getOverdraft() {
return overdraft;
}
}

View File

@ -1,10 +1,10 @@
package me.teridax.jcash.banking.management; package me.teridax.jcash.banking;
import org.junit.Test; import org.junit.Test;
import java.nio.file.Paths; import java.nio.file.Paths;
public class BankingManagementSystemTest { public class DataClassTests {
@Test @Test
public void test() { public void test() {

View File

@ -1,6 +1,5 @@
package me.teridax.jcash.banking.accounts; package me.teridax.jcash.banking;
import me.teridax.jcash.Logging;
import me.teridax.jcash.decode.StringDecoder; import me.teridax.jcash.decode.StringDecoder;
import java.util.Objects; import java.util.Objects;
@ -34,7 +33,6 @@ public final class Owner {
/** /**
* Create a new instance of this class parsed from the columns. * Create a new instance of this class parsed from the columns.
*
* @param columns the fields of this class as strings * @param columns the fields of this class as strings
* @return an instance of this class * @return an instance of this class
* @throws IllegalArgumentException if the supplied columns is invalid * @throws IllegalArgumentException if the supplied columns is invalid
@ -42,12 +40,10 @@ public final class Owner {
*/ */
public static Owner fromColumns(String... columns) throws IllegalArgumentException, NullPointerException { public static Owner fromColumns(String... columns) throws IllegalArgumentException, NullPointerException {
Objects.requireNonNull(columns); Objects.requireNonNull(columns);
Logging.LOGGER.finer("parsing owner from columns");
if (columns.length != 6) if (columns.length != 6)
throw new IllegalArgumentException("Invalid number of columns: " + columns.length); throw new IllegalArgumentException("Invalid number of columns: " + columns.length);
Logging.LOGGER.finer("Decoding owner fields");
// decode fields // decode fields
var uid = StringDecoder.decodeUniqueIdentificationNumber(columns[0]); var uid = StringDecoder.decodeUniqueIdentificationNumber(columns[0]);
var familyName = StringDecoder.decodeName(columns[1]); var familyName = StringDecoder.decodeName(columns[1]);
@ -90,9 +86,9 @@ public final class Owner {
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (obj instanceof Owner) if (obj instanceof Owner) {
return this.uid == ((Owner) obj).getUid(); return this.uid == ((Owner) obj).getUid();
}
return false; return false;
} }

View File

@ -1,11 +1,4 @@
package me.teridax.jcash.banking.management; package me.teridax.jcash.banking;
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. * Groups an owner and all of its accounts registered at a specific bank together.
@ -15,29 +8,12 @@ import java.util.Arrays;
*/ */
public class Profile { public class Profile {
/**
* Owner of the primary account and all other accounts registered at a specific bank
*/
private final Owner owner; private final Owner owner;
/**
* The bank that manages every account referenced by this profile
*/
private final Bank bank; private final Bank bank;
/**
* All other account registered at a specific bank for the specified owner
*/
private final Account[] accounts;
/**
* Primary or currently selected account.
*/
private Account primaryAccount; private Account primaryAccount;
private final Account[] accounts;
public Profile(Owner owner, Bank bank, Account account, Account[] accounts) { 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.owner = owner;
this.bank = bank; this.bank = bank;
this.accounts = accounts; this.accounts = accounts;
@ -48,22 +24,6 @@ public class Profile {
return primaryAccount; 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() { public Owner getOwner() {
return owner; return owner;
} }
@ -75,4 +35,18 @@ public class Profile {
public Account[] getAccounts() { public Account[] getAccounts() {
return accounts; 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

@ -1,4 +1,4 @@
package me.teridax.jcash.banking.accounts; package me.teridax.jcash.banking;
/** /**
* Savings account representing a german "Sparkonto". * Savings account representing a german "Sparkonto".

View File

@ -1,49 +0,0 @@
package me.teridax.jcash.banking.accounts;
import me.teridax.jcash.Logging;
/**
* Immutable currency account storing only overdraft.
* English equivalent to "Girokonto"
*/
public final class CurrentAccount extends Account {
/**
* Overdraft amount in currency.
*/
private final double overdraft;
public CurrentAccount(int iban, int pin, double balance, double overdraft) {
super(iban, pin, balance);
this.overdraft = overdraft;
}
public double getOverdraft() {
return overdraft;
}
/**
* Takeoff a certain amount of money from this accounts balance.
* Saturates the result if the value to subtract is greater than the balance present.
*
* @param amount the amount of money to subtract from the accounts balance
* @throws IllegalArgumentException if amount is smaller than 0 or the overflow is greater than the overdraft.
*/
@Override
public void takeoff(double amount) throws IllegalArgumentException {
Logging.LOGGER.fine("taking off money: " + amount + " from account: " + iban);
var overflow = amount - getBalance();
if (overflow > 0) {
Logging.LOGGER.fine("taking off money with overflow: " + overflow);
if (overflow > overdraft) {
Logging.LOGGER.warning("amount to takeoff greater than overdraft");
throw new IllegalArgumentException("amount to takeoff greater than overdraft");
}
}
this.balance -= amount;
}
}

View File

@ -1,67 +0,0 @@
package me.teridax.jcash.decode;
import me.teridax.jcash.lang.Locales;
import org.junit.Test;
import static junit.framework.TestCase.assertEquals;
import static me.teridax.jcash.decode.StringDecoder.*;
public class DecodeTests {
@Test
public void testDecodeSuccessfulFunctions() {
// make sure a comma separated integer from fractions
Locales.setDefaultLocale("de", "DE");
assertEquals(decodePercent("1,003"), 100.3d, 1e-3d);
decodeUniqueIdentificationNumber("9578647895");
decodeUniqueIdentificationNumber(" 927856347 ");
decodeUniqueIdentificationNumber("0");
decodeUniqueIdentificationNumber("'9578647895");
decodeName("Adolf");
decodeName("Günther");
decodeName("Saßkia");
decodeStreet("Bahnhofstraße 40/1");
decodeStreet("Gülleweg 9");
decodeStreet("Echsengaße 67 / 4");
assertEquals(decodePercent("1,4%"), 1.4, 1e-3d);
assertEquals(decodePercent("99"), 9900.0d);
assertEquals(decodePercent("1,003%"), 1.003, 1e-5d);
assertEquals(decodePercent("1,003"), 100.3, 1e-5d);
assertEquals(decodePercent("'1,003"), 100.3, 1e-5d);
assertEquals(decodeCurrency("1,3€"), 1.3d);
assertEquals(decodeCurrency("0567€"), 567d);
assertEquals(decodeCurrency("145,34"), 145, 34d);
assertEquals(decodeCurrency("0,45 €"), 0.45d);
}
@Test
public void decodeLocaleDependent() {
Locales.setDefaultLocale("de", "DE");
assertEquals(decodeCurrency("13.00,45€"), 1300.45);
Locales.setDefaultLocale("en", "EN");
assertEquals(decodeCurrency("13,00.45€"), 1300.45);
}
@Test(expected = IllegalArgumentException.class)
public void testDecodeInvalidFunctions() {
decodeUniqueIdentificationNumber("q0948tvb6q047t 740 t74z0tz 784");
decodeUniqueIdentificationNumber("-39867.8475");
decodeUniqueIdentificationNumber("-398678475");
decodeUniqueIdentificationNumber(" ß9qu908t76q34798t6q734vb9843");
decodeName("John Doe");
decodeName("3490qt67v 0b34");
decodeName("Alexander9");
decodeName("-ga76re78g6$§");
decodeStreet("Bahnhofstraße -40/1");
decodeStreet("Gülleweg 9//567");
decodeStreet("23Echsengaße 67 / 4 Hofwetg 9");
}
}

View File

@ -1,57 +1,33 @@
package me.teridax.jcash.decode; package me.teridax.jcash.decode;
import me.teridax.jcash.lang.Locales; import org.junit.Test;
import javax.swing.text.NumberFormatter;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.text.ParseException; import java.text.ParseException;
import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.regex.Pattern; 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 * Utility class for converting various single line strings into a specific data type according
* to a format mostly dictated by a locale. * to a format mostly dictated by a locale.
*/ */
public class StringDecoder { public class StringDecoder {
public static NumberFormat getNumberFormat() {
return NumberFormat.getInstance(Locales.getDefaultLocale());
}
/** /**
* Returns a NumberFormatter for parsing double values in the appropriate locale. * Locale to use when converting strings
* @return the number formatter
*/ */
public static NumberFormatter getNumberFormatter(double maxValue) { private static final Locale LOCALE = Locale.GERMANY;
var formatter = new NumberFormatter();
formatter.setValueClass(Double.class);
formatter.setMinimum(0d);
formatter.setMaximum(maxValue);
formatter.setAllowsInvalid(true);
formatter.setCommitsOnValidEdit(true);
return formatter;
}
/** /**
* Returns a NumberFormatter for parsing integer values in the appropriate locale. * NumberFormat to use when converting strings
* @return the number formatter
*/ */
public static NumberFormatter getIntegerNumberFormatter() { public static final NumberFormat LOCAL_NUMBER_FORMAT = NumberFormat.getInstance(LOCALE);
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. * Attempts to convert the given string into a double value representing a percentage.
* The output value will be in the range [0, 100]. Strings formatted without a percentage * The percentage is stored in the range [0,1] and can linearly be mapped to [0, 100] by multiplying with 100.
* symbol will be assumed to be normalized and thus multiplied by 100 to retrieve the result in percent.
*
* @param number the string to convert * @param number the string to convert
* @return the double value * @return the double value
* @throws IllegalArgumentException when the format is invalid * @throws IllegalArgumentException when the format is invalid
@ -60,33 +36,18 @@ public class StringDecoder {
public static double decodePercent(String number) throws IllegalArgumentException, NullPointerException { public static double decodePercent(String number) throws IllegalArgumentException, NullPointerException {
Objects.requireNonNull(number); Objects.requireNonNull(number);
// trim and cut out weird leading single quotes for numbers // trim the number and cut out optional percent symbols
var prepared = number.trim().replaceAll("^\\s*['`](?=\\d)", ""); var trimmed = number.trim().replace("%", "");
var pattern = Pattern.compile("^([^%]+)?(%)?$", Pattern.CASE_INSENSITIVE);
var matcher = pattern.matcher(prepared);
if (matcher.find()) {
// if no percentage symbol is given the number will be multiplied by 100%
var scale = 1e2;
// check if capture group 2 captured a percentage symbol
// if to we don't want to apply any scaling to the value
if (null != matcher.group(2))
scale = 1;
try { try {
return getNumberFormat().parse(matcher.group(1)).doubleValue() * scale; return LOCAL_NUMBER_FORMAT.parse(trimmed).doubleValue();
} catch (ParseException ex) { } catch (ParseException ex) {
throw new IllegalArgumentException("Not a valid number: " + number, 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. * Attempts to convert the given string into a currency value.
*
* @param currency the string to convert * @param currency the string to convert
* @return the double value * @return the double value
* @throws IllegalArgumentException when the format is invalid * @throws IllegalArgumentException when the format is invalid
@ -95,11 +56,8 @@ public class StringDecoder {
public static double decodeCurrency(String currency) throws IllegalArgumentException, NullPointerException { public static double decodeCurrency(String currency) throws IllegalArgumentException, NullPointerException {
Objects.requireNonNull(currency); Objects.requireNonNull(currency);
// trim and cut out weird leading single quotes for numbers
var prepared = currency.trim().replaceAll("^\\s*['`](?=\\d)", "");
try { try {
return getNumberFormat().parse(prepared).doubleValue(); return LOCAL_NUMBER_FORMAT.parse(currency.trim()).doubleValue();
} catch (ParseException ex) { } catch (ParseException ex) {
throw new IllegalArgumentException("Not a valid currency in german locale: " + currency, ex); throw new IllegalArgumentException("Not a valid currency in german locale: " + currency, ex);
} }
@ -108,7 +66,6 @@ public class StringDecoder {
/** /**
* Attempts to convert the given string into universally unique number. * Attempts to convert the given string into universally unique number.
* This function does not check for duplicates. The number must be a positive integer. * This function does not check for duplicates. The number must be a positive integer.
*
* @param number the string to convert * @param number the string to convert
* @return the integer serial number * @return the integer serial number
* @throws IllegalArgumentException when the format is invalid * @throws IllegalArgumentException when the format is invalid
@ -117,12 +74,11 @@ public class StringDecoder {
public static int decodeUniqueIdentificationNumber(String number) throws IllegalArgumentException, NullPointerException { public static int decodeUniqueIdentificationNumber(String number) throws IllegalArgumentException, NullPointerException {
Objects.requireNonNull(number); 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 // check if the string is a valid unsigned number
try { try {
var serialNumber = getNumberFormat().parse(preparedUID); LOCAL_NUMBER_FORMAT.setParseIntegerOnly(true);
var serialNumber = LOCAL_NUMBER_FORMAT.parse(number.trim());
LOCAL_NUMBER_FORMAT.setParseIntegerOnly(false);
if (serialNumber.intValue() < 0) if (serialNumber.intValue() < 0)
throw new IllegalArgumentException("Not a valid unique identification number: " + number); throw new IllegalArgumentException("Not a valid unique identification number: " + number);
@ -136,7 +92,6 @@ public class StringDecoder {
/** /**
* Attempts to convert the given string into a name. * Attempts to convert the given string into a name.
* This method performs validation and trimming. * This method performs validation and trimming.
*
* @param name the string to convert * @param name the string to convert
* @return the qualified name * @return the qualified name
* @throws IllegalArgumentException when the format is invalid * @throws IllegalArgumentException when the format is invalid
@ -147,7 +102,7 @@ public class StringDecoder {
var trimmed = name.trim(); var trimmed = name.trim();
var pattern = Pattern.compile("[^\\d]+", Pattern.CASE_INSENSITIVE); var pattern = Pattern.compile("[\\w-\\s]+", Pattern.CASE_INSENSITIVE);
var matcher = pattern.matcher(trimmed); var matcher = pattern.matcher(trimmed);
if (matcher.find()) { if (matcher.find()) {
return matcher.group(); return matcher.group();
@ -158,7 +113,6 @@ public class StringDecoder {
/** /**
* Attempts to convert the given string into a street and an optional house address. * Attempts to convert the given string into a street and an optional house address.
*
* @param street the string to convert * @param street the string to convert
* @return the address name * @return the address name
* @throws IllegalArgumentException when the format is invalid * @throws IllegalArgumentException when the format is invalid
@ -167,7 +121,7 @@ public class StringDecoder {
public static String decodeStreet(String street) throws IllegalArgumentException, NullPointerException { public static String decodeStreet(String street) throws IllegalArgumentException, NullPointerException {
Objects.requireNonNull(street); Objects.requireNonNull(street);
var pattern = Pattern.compile("\\S+(\\s+\\d+(\\s*/\\s*\\d+)?)?", Pattern.CASE_INSENSITIVE); var pattern = Pattern.compile("\\S+(\\s+\\d+(/\\d+)?)?", Pattern.CASE_INSENSITIVE);
var matcher = pattern.matcher(street); var matcher = pattern.matcher(street);
if (matcher.find()) { if (matcher.find()) {
return matcher.group(); return matcher.group();
@ -175,4 +129,44 @@ public class StringDecoder {
throw new IllegalArgumentException("not a void address"); 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

@ -1,50 +0,0 @@
package me.teridax.jcash.gui;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Objects;
import static me.teridax.jcash.Logging.LOGGER;
/**
* Static class for providing the capabilities to load images from file.
*/
public class IconProvider {
private static final Image DEFAULT_IMAGE = new BufferedImage(256, 256, BufferedImage.TYPE_INT_RGB);
/**
* Fetches the windows icon.
* @return the windows icon
*/
public static Image getWindowIcon() {
return loadIcon("res/register.png");
}
/**
* Loads the specified image from disk.
* If the file cannot be made into an image because its corrupted or the file cannot be read,
* the default image is returned {@link #DEFAULT_IMAGE}
* @param path the path to the image
* @return the specified image or {@link #DEFAULT_IMAGE}
*/
private static Image loadIcon(String path) {
try {
var is = Objects.requireNonNull(IconProvider.class.getResourceAsStream(path));
return ImageIO.read(is);
} catch (Exception e) {
LOGGER.severe("Unable to load icon " + path + " because: " + e.getMessage());
}
return DEFAULT_IMAGE;
}
/**
* Fetches the background image used for the login screen
* @return login screen background image
*/
public static Image getBackground() {
return loadIcon("res/background.png");
}
}

View File

@ -1,21 +0,0 @@
package me.teridax.jcash.gui;
/**
* Exception thrown when some user input is invalid
*/
@SuppressWarnings("unused")
public class InvalidInputException extends IllegalStateException {
public InvalidInputException(String message, Exception cause) {
super(message, cause);
}
public InvalidInputException(String message) {
super(message);
}
public InvalidInputException(Exception cause) {
super(cause);
}
}

View File

@ -1,14 +1,12 @@
package me.teridax.jcash.gui; package me.teridax.jcash.gui;
import me.teridax.jcash.Logging; import me.teridax.jcash.banking.BankingManagementSystem;
import me.teridax.jcash.Main;
import me.teridax.jcash.banking.management.BankingManagementSystem;
import javax.swing.*; import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter; import javax.swing.filechooser.FileNameExtensionFilter;
import java.io.File;
import static javax.swing.JFileChooser.APPROVE_OPTION; 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. * Utility class for loading a BMS configuration from a csv file.
@ -18,15 +16,13 @@ public class Loader {
/** /**
* Filter that only allows for files with *.csv extension * Filter that only allows for files with *.csv extension
*/ */
private static final FileNameExtensionFilter FILE_FILTER = new FileNameExtensionFilter(translate("Comma separated value spreadsheet"), "csv", "CSV"); private static final FileNameExtensionFilter FILE_FILTER = new FileNameExtensionFilter("Comma separated value spreadsheet", "csv", "CSV");
private Loader() { private Loader() {}
}
/** /**
* Load a BMS from a csv file. Opens up a dialog which prompts the user to select a single file. * 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. * 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 * @return a valid BMS instance loaded from a file
* @throws IllegalStateException When either no file is selected or the selected files content is invalid * @throws IllegalStateException When either no file is selected or the selected files content is invalid
*/ */
@ -37,8 +33,9 @@ public class Loader {
fileChooser.setFileFilter(FILE_FILTER); fileChooser.setFileFilter(FILE_FILTER);
fileChooser.setDialogType(JFileChooser.OPEN_DIALOG); fileChooser.setDialogType(JFileChooser.OPEN_DIALOG);
fileChooser.setAcceptAllFileFilterUsed(false); fileChooser.setAcceptAllFileFilterUsed(false);
fileChooser.setCurrentDirectory(new File("/home/teridax/IdeaProjects/JCash/res"));
if (fileChooser.showDialog(Main.getInstance().getWindow(), translate("Load database")) == APPROVE_OPTION) { if (fileChooser.showDialog(null, "Load database") == APPROVE_OPTION) {
// parse file content // parse file content
try { try {
return BankingManagementSystem.loadFromCsv(fileChooser.getSelectedFile().toPath()); return BankingManagementSystem.loadFromCsv(fileChooser.getSelectedFile().toPath());
@ -47,7 +44,6 @@ public class Loader {
} }
} }
Logging.LOGGER.warning("no file selected");
throw new IllegalStateException("No file selected"); throw new IllegalStateException("No file selected");
} }
} }

View File

@ -1,128 +0,0 @@
package me.teridax.jcash.gui;
import me.teridax.jcash.banking.management.BankingManagementSystem;
import me.teridax.jcash.gui.account.AccountController;
import me.teridax.jcash.gui.login.LoginController;
import me.teridax.jcash.lang.Locales;
import javax.swing.*;
import java.awt.*;
import java.util.Objects;
import static me.teridax.jcash.Logging.LOGGER;
import static me.teridax.jcash.lang.Translator.translate;
public class MainFrame {
/**
* Constant used to identify the login screen on the cardlayout
*/
private static final String LOGIN_SCREEN_STRING_IDENT = "LoginScreen";
/**
* Constant used to identify the profile screen on the cardlayout
*/
private static final String PROFILE_SCREEN_STRING_IDENT = "ProfileScreen";
/**
* Version of this application
*/
private static final String VERSION = "v2.1.0";
/**
* Primary window of this program
*/
private final JFrame window;
/**
* Primary layout of this application
*/
private final CardLayout layout;
/**
* Database containing every bank, account and owner available
*/
private BankingManagementSystem bms;
private LoginController loginMask;
private AccountController accountController;
public MainFrame() {
// create main window and set defaults
this.window = new JFrame();
this.window.setTitle(translate("Cashmachine") + getInfoString());
this.window.setLocationByPlatform(true);
this.window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.window.setIconImage(IconProvider.getWindowIcon());
this.layout = new CardLayout();
this.window.getContentPane().setLayout(this.layout);
initialize();
}
/**
* Creates and returns a general information string about this application
* @return the locale and the current version as string
*/
private String getInfoString() {
return " locale: [" + Locales.getDefaultLocale().toString() + "] " + VERSION;
}
/**
* Initializes the GUI components of login screen and profile view
*/
private void initialize() {
// create the login mask
this.loginMask = new LoginController();
// when we have logged in set the account viewer as window content
this.loginMask.addAccountSelectionListener(account -> {
LOGGER.finer("account selected: " + Objects.toString(account, "null"));
accountController.setProfile(account, bms);
layout.show(window.getContentPane(), PROFILE_SCREEN_STRING_IDENT);
});
this.window.getContentPane().add(loginMask.getView(), LOGIN_SCREEN_STRING_IDENT);
// create the account viewer
this.accountController = new AccountController();
this.window.getContentPane().add(accountController.getView(), PROFILE_SCREEN_STRING_IDENT);
}
/**
* Sets the BMS of this application to use for the GUI.
* This method will show the login screen to the user
* @param bms the BMS to use for the GUI
*/
public void setBms(BankingManagementSystem bms) {
this.bms = bms;
this.loginMask.setBankingManagementSystem(bms);
this.showLoginScreen();
this.window.pack();
this.window.setResizable(false);
this.window.setLocationRelativeTo(null);
this.window.setVisible(true);
}
/**
* Shows the open dialog for selecting a database file. After selection, it then proceeds to prompt login.
* Afterward the selected account can be managed.
* This method is non-blocking and all work described is performed asynchronously on the AWT Event dispatcher.
*/
private void showLoginScreen() {
this.layout.show(this.window.getContentPane(), LOGIN_SCREEN_STRING_IDENT);
}
/**
* Logs the user out of the database, hiding the main window.
*/
public void logout() {
this.loginMask.logout();
this.layout.show(this.window.getContentPane(), LOGIN_SCREEN_STRING_IDENT);
}
public JFrame getWindow() {
return this.window;
}
}

View File

@ -1,86 +0,0 @@
package me.teridax.jcash.gui;
import me.teridax.jcash.Main;
import me.teridax.jcash.lang.Locales;
import javax.swing.*;
import java.awt.*;
import java.text.Format;
import java.text.NumberFormat;
public class Utils {
/**
* Formats the string so that it will be displayed as a Heading 1 element by JLabels.
* This embeds the given string into two html tags.
* Note that eny html entities in the string will be formatted as valid HTML entities.
* Meaning they won't show up in as plain text.
* @param title the title to format.
* @return the given string embedded into <pre>&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,12 +1,11 @@
package me.teridax.jcash.gui.account; package me.teridax.jcash.gui.account;
import me.teridax.jcash.Logging;
import me.teridax.jcash.Main; import me.teridax.jcash.Main;
import me.teridax.jcash.banking.management.BankingManagementSystem; import me.teridax.jcash.banking.BankingManagementSystem;
import me.teridax.jcash.banking.management.Profile; import me.teridax.jcash.banking.Profile;
import me.teridax.jcash.gui.deposit.DepositController; import me.teridax.jcash.gui.deposit.DepositDialog;
import me.teridax.jcash.gui.takeoff.TakeoffController; import me.teridax.jcash.gui.takeoff.TakeoffDialog;
import me.teridax.jcash.gui.transfer.TransferController; import me.teridax.jcash.gui.transfer.TransferDialog;
/** /**
* Controller for controlling the gui of an account. * Controller for controlling the gui of an account.
@ -18,73 +17,31 @@ public class AccountController {
*/ */
private final AccountView view; private final AccountView view;
private Profile profile; public AccountController(Profile profile, BankingManagementSystem bms) {
public AccountController() {
this.view = new AccountView(); this.view = new AccountView();
this.data = new AccountData();
}
/**
* Sets the profile and BMS used to manage banking.
* @param profile the profile used to manage the account
* @param bms the BMS used access other banking accounts
*/
public void setProfile(Profile profile, BankingManagementSystem bms) {
this.profile = profile;
this.view.setProfile(profile); this.view.setProfile(profile);
this.data.setBms(bms); this.data = new AccountData(bms);
this.createListeners();
createListeners(profile);
} }
/** private void createListeners(Profile profile) {
* Create listeners for GUI components this.view.getAccountSelection().addActionListener(e -> {
*/
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());
}
/**
* Open dialog to deposit money
*/
private void depositMoney() {
new DepositController(profile.getPrimaryAccount());
this.view.updateAccountVariables(profile);
}
/**
* Open dialog to transfer money
*/
private void transferMoney() {
new TransferController(profile.getPrimaryAccount(), data.getBms());
this.view.updateAccountVariables(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()); var description = ((String) this.view.getAccountSelection().getSelectedItem());
Logging.LOGGER.fine("Changing primary account selected: " + description); profile.setPrimaryAccount(description);
this.profile.setPrimaryAccount(description);
this.view.setProfile(profile); this.view.setProfile(profile);
});
this.view.getLogout().addActionListener(e -> {
Main.getInstance().logout();
Main.getInstance().showLoginScreen();
});
this.view.getDeposit().addActionListener(e -> new DepositDialog(profile.getPrimaryAccount(), () -> this.view.setProfile(profile)));
this.view.getTakeoff().addActionListener(e -> new TakeoffDialog(profile.getPrimaryAccount(), () -> this.view.setProfile(profile)));
this.view.getTransfer().addActionListener(e -> new TransferDialog(profile.getPrimaryAccount(), data.getBms(), () -> this.view.setProfile(profile)));
} }
public AccountView getView() { public AccountView getView() {

View File

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

View File

@ -1,20 +1,12 @@
package me.teridax.jcash.gui.account; package me.teridax.jcash.gui.account;
import me.teridax.jcash.Logging; import me.teridax.jcash.banking.*;
import me.teridax.jcash.banking.accounts.Account;
import me.teridax.jcash.banking.accounts.CurrentAccount;
import me.teridax.jcash.banking.accounts.SavingsAccount;
import me.teridax.jcash.banking.management.Profile;
import me.teridax.jcash.decode.StringDecoder; import me.teridax.jcash.decode.StringDecoder;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.util.Arrays;
import java.util.Comparator;
import static javax.swing.SwingConstants.RIGHT; 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 { public class AccountView extends JPanel {
@ -37,47 +29,63 @@ public class AccountView extends JPanel {
createComponents(); createComponents();
createLayout(); 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) { public void setProfile(Profile profile) {
this.updateAccountVariables(profile); var bank = profile.getBank();
var account = profile.getPrimaryAccount();
var owner = profile.getOwner();
this.blz.setText(bank.getBlz());
this.bankName.setText(bank.getName());
this.iban.setText(String.valueOf(account.getIban()));
this.name.setText(owner.getName() + " " + owner.getFamilyName());
this.address.setText(owner.getStreet() + " " + owner.getCity());
this.balance.setText(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.accountSelection.removeAllItems(); this.accountSelection.removeAllItems();
var accounts = profile.getAccounts(); for (var otherAccount : profile.getAccounts()) {
Arrays.stream(accounts).sorted(Comparator.comparingInt(Account::getIban)).forEach(a -> this.accountSelection.addItem(a.getDescription())); this.accountSelection.addItem(otherAccount.getDescription());
}
this.accountSelection.setSelectedItem(profile.getPrimaryAccount().getDescription()); this.accountSelection.setSelectedItem(account.getDescription());
} }
private void createLayout() { private void createLayout() {
var content = new JPanel(new GridBagLayout()); var content = new JPanel(new GridBagLayout());
this.setLayout(new BorderLayout(12, 12)); this.setLayout(new BorderLayout(16, 16));
this.add(new JScrollPane(content), BorderLayout.CENTER); this.add(new JScrollPane(content), BorderLayout.CENTER);
var constraints = new GridBagConstraints(); var constraints = new GridBagConstraints();
constraints.gridwidth = 4; 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)); var accountSelectionPanel = new JPanel(new BorderLayout(12, 0));
accountSelectionPanel.add(iban, BorderLayout.CENTER); accountSelectionPanel.add(iban, BorderLayout.CENTER);
accountSelectionPanel.add(accountSelection, BorderLayout.EAST); accountSelectionPanel.add(accountSelection, BorderLayout.EAST);
addGridBagRow(constraints, content, accountSelectionPanel, 1, translate("IBAN")); addInputRow(constraints, content, accountSelectionPanel, 1, new JLabel("IBAN", RIGHT));
addGridBagRow(constraints, content, name, 2, translate("Name/Family-name")); addInputRow(constraints, content, name, 2, new JLabel("Name/Family-name", RIGHT));
addGridBagRow(constraints, content, address, 3, translate("Address")); addInputRow(constraints, content, address, 3, new JLabel("Address", RIGHT));
addGridBagRow(constraints, content, bankName, 4, translate("Bank")); addInputRow(constraints, content, bankName, 4, new JLabel("Bank", RIGHT));
addGridBagRow(constraints, content, blz, 5, translate("BLZ")); addInputRow(constraints, content, blz, 5, new JLabel("BLZ", RIGHT));
addGridBagRow(constraints, content, type, 6, translate("Account")); addInputRow(constraints, content, type, 6, new JLabel("Account", RIGHT));
addGridBagRow(constraints, content, typeSpecialProperty, 7, typeSpecialLabel); addInputRow(constraints, content, typeSpecialProperty, 7, typeSpecialLabel);
addGridBagRow(constraints, content, balance, 8, translate("Balance")); addInputRow(constraints, content, balance, 8, new JLabel("Balance", RIGHT));
var buttonPanel = Box.createHorizontalBox(); var buttonPanel = Box.createHorizontalBox();
buttonPanel.add(Box.createHorizontalStrut(4)); buttonPanel.add(Box.createHorizontalStrut(4));
@ -115,10 +123,34 @@ public class AccountView extends JPanel {
this.accountSelection = new JComboBox<>(); this.accountSelection = new JComboBox<>();
this.logout = new JButton(translate("Logout")); this.logout = new JButton("Logout");
this.transfer = new JButton(translate("Transfer")); this.transfer = new JButton("Transfer");
this.deposit = new JButton(translate("Deposit")); this.deposit = new JButton("Deposit");
this.takeoff = new JButton(translate("Takeoff")); 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);
} }
public JComboBox<String> getAccountSelection() { public JComboBox<String> getAccountSelection() {
@ -140,40 +172,4 @@ public class AccountView extends JPanel {
public JButton getTakeoff() { public JButton getTakeoff() {
return takeoff; 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

@ -1,84 +0,0 @@
package me.teridax.jcash.gui.deposit;
import me.teridax.jcash.Logging;
import me.teridax.jcash.banking.accounts.Account;
import me.teridax.jcash.gui.InvalidInputException;
import me.teridax.jcash.gui.Utils;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.text.ParseException;
/**
* Class for controlling the deposit operation via a dialog.
*/
public class DepositController {
private final DepositView view;
/**
* Account to deposit money to.
*/
private final Account account;
public DepositController(Account account) {
this.account = account;
this.view = new DepositView(account.getBalance());
this.view.getDeposit().addActionListener(e -> depositMoney());
this.view.getCancel().addActionListener(e -> view.dispose());
this.view.getValue().getDocument().addDocumentListener(new DocumentListener() {
/**
* Validate the amount to deposit and update display
* variables.
*/
private void validateInputState() {
var balance = account.getBalance();
try {
view.getValue().commitEdit();
var amount = view.getAmount();
view.setCommittedValue(amount, balance + amount);
view.getDeposit().setEnabled(true);
} catch (InvalidInputException | ParseException ex) {
view.setCommittedValue(0, balance);
view.getDeposit().setEnabled(false);
}
}
@Override
public void insertUpdate(DocumentEvent documentEvent) {
validateInputState();
}
@Override
public void removeUpdate(DocumentEvent documentEvent) {
validateInputState();
}
@Override
public void changedUpdate(DocumentEvent documentEvent) {
validateInputState();
}
});
this.view.showDialog();
}
/**
* Deposit the last valid value to the account.
* This method may display error dialogs when no money can be deposited.
*/
private void depositMoney() {
try {
var amount = view.getAmount();
Logging.LOGGER.fine("Depositing money of account: " + account.getIban() + " amount: " + amount);
account.deposit(amount);
} catch (IllegalArgumentException ex) {
Logging.LOGGER.severe("Cannot deposit money of account: " + account.getIban() + " because: " + ex.getMessage());
Utils.error(ex.getMessage());
} catch (InvalidInputException ex) {
Utils.error(ex.getMessage());
}
view.dispose();
}
}

View File

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

View File

@ -1,52 +1,29 @@
package me.teridax.jcash.gui.deposit; package me.teridax.jcash.gui.deposit;
import me.teridax.jcash.Logging; import me.teridax.jcash.banking.Account;
import me.teridax.jcash.decode.StringDecoder; import me.teridax.jcash.decode.StringDecoder;
import me.teridax.jcash.gui.IconProvider;
import me.teridax.jcash.gui.InvalidInputException;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.text.NumberFormat;
import java.text.ParseException; 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 { public class DepositView {
/**
* Window to use
*/
private JDialog dialog; private JDialog dialog;
private JButton cancel; private JButton cancel;
/**
* Button for applying the deposit operation
*/
private JButton deposit; 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; private JFormattedTextField value;
public DepositView(double maxValue) { public DepositView() {
createComponents(maxValue); createComponents();
layoutComponents(); layoutComponents();
} }
public void showDialog() { public void showDialog() {
dialog.setIconImage(IconProvider.getWindowIcon());
dialog.setModalityType(Dialog.ModalityType.APPLICATION_MODAL); dialog.setModalityType(Dialog.ModalityType.APPLICATION_MODAL);
dialog.setTitle(translate("Deposit money")); dialog.setTitle("Deposit money");
dialog.pack(); dialog.pack();
dialog.setSize(dialog.getWidth() * 2, dialog.getHeight());
dialog.setResizable(false); dialog.setResizable(false);
dialog.setLocationRelativeTo(null); dialog.setLocationRelativeTo(null);
dialog.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); dialog.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
@ -60,103 +37,70 @@ public class DepositView {
c.gridx = 0; c.gridx = 0;
c.gridy = 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.gridwidth = 1;
c.fill = GridBagConstraints.NONE; c.fill = GridBagConstraints.NONE;
c.anchor = GridBagConstraints.LAST_LINE_END; c.anchor = GridBagConstraints.LAST_LINE_END;
c.weightx = 0; c.weightx = 0;
c.insets = new Insets(6, 6, 6, 6); dialog.getContentPane().add(new JLabel("Value", SwingConstants.RIGHT), c);
dialog.getContentPane().add(new JLabel(translate("Value"), SwingConstants.RIGHT), c);
c.gridx = 1; c.gridx = 1;
c.gridy = 0; c.gridy = 1;
c.fill = GridBagConstraints.HORIZONTAL; c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.LAST_LINE_END; c.anchor = GridBagConstraints.LAST_LINE_END;
c.weightx = 0.5; c.weightx = 0.5;
dialog.getContentPane().add(value, c); dialog.getContentPane().add(value, c);
c.gridx = 2; c.gridx = 2;
c.gridy = 0; c.gridy = 1;
c.fill = GridBagConstraints.NONE; c.fill = GridBagConstraints.NONE;
c.anchor = GridBagConstraints.LINE_START; c.anchor = GridBagConstraints.LINE_START;
c.weightx = 0; c.weightx = 0;
dialog.getContentPane().add(new JLabel(""), c); 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)); var buttonPanel = new JPanel(new FlowLayout(FlowLayout.TRAILING));
buttonPanel.add(cancel); buttonPanel.add(cancel);
buttonPanel.add(deposit); buttonPanel.add(deposit);
c.gridx = 0; c.gridx = 0;
c.gridy = 3; c.gridy = 2;
c.gridwidth = 3; c.gridwidth = 3;
c.fill = GridBagConstraints.HORIZONTAL; c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.LAST_LINE_END; c.anchor = GridBagConstraints.LAST_LINE_END;
c.insets = new Insets(10, 10, 10, 10);
dialog.getContentPane().add(buttonPanel, c); dialog.getContentPane().add(buttonPanel, c);
} }
private void createComponents(double maxValue) { private void createComponents() {
this.dialog = new JDialog(); this.dialog = new JDialog();
this.cancel = new JButton(translate("Cancel")); this.cancel = new JButton("Cancel");
this.deposit = new JButton(translate("Deposit")); this.deposit = new JButton("Deposit");
this.value = new JFormattedTextField(StringDecoder.getNumberFormatter(Double.MAX_VALUE)); this.value = new JFormattedTextField(StringDecoder.LOCAL_NUMBER_FORMAT);
this.enteredValue = new JLabel();
this.balanceAfterDeposit = new JLabel(StringDecoder.getNumberFormat().format(maxValue));
this.deposit.setEnabled(false);
this.dialog.setContentPane(new JPanel(new GridBagLayout())); 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()) if (value.getText().isBlank())
throw new InvalidInputException("currency value is blank or has been invalid whilst entered"); return 0;
try { try {
return StringDecoder.getNumberFormat().parse(value.getText()).doubleValue(); return NumberFormat.getNumberInstance().parse(value.getText()).doubleValue();
} catch (ParseException e) { } catch (ParseException e) {
Logging.LOGGER.severe("Amount text field contains invalid value: " + value); throw new RuntimeException(e);
throw new InvalidInputException(e);
} }
} }
public JFormattedTextField getValue() {
return value;
}
public JButton getCancel() { public JButton getCancel() {
return cancel; return cancel;
} }
@ -168,14 +112,4 @@ public class DepositView {
public void dispose() { public void dispose() {
this.dialog.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

@ -1,17 +1,15 @@
package me.teridax.jcash.gui.login; package me.teridax.jcash.gui.login;
import me.teridax.jcash.banking.management.Profile; import me.teridax.jcash.banking.Profile;
/** /**
* Listens for changes in a selected account. * Listens for changes in a selected account.
*/ */
@FunctionalInterface
public interface AccountSelectionListener { public interface AccountSelectionListener {
/** /**
* Run when a new account is selected. * Run when a new account is selected.
* The selected account is set as the primary account of the profile * The selected account is set as the primary account of the profile
*
* @param account the profile for the selected account * @param account the profile for the selected account
*/ */
void onAccountSelected(Profile account); void onAccountSelected(Profile account);

View File

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

View File

@ -1,52 +1,38 @@
package me.teridax.jcash.gui.login; package me.teridax.jcash.gui.login;
import me.teridax.jcash.Logging; import me.teridax.jcash.banking.BankingManagementSystem;
import me.teridax.jcash.banking.management.BankingManagementSystem; import me.teridax.jcash.banking.Profile;
import me.teridax.jcash.banking.management.Profile;
import me.teridax.jcash.gui.Utils;
import java.util.Optional; import java.util.Optional;
/** /**
* Wrapper class for a {@link BankingManagementSystem} * Wrapper class for a {@link me.teridax.jcash.banking.BankingManagementSystem}
*/ */
public class LoginData { public class LoginData {
private BankingManagementSystem bms; private final BankingManagementSystem bms;
public LoginData(BankingManagementSystem bms) {
this.bms = bms;
}
/** /**
* authenticate the specified account with the provided pin. * authenticate the specified account with the provided pin.
*
* @param blz the bank identifier * @param blz the bank identifier
* @param iban the account 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 * @return an optional wrapping the specified account if authentication was successful
*/ */
public Optional<Profile> authenticateAccount(String blz, int iban, int pin) { public Optional<Profile> authenticateAccount(String blz, int iban, int pin) {
Logging.LOGGER.info("Authenticating account " + iban);
var optionalBank = bms.getBank(blz); var optionalBank = bms.getBank(blz);
if (optionalBank.isEmpty()) { if (optionalBank.isEmpty())
Utils.error("Unknown BLZ: " + blz);
return Optional.empty(); return Optional.empty();
}
var profile = optionalBank.get().getAccount(iban); var profile = optionalBank.get().getAccount(iban);
if (profile.isEmpty()) { return profile.filter(value -> value.getPrimaryAccount().getPin() == pin);
Utils.error("Unknown account: " + iban);
return Optional.empty();
} }
var account = profile.filter(value -> value.getPrimaryAccount().getPin() == pin); public BankingManagementSystem getBms() {
if (account.isEmpty()) { return bms;
Utils.error("PIN does not match");
return Optional.empty();
}
return account;
}
public void setBms(BankingManagementSystem bms) {
this.bms = bms;
} }
} }

View File

@ -1,30 +1,12 @@
package me.teridax.jcash.gui.login; package me.teridax.jcash.gui.login;
import me.teridax.jcash.gui.IconProvider;
import javax.swing.*; import javax.swing.*;
import javax.swing.text.*; import javax.swing.text.NumberFormatter;
import java.awt.*; import java.awt.*;
import java.text.NumberFormat;
import static me.teridax.jcash.gui.Utils.addGridBagRow;
import static me.teridax.jcash.gui.Utils.addHeading;
import static me.teridax.jcash.lang.Translator.translate;
/**
* GUI class for login into an account
*/
public class LoginView extends JPanel { 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 blz;
private JFormattedTextField iban; private JFormattedTextField iban;
private JPasswordField pin; private JPasswordField pin;
@ -36,85 +18,64 @@ public class LoginView extends JPanel {
} }
private void layoutComponents() { private void layoutComponents() {
var content = new JLabel(); var content = new JPanel(new GridBagLayout());
content.setIcon(new ImageIcon(IconProvider.getBackground()));
content.setLayout(new BorderLayout());
var loginPane = new JPanel(new GridBagLayout()); this.setBorder(BorderFactory.createEmptyBorder(8,8,8,8));
loginPane.setOpaque(true); this.setLayout(new BorderLayout(16, 16));
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 JScrollPane(content), BorderLayout.CENTER);
this.add(new JLabel("Bankautomat"), BorderLayout.NORTH);
var constraints = new GridBagConstraints(); var constraints = new GridBagConstraints();
constraints.gridwidth = 4; constraints.gridwidth = 4;
constraints.insets = new Insets(12, 12, 12, 12); constraints.insets = new Insets(12,12,12,12);
addGridBagRow(constraints, loginPane, new JLabel(addHeading(translate("Cashmachine"))), 0, ""); addInputRow(constraints, content, blz, 1, "BLZ");
addGridBagRow(constraints, loginPane, blz, 1, translate("BLZ")); addInputRow(constraints, content, iban, 2, "IBAN");
addGridBagRow(constraints, loginPane, iban, 2, translate("IBAN")); addInputRow(constraints, content, pin, 3, "PIN");
addGridBagRow(constraints, loginPane, pin, 3, translate("PIN"));
constraints.gridy = 4; constraints.gridy = 4;
constraints.anchor = GridBagConstraints.PAGE_END; constraints.anchor = GridBagConstraints.PAGE_END;
constraints.weightx = 0; constraints.weightx = 0;
constraints.fill = GridBagConstraints.NONE; constraints.fill = GridBagConstraints.NONE;
constraints.insets = new Insets(0, 0, 0, 12); constraints.insets = new Insets(12,12,12,12);
loginPane.add(login, constraints); content.add(login, constraints);
} }
private void createComponents() { private void createComponents() {
this.blz = new JFormattedTextField(); this.blz = new JFormattedTextField("MA2424");
this.iban = new JFormattedTextField(); this.iban = new JFormattedTextField(getNumberFormat());
this.pin = new JPasswordField(); this.iban.setText("4711");
this.login = new JButton(translate("Login")); this.pin = new JPasswordField("1234");
this.login = new JButton("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() {
* Adds a document filter onto {@link #pin} that filters out everything that is not a digit. var format = NumberFormat.getIntegerInstance();
* The filter also restricts the amount of digits that can be entered to {@link #MAX_PIN_DECIMAL_DIGITS} format.setGroupingUsed(false);
*/
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;
if (newText.matches(String.format("\\d{1,%s}", MAX_PIN_DECIMAL_DIGITS))) { var formatter = new NumberFormatter(format);
super.replace(fb, offset, length, text, attrs); formatter.setValueClass(Integer.class);
} formatter.setMinimum(0);
} formatter.setMaximum(Integer.MAX_VALUE);
}); formatter.setAllowsInvalid(false);
return formatter;
} }
/** private void addInputRow(GridBagConstraints constraints, JComponent target, JComponent comp, int row, String name) {
* Adds a document filter onto {@link #iban} that filters out everything that is not a digit. constraints.gridwidth = 1;
* The filter also restricts the amount of digits that can be entered to {@link #MAX_PIN_DECIMAL_DIGITS} constraints.gridx = 1;
*/ constraints.gridy = row;
private void restrictIbanInput() { constraints.weightx = 0;
((AbstractDocument) this.iban.getDocument()).setDocumentFilter(new DocumentFilter() { constraints.fill = GridBagConstraints.HORIZONTAL;
@Override target.add(new JLabel(name, SwingConstants.RIGHT), constraints);
public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs)
throws BadLocationException {
String newText = fb.getDocument().getText(0, fb.getDocument().getLength()) + text;
if (newText.matches(String.format("\\d{1,%s}", MAX_PIN_DECIMAL_DIGITS))) { constraints.gridx = 2;
super.replace(fb, offset, length, text, attrs); constraints.gridy = row;
} constraints.weightx = 1;
} constraints.fill = GridBagConstraints.HORIZONTAL;
}); target.add(comp, constraints);
} }
public JTextField getBlz() { public JTextField getBlz() {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

View File

@ -1,4 +0,0 @@
https://pixabay.com/vectors/register-cash-register-modern-23666/
![register](https://cdn.pixabay.com/photo/2012/04/01/17/34/register-23666_960_720.png)
Font file
https://www.dafont.com/de/circus.font?text=Login

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

View File

@ -1,86 +0,0 @@
package me.teridax.jcash.gui.takeoff;
import me.teridax.jcash.Logging;
import me.teridax.jcash.banking.accounts.Account;
import me.teridax.jcash.banking.accounts.CurrentAccount;
import me.teridax.jcash.gui.InvalidInputException;
import me.teridax.jcash.gui.Utils;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.text.ParseException;
/**
* Controller class for handling bank account take off.
*/
public class TakeoffController {
/**
* Account to take off
*/
private final Account account;
/**
* GUI object
*/
private final TakeoffView view;
public TakeoffController(Account account) {
this.account = account;
// add overdraft on top of the maximum amount
// a user is allowed to take off
var overdraft = 0.0;
if (account instanceof CurrentAccount) {
overdraft += ((CurrentAccount) account).getOverdraft();
}
TakeoffData data = new TakeoffData(account.getBalance());
this.view = new TakeoffView(data.getMaxValue() + overdraft);
this.view.getTakeoff().addActionListener(e -> takeOff());
this.view.getCancel().addActionListener(e -> view.dispose());
this.view.getValue().getDocument().addDocumentListener(new DocumentListener() {
private void validateInputState() {
var balance = account.getBalance();
try {
view.getValue().commitEdit();
var amount = view.getAmount();
view.setCommittedValue(amount, balance - amount);
view.getTakeoff().setEnabled(true);
} catch (InvalidInputException | ParseException ex) {
view.setCommittedValue(0, balance);
view.getTakeoff().setEnabled(false);
}
}
@Override
public void insertUpdate(DocumentEvent documentEvent) {
validateInputState();
}
@Override
public void removeUpdate(DocumentEvent documentEvent) {
validateInputState();
}
@Override
public void changedUpdate(DocumentEvent documentEvent) {
validateInputState();
}
});
this.view.showDialog();
}
/**
* Attempts to take off some money from an account.
*/
private void takeOff() {
try {
account.takeoff(view.getAmount());
} catch (IllegalArgumentException | InvalidInputException ex) {
Logging.LOGGER.severe("Could not take off money: " + ex.getMessage());
Utils.error("Reason: " + ex.getMessage());
}
view.dispose();
}
}

View File

@ -1,20 +0,0 @@
package me.teridax.jcash.gui.takeoff;
/**
* Data class for taking off value from a certain account
*/
public class TakeoffData {
/**
* Maximum value a user is allowed to take off
*/
private final double maxValue;
public TakeoffData(double maxValue) {
this.maxValue = maxValue;
}
public double getMaxValue() {
return maxValue;
}
}

View File

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

View File

@ -1,42 +1,31 @@
package me.teridax.jcash.gui.takeoff; package me.teridax.jcash.gui.takeoff;
import me.teridax.jcash.Logging;
import me.teridax.jcash.decode.StringDecoder; import me.teridax.jcash.decode.StringDecoder;
import me.teridax.jcash.gui.IconProvider;
import me.teridax.jcash.gui.InvalidInputException;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.text.NumberFormat;
import java.text.ParseException; import java.text.ParseException;
import static me.teridax.jcash.lang.Translator.translate; import static javax.swing.JOptionPane.ERROR_MESSAGE;
import static javax.swing.JOptionPane.showMessageDialog;
/**
* Dialog for taking off money of an account.
*/
public class TakeoffView { public class TakeoffView {
private JDialog dialog; private JDialog dialog;
private JButton cancel; private JButton cancel;
private JButton takeoff; private JButton takeoff;
private JLabel enteredValue;
private JLabel balanceAfterDeposit;
private JFormattedTextField value; private JFormattedTextField value;
public TakeoffView(double maxValue) { public TakeoffView() {
createComponents(maxValue); createComponents();
layoutComponents(); layoutComponents();
} }
/**
* Makes this dialog visible.
*/
public void showDialog() { public void showDialog() {
dialog.setIconImage(IconProvider.getWindowIcon());
dialog.setModalityType(Dialog.ModalityType.APPLICATION_MODAL); dialog.setModalityType(Dialog.ModalityType.APPLICATION_MODAL);
dialog.setTitle(translate("Takeoff money")); dialog.setTitle("Takeoff money");
dialog.pack(); dialog.pack();
dialog.setSize(dialog.getWidth() * 2, dialog.getHeight());
dialog.setResizable(false); dialog.setResizable(false);
dialog.setLocationRelativeTo(null); dialog.setLocationRelativeTo(null);
dialog.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); dialog.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
@ -48,61 +37,42 @@ public class TakeoffView {
c.gridx = 0; c.gridx = 0;
c.gridy = 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.gridwidth = 1;
c.fill = GridBagConstraints.NONE; c.fill = GridBagConstraints.NONE;
c.anchor = GridBagConstraints.LAST_LINE_END; c.anchor = GridBagConstraints.LAST_LINE_END;
c.weightx = 0; c.weightx = 0;
c.insets = new Insets(6,6,6,6); dialog.getContentPane().add(new JLabel("Value", SwingConstants.RIGHT), c);
dialog.getContentPane().add(new JLabel(translate("Value"), SwingConstants.RIGHT), c);
c.gridx = 1; c.gridx = 1;
c.gridy = 0; c.gridy = 1;
c.fill = GridBagConstraints.HORIZONTAL; c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.LAST_LINE_END; c.anchor = GridBagConstraints.LAST_LINE_END;
c.weightx = 0.5; c.weightx = 0.5;
dialog.getContentPane().add(value, c); dialog.getContentPane().add(value, c);
c.gridx = 2; c.gridx = 2;
c.gridy = 0; c.gridy = 1;
c.fill = GridBagConstraints.NONE; c.fill = GridBagConstraints.NONE;
c.anchor = GridBagConstraints.LINE_START; c.anchor = GridBagConstraints.LINE_START;
c.weightx = 0; c.weightx = 0;
dialog.getContentPane().add(new JLabel(""), c); 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)); var buttonPanel = new JPanel(new FlowLayout(FlowLayout.TRAILING));
buttonPanel.add(cancel); buttonPanel.add(cancel);
buttonPanel.add(takeoff); buttonPanel.add(takeoff);
c.gridx = 0; c.gridx = 0;
c.gridy = 3; c.gridy = 2;
c.gridwidth = 3; c.gridwidth = 3;
c.fill = GridBagConstraints.HORIZONTAL; c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.LAST_LINE_END; c.anchor = GridBagConstraints.LAST_LINE_END;
@ -110,50 +80,29 @@ public class TakeoffView {
dialog.getContentPane().add(buttonPanel, c); 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.dialog = new JDialog();
this.cancel = new JButton(translate("Cancel")); this.cancel = new JButton("Cancel");
this.takeoff = new JButton(translate("Takeoff")); this.takeoff = new JButton("Takeoff");
this.value = new JFormattedTextField(StringDecoder.getNumberFormatter(maxValue)); this.value = new JFormattedTextField(StringDecoder.LOCAL_NUMBER_FORMAT);
this.enteredValue = new JLabel();
this.balanceAfterDeposit = new JLabel(StringDecoder.getNumberFormat().format(maxValue));
this.dialog.setContentPane(new JPanel(new GridBagLayout())); this.dialog.setContentPane(new JPanel(new GridBagLayout()));
} }
/** public double getAmount() {
* The getAmount function is used to get the amount of currency that has been entered into the text field. if (value.getText().isBlank()) {
* @return A double value, which is the parsed amount from the text field showMessageDialog(null, "invalid amount", "currency must not be blank", ERROR_MESSAGE);
*/ return 0;
public double getAmount() throws InvalidInputException { }
if (value.getText().isBlank())
throw new InvalidInputException("currency value is blank or has been invalid whilst entered");
try { try {
return StringDecoder.getNumberFormat().parse(value.getText()).doubleValue(); return NumberFormat.getNumberInstance().parse(value.getText()).doubleValue();
} catch (ParseException e) { } catch (ParseException e) {
Logging.LOGGER.severe("Amount text field contains invalid value: " + value); throw new RuntimeException(e);
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() { public JButton getCancel() {
return cancel; return cancel;
} }
@ -162,10 +111,6 @@ public class TakeoffView {
return takeoff; return takeoff;
} }
public JFormattedTextField getValue() {
return value;
}
public void dispose() { public void dispose() {
this.dialog.dispose(); this.dialog.dispose();
} }

View File

@ -1,111 +0,0 @@
package me.teridax.jcash.gui.transfer;
import me.teridax.jcash.Logging;
import me.teridax.jcash.Main;
import me.teridax.jcash.banking.accounts.Account;
import me.teridax.jcash.banking.accounts.CurrentAccount;
import me.teridax.jcash.banking.management.BankingManagementSystem;
import me.teridax.jcash.gui.InvalidInputException;
import me.teridax.jcash.gui.Utils;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.text.ParseException;
/**
* Dialog class for transferring some value from one account to another
*/
public class TransferController {
private final Account account;
private final TransferData transferData;
private final TransferView view;
public TransferController(Account account, BankingManagementSystem bms) {
this.account = account;
var overdraft = 0.0;
if (account instanceof CurrentAccount) {
overdraft += ((CurrentAccount) account).getOverdraft();
}
this.view = new TransferView(account.getBalance() + overdraft);
this.transferData = new TransferData(bms);
this.view.getTransfer().addActionListener(e -> transfer());
this.view.getCancel().addActionListener(e -> view.dispose());
// validates the users input
var validator = new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent documentEvent) {
validateInputState();
}
@Override
public void removeUpdate(DocumentEvent documentEvent) {
validateInputState();
}
@Override
public void changedUpdate(DocumentEvent documentEvent) {
validateInputState();
}
};
this.view.getValue().getDocument().addDocumentListener(validator);
this.view.getIbanTextField().getDocument().addDocumentListener(validator);
this.view.getBlzTextField().getDocument().addDocumentListener(validator);
this.view.showDialog();
}
/**
* Returns true if the target bank account is valid.
* @return true if the target bank account is valid false otherwise
*/
private boolean validateTargetAccount() {
if (transferData.validateBLZ(this.view.getBlz())) {
return transferData.validateIBAN(this.view.getBlz(), view.getIban());
}
return false;
}
/**
* Returns true if the entered value to transfer is valid.
* This method will also commit the valid value back to the text field.
* @return true if the value to transfer is valid, false otherwise
*/
private boolean validateTransferValue() {
var balance = account.getBalance();
try {
view.getValue().commitEdit();
var amount = view.getAmount();
view.setCommittedValue(amount, balance - amount);
return true;
} catch (InvalidInputException | ParseException ex) {
view.setCommittedValue(0, balance);
return false;
}
}
private void validateInputState() {
var valid = validateTargetAccount() && validateTransferValue();
view.getTransfer().setEnabled(valid);
}
/**
* Attempts to transfer the balance from one account to another
* This method will close the dialog.
*/
private void transfer() {
try {
var amount = view.getAmount();
this.account.takeoff(amount);
this.transferData.transferValue(amount, view.getBlz(), view.getIban());
} catch (IllegalArgumentException | InvalidInputException ex) {
Logging.LOGGER.severe("Could not transfer: " + ex.getMessage());
Utils.error("Reason: " + ex.getMessage());
}
this.view.dispose();
}
}

View File

@ -1,7 +1,6 @@
package me.teridax.jcash.gui.transfer; package me.teridax.jcash.gui.transfer;
import me.teridax.jcash.Logging; import me.teridax.jcash.banking.BankingManagementSystem;
import me.teridax.jcash.banking.management.BankingManagementSystem;
import me.teridax.jcash.decode.StringDecoder; import me.teridax.jcash.decode.StringDecoder;
public class TransferData { public class TransferData {
@ -14,64 +13,27 @@ public class TransferData {
/** /**
* Transfers a certain amount of money to the specified account of the specified bank * Transfers a certain amount of money to the specified account of the specified bank
*
* @param amount the amount to transfer * @param amount the amount to transfer
* @param blz the bank that manages the account * @param blz the bank that manages the account
* @param ibanString the internal bank account number to transfer money to * @param ibanString the internal bank account number to transfer money to
* @throws IllegalArgumentException if the bank or the account do not exist * @throws IllegalArgumentException if the bank or the account do not exist
*/ */
public void transferValue(double amount, String blz, String ibanString) throws IllegalArgumentException { public void transferValue(double amount, String blz, String ibanString) throws IllegalArgumentException {
// get bank to transfer to
var bank = bms.getBank(blz); 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); throw new IllegalArgumentException("Bank not found: " + blz);
}
// validate iban of target account
var iban = 0; var iban = 0;
try { try {
iban = StringDecoder.decodeUniqueIdentificationNumber(ibanString); iban = StringDecoder.decodeUniqueIdentificationNumber(ibanString);
} catch (Exception ex) { } catch (Exception ex) {
Logging.LOGGER.warning("IBAN has invalid format: " + ibanString + " because: " + ex.getMessage());
throw new IllegalArgumentException("IBAN has invalid format: " + ibanString); throw new IllegalArgumentException("IBAN has invalid format: " + ibanString);
} }
// get account to transfer value to
var account = bank.get().getAccount(iban); 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); throw new IllegalArgumentException("Account not found: " + iban);
}
account.get().getPrimaryAccount().deposit(amount); 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

@ -0,0 +1,31 @@
package me.teridax.jcash.gui.transfer;
import me.teridax.jcash.banking.Account;
import me.teridax.jcash.banking.BankingManagementSystem;
import javax.swing.*;
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,34 +1,15 @@
package me.teridax.jcash.gui.transfer; package me.teridax.jcash.gui.transfer;
import me.teridax.jcash.Logging;
import me.teridax.jcash.decode.StringDecoder; import me.teridax.jcash.decode.StringDecoder;
import me.teridax.jcash.gui.IconProvider;
import me.teridax.jcash.gui.InvalidInputException;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.text.NumberFormat;
import java.text.ParseException; import java.text.ParseException;
import static me.teridax.jcash.lang.Translator.translate; import static javax.swing.JOptionPane.ERROR_MESSAGE;
import static javax.swing.JOptionPane.showMessageDialog;
/**
* JDialog for displaying the GUI for a transfer dialog
* with the following crude layout:
* <pre>
BLZ IBAN
VALUE
Cancel Transfer
* </pre>
*/
public class TransferView { public class TransferView {
private JDialog dialog; private JDialog dialog;
@ -37,21 +18,15 @@ public class TransferView {
private JFormattedTextField iban; private JFormattedTextField iban;
private JFormattedTextField blz; private JFormattedTextField blz;
private JFormattedTextField value; private JFormattedTextField value;
private JLabel balanceAfterTransfer;
private JLabel enteredValue;
public TransferView(double maxValue) { public TransferView() {
createComponents(maxValue); createComponents();
layoutComponents(); layoutComponents();
} }
/**
* Makes this dialog visible to the user
*/
public void showDialog() { public void showDialog() {
dialog.setIconImage(IconProvider.getWindowIcon());
dialog.setModalityType(Dialog.ModalityType.APPLICATION_MODAL); dialog.setModalityType(Dialog.ModalityType.APPLICATION_MODAL);
dialog.setTitle(translate("Transfer money")); dialog.setTitle("Transfer money");
dialog.pack(); dialog.pack();
dialog.setSize(dialog.getWidth() * 2, dialog.getHeight()); dialog.setSize(dialog.getWidth() * 2, dialog.getHeight());
dialog.setResizable(false); dialog.setResizable(false);
@ -60,142 +35,100 @@ public class TransferView {
dialog.setVisible(true); dialog.setVisible(true);
} }
/**
* Layout all components of this dialog.
*/
private void layoutComponents() { private void layoutComponents() {
var c = new GridBagConstraints(); var c = new GridBagConstraints();
c.gridx = 0; c.gridx = 0;
c.gridy = 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.gridwidth = 1;
c.fill = GridBagConstraints.NONE; c.fill = GridBagConstraints.NONE;
c.anchor = GridBagConstraints.LAST_LINE_END; c.anchor = GridBagConstraints.LAST_LINE_END;
c.weightx = 0; c.weightx = 0;
c.insets = new Insets(6,6,6,6); dialog.getContentPane().add(new JLabel("BLZ", SwingConstants.RIGHT), c);
dialog.getContentPane().add(new JLabel(translate("BLZ"), SwingConstants.RIGHT), c);
c.gridx = 1; c.gridx = 1;
c.gridy = 0; c.gridy = 1;
c.fill = GridBagConstraints.HORIZONTAL; c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.LAST_LINE_END; c.anchor = GridBagConstraints.LAST_LINE_END;
c.weightx = 0.5; c.weightx = 0.5;
dialog.getContentPane().add(blz, c); dialog.getContentPane().add(blz, c);
c.gridx = 2; c.gridx = 2;
c.gridy = 0; c.gridy = 1;
c.fill = GridBagConstraints.NONE; c.fill = GridBagConstraints.NONE;
c.anchor = GridBagConstraints.LAST_LINE_END; c.anchor = GridBagConstraints.LAST_LINE_END;
c.weightx = 0; c.weightx = 0;
dialog.getContentPane().add(new JLabel(translate("IBAN"), SwingConstants.RIGHT), c); dialog.getContentPane().add(new JLabel("IBAN", SwingConstants.RIGHT), c);
c.gridx = 3; c.gridx = 3;
c.gridy = 0; c.gridy = 1;
c.fill = GridBagConstraints.HORIZONTAL; c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.LAST_LINE_END; c.anchor = GridBagConstraints.LAST_LINE_END;
c.weightx = 1; c.weightx = 1;
dialog.getContentPane().add(iban, c); dialog.getContentPane().add(iban, c);
c.gridx = 0; c.gridx = 0;
c.gridy = 1; c.gridy = 2;
c.fill = GridBagConstraints.NONE; c.fill = GridBagConstraints.NONE;
c.anchor = GridBagConstraints.LAST_LINE_END; c.anchor = GridBagConstraints.LAST_LINE_END;
c.weightx = 0; c.weightx = 0;
dialog.getContentPane().add(new JLabel(translate("Betrag"), SwingConstants.RIGHT), c); dialog.getContentPane().add(new JLabel("Betrag", SwingConstants.RIGHT), c);
c.gridx = 1; c.gridx = 1;
c.gridy = 1; c.gridy = 2;
c.fill = GridBagConstraints.HORIZONTAL; c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.LAST_LINE_END; c.anchor = GridBagConstraints.LAST_LINE_END;
c.weightx = 0.5; c.weightx = 0.5;
dialog.getContentPane().add(value, c); dialog.getContentPane().add(value, c);
c.gridx = 0;
c.gridy = 2;
c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.LAST_LINE_END;
c.weightx = 0.5;
dialog.getContentPane().add(new JLabel(translate("Value to transfer:"), JLabel.RIGHT), c);
c.gridx = 1;
c.gridy = 2;
c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.LAST_LINE_END;
c.weightx = 0.5;
dialog.getContentPane().add(enteredValue, c);
c.gridx = 0;
c.gridy = 3;
c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.LAST_LINE_END;
c.weightx = 0.5;
dialog.getContentPane().add(new JLabel(translate("Balance after transfer:"), JLabel.RIGHT), c);
c.gridx = 1;
c.gridy = 3;
c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.LAST_LINE_END;
c.weightx = 0.5;
dialog.getContentPane().add(balanceAfterTransfer, c);
var buttonPanel = new JPanel(new FlowLayout(FlowLayout.TRAILING)); var buttonPanel = new JPanel(new FlowLayout(FlowLayout.TRAILING));
buttonPanel.add(cancel); buttonPanel.add(cancel);
buttonPanel.add(transfer); buttonPanel.add(transfer);
c.gridx = 0; c.gridx = 0;
c.gridy = 4; c.gridy = 3;
c.gridwidth = 4; c.gridwidth = 4;
c.fill = GridBagConstraints.HORIZONTAL; c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.LAST_LINE_END; c.anchor = GridBagConstraints.LAST_LINE_END;
c.insets = new Insets(10, 10, 10, 10);
dialog.getContentPane().add(buttonPanel, c); dialog.getContentPane().add(buttonPanel, c);
} }
private void createComponents(double maxValue) { private void createComponents() {
this.dialog = new JDialog(); this.dialog = new JDialog();
this.cancel = new JButton(translate("Cancel")); this.cancel = new JButton("Cancel");
this.transfer = new JButton(translate("Transfer")); this.transfer = new JButton("Transfer");
this.transfer.setEnabled(false); this.value = new JFormattedTextField(StringDecoder.LOCAL_NUMBER_FORMAT);
this.value = new JFormattedTextField(StringDecoder.getNumberFormatter(maxValue));
this.iban = new JFormattedTextField(); this.iban = new JFormattedTextField();
this.blz = 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())); this.dialog.setContentPane(new JPanel(new GridBagLayout()));
} }
/** public double getAmount() {
* Returns the entered amount parsed into a double value. if (value.getText().isBlank()) {
* @return the amount parsed into a double showMessageDialog(null, "invalid amount", "currency must not be blank", ERROR_MESSAGE);
* @throws InvalidInputException if the text in {@link #value} is not a valid double value. return 0;
*/ }
public double getAmount() throws InvalidInputException {
if (value.getText().isBlank())
throw new InvalidInputException("currency value is blank or has been invalid whilst entered");
try { try {
return StringDecoder.getNumberFormat().parse(value.getText()).doubleValue(); return NumberFormat.getNumberInstance().parse(value.getText()).doubleValue();
} catch (ParseException e) { } catch (ParseException e) {
Logging.LOGGER.severe("Amount text field contains invalid value: " + value); throw new RuntimeException(e);
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() { public JButton getCancel() {
return cancel; return cancel;
} }
@ -212,14 +145,6 @@ public class TransferView {
return blz.getText(); return blz.getText();
} }
public JFormattedTextField getIbanTextField() {
return iban;
}
public JFormattedTextField getBlzTextField() {
return blz;
}
public void dispose() { public void dispose() {
this.dialog.dispose(); this.dialog.dispose();
} }

View File

@ -1,60 +0,0 @@
package me.teridax.jcash.lang;
import me.teridax.jcash.Logging;
import java.util.Locale;
/**
* Class for storing static information about the locale used by the application instance at runtime.
*/
@SuppressWarnings("unused")
public class Locales {
/**
* Default locale initialized to the fallback locale used by this application.
*/
private static Locale defaultLocale = new Locale("en", "EN");
/**
* Sets the default locale to use for the application instance.
* This will instruct the translator to use the default locale as well as
* Java swing components.
*
* @param language the locale to use for language
* @param country the locale to use for country
*/
public static void setDefaultLocale(String language, String country) {
var locale = language + "_" + country;
if (Translator.setTranslationLocale(locale)) {
defaultLocale = new Locale(language, country);
// apply locale to JVM
Locale.setDefault(defaultLocale);
}
Logging.LOGGER.info("Using locale: " + locale);
}
public static Locale getDefaultLocale() {
return defaultLocale;
}
/**
* Tries to automatically detect the default locale.
* This will prefer the users locale over the systems locale.
* If both fail, the JVMs default locale will be used.
*/
public static void autodetectDefaultLocale() {
var country = System.getProperty("user.country");
var language = System.getProperty("user.language");
var jvmLocale = Locale.getDefault();
if (null == country)
country = jvmLocale.getCountry();
if (null == language)
language = jvmLocale.getLanguage();
setDefaultLocale(language, country);
}
}

View File

@ -1,130 +0,0 @@
package me.teridax.jcash.lang;
import me.teridax.jcash.Logging;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
/**
* Static translator class.
* This is a very simple translator able to translate base tokens from english into a variety of
* configured languages.
*/
public final class Translator {
/**
* List of all supported locales in the format: language_country. Examples: en_EN, fr_FR
* The index inside the list is directly related to the index of the translation array inside the list translations.
*/
private static final List<String> languages = new ArrayList<>();
/**
* Mapping of a default english phrase of the code en_EN which is associated with a list of possible translations.
* Index 0 of the translation is equivalent to the key itself since locale 0 is always en_EN.
*/
private static final Map<String, String[]> translations = new HashMap<>();
/**
* Precomputed index of the locale used to statically translate a phrase
*/
private static int localeTranslationIndex = 0;
static {
// read language file and parse
try (var stream = Objects.requireNonNull(Translator.class.getResourceAsStream("languages.csv"))) {
var text = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
// parse each line
text.lines().forEach(line -> {
// read header i.e. the locales identifier
if (languages.isEmpty()) {
// split string into columns by comma and trim each column
languages.addAll(Arrays.stream(line.split(",")).map(String::trim).collect(Collectors.toList()));
// check if default locale is present
if (!languages.contains("en_EN")) {
Logging.LOGGER.severe("Missing default en_EN locale");
throw new IllegalArgumentException("Missing en_EN locale");
}
Logging.LOGGER.info("Read locales: " + Arrays.deepToString(languages.toArray()));
return;
}
var translation = Arrays.stream(line.split(",")).map(String::trim).toArray(String[]::new);
// check if all translations are present
// it may happen at a locale does not provide a translation
if (translation.length != languages.size())
Logging.LOGGER.warning("invalid translations: " + translation.length + " " + languages.size());
translations.put(translation[0], translation);
});
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Translates the given phrase into the corresponding phrase of the selected locale of the translator.
* If no translation is found or no locale is defined for the translator this function will return the given phrase.
*
* @param phrase the text to translate
* @return the translated phrase, or the phrase itself in case no translation can be found
*/
public static String translate(String phrase) {
try {
return translations.get(phrase)[localeTranslationIndex];
} catch (ArrayIndexOutOfBoundsException e) {
Logging.LOGGER.severe("Locale does not exist with index: " + localeTranslationIndex);
} catch (NullPointerException e) {
Logging.LOGGER.severe("No translation found for phrase: " + phrase);
}
return phrase;
}
/**
* Returns an array of all available locales for this translator
*
* @return an array of all available locales for this translator
*/
public static String[] availableLocales() {
return languages.toArray(String[]::new);
}
/**
* Map the given locale string to an index indicating which array location to choose when fetching a result from
* the translation map.
*
* @param locale the locale string
* @return a matching index of the locale
* @throws IllegalArgumentException if the given locale is not part of the available locales
*/
private static int mapLocaleToIndex(String locale) throws IllegalArgumentException {
for (int i = 0; i < languages.size(); i++) {
if (languages.get(i).equals(locale)) {
return i;
}
}
throw new IllegalArgumentException("Locale does not exist: " + locale);
}
/**
* Sets the default locale to use when translating.
* The locale must have the format language_COUNTRY like en_EN
*
* @param locale the locale to use when translating
* @return if the specified locale can be used by the translator
*/
public static boolean setTranslationLocale(String locale) {
try {
localeTranslationIndex = Translator.mapLocaleToIndex(locale);
return true;
} catch (IllegalArgumentException ex) {
Logging.LOGGER.severe("unable to set locale for translation: " + locale + " because: " + ex.getMessage());
return false;
}
}
}

View File

@ -1,41 +0,0 @@
package me.teridax.jcash.lang;
import org.junit.Test;
import java.util.Arrays;
import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertTrue;
public class TranslatorTest {
@Test
public void testLoading() {
// test if all locales are read
assertTrue(Arrays.deepEquals(Translator.availableLocales(), new String[]{"en_EN", "de_DE", "es_ES", "fr_FR", "zh_Hans"}));
}
@Test
public void testTranslationBasic() {
// test if basic translation works
Translator.setTranslationLocale("de_DE");
assertEquals(Translator.translate("Overdraft"), "Überziehungsbetrag");
assertEquals(Translator.translate("Load database"), "Datenbank auswählen");
assertEquals(Translator.translate("currency must not be blank"), "Betrag darf nicht leer sein");
assertEquals(Translator.translate("Deposit money"), "Geld einzahlen");
Translator.setTranslationLocale("es_ES");
assertEquals(Translator.translate("Account"), "Cuenta");
assertEquals(Translator.translate("Faulty login attempt"), "Solicitud de autenticación no válida");
assertEquals(Translator.translate("Transfer money"), "Transferencia de dinero");
// test if translation with no available translation works as expected
assertEquals(Translator.translate("Guppi guppi guppi"), "Guppi guppi guppi");
}
@Test
public void testInvalidLocales() {
// test if app crashes when invalid locale is loaded
Translator.setTranslationLocale("ar_TD");
}
}

View File

@ -1,54 +0,0 @@
en_EN,de_DE,es_ES,fr_FR,zh_Hans
Bank,Bank,Banco,Banque,银行
BLZ,BLZ,BLZ,BLZ,BLZ
PIN,PIN,PIN,PIN,密码
Balance,Kontostand,Saldo,Solde,余额
Account type,Kontoart,Tipo de cuenta,Type de compte,账户类型
Interest,Zinsen,Interés,Intérêts,利息
Overdraft,Überziehungskredit,Descubierto,Découvert,透支
Customer number,Kundennummer,Número de cliente,Numéro de client,客户编号
Name,Name,Nombre,Nom du client,姓名
Name,Name,Nombre,Nom et prénom,姓名
Street,Straße,Calle,Rue,街道
PLZ,PLZ,PLZ,PLZ,PLZ
City,Ort,Ciudad,Ville,城市
Password,Kennwort,Contraseña,Mot de passe,密码
Login,Anmeldung,Inicio de sesión,Connexion,登录
CurrentAccount,Girokonto,Cuenta corriente,Compte courant,活期账户
SavingsAccount,Sparkonto,CuentaAhorro,Compte d'épargne,储蓄账户
Address,Adresse,Dirección,Adresse,地址
Logout,Abmelden,Cerrar sesión,Déconnexion,注销
Transfer,Überweisung,Transferir,Virement,转账
Deposit,Einzahlen,Ingresar,Dépôt,存款
Take off,Abheben,Retirar,Enlever,取款
Value,Wert,Valor,Valeur,价值
Cancel,Abbrechen,Cancelar,Annuler,取消
Load database,Datenbank laden,Cargar base de datos,Charger la base de données,加载数据库
Invalid account,Ungültiges Konto,Cuenta no válida,Compte non valide,无效账户
Could not transfer,Konnte nicht übertragen werden,No se ha podido transferir,Impossible de transférer,无法转账
Transfer,Überweisung,Transferencia,Transférer,转账
invalid amount,Ungültiger Betrag,Importe no válido,montant non valide,无效金额
currency must not be blank,Währung darf nicht leer sein,La divisa no debe estar en blanco,la devise ne doit pas être vide,货币不得为空
Transfer money,Geld überweisen,Transferencia de dinero,Transférer de l'argent,转账金额
Could not take off money,Konnte Geld nicht abheben,No se ha podido retirar el dinero,Impossible de retirer de l'argent,无法取款
Takeoff money,Geld abheben,Retirar dinero,Retirer de l'argent,取款
Takeoff,Abheben,Despegue,Décoller,起飞
Currency must not be blank,Die Währung darf nicht leer sein,La divisa no debe estar en blanco,La monnaie ne doit pas être en blanc,货币不得为空
Cashmachine,Geldautomat,Cajero,Cashmachine,提款机
Invalid IBAN,Ungültige IBAN,IBAN no válido,IBAN non valide,IBAN 无效
Faulty login attempt,Fehlerhafter Anmeldeversuch,Intento de acceso erróneo,Tentative de connexion erronée,尝试登录失败
Invalid PIN,Ungültige PIN,PIN no válido,PIN invalide,密码无效
Invalid login credentials,Ungültige Anmeldedaten,Credenciales de inicio de sesión no válidas,Identifiants de connexion invalides,登录凭证无效
Deposit money,Geld einzahlen,Depositar dinero,Dépôt d'argent,存款
Interest rate,Zinssatz,Tipo de interés,Taux d'intérêt,利率
Name/Family-name,Name/Familienname,Nombre y apellidos,Nom/Prénom de famille,姓名
Address,Anschrift,Dirección,Adresse,地址
Account,Konto,Cuenta,Compte,账户
Closing JCash,JCash wird geschlossen,Cerrar JCash,Clôture de JCash,关闭 JCash
you're logged out,Sie sind abgemeldet,has cerrado sesión,Vous êtes déconnecté,您已退出登录
Comma separated value spreadsheet,Kommagetrennte Werte-Tabelle,Hoja de cálculo de valores separados por comas,Tableur de valeurs séparées par des virgules,逗号分隔值电子表格
Value to transfer:,Zu übertragender Wert:,Valor a transferir:,Valeur à transférer :,要转账的金额:
Balance after transfer:,Kontostand nach Überweisung:,Saldo después de la transferencia:,Solde après transfert :,转账后的余额
Balance after takeoff:,Kontostand nach dem Abheben:,Saldo después de retirar:,Solde après décollage :,取出后的余额
Value to deposit:,Einzuzahlender Wert:,Valor a ingresar:,Valeur à déposer :,存款价值
Value to takeoff:,Auszuhalender Wert:,Valor a despegar:,Valeur à l'enlèvement :,起飞价值
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 : 起飞价值