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