Compare commits

...

18 Commits

41 changed files with 1535 additions and 576 deletions

View File

@ -4,6 +4,8 @@
Draft program for the Java class of semester 2. The goal was to simulate basic cash machine that can read customer data from a `.csv` file and let the user view the data with crude forms of authentication.
> This project was graded with 100,0 out of 100,0 points
## Overview
The program can read `.csv` file from disk and allows the user login to an account specified.

View File

@ -7,6 +7,8 @@ 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
*/
@ -21,6 +23,10 @@ public final class Logging {
* 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.
@ -35,21 +41,36 @@ public final class Logging {
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) {
var ch = new ConsoleHandler();
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) {
var now = LocalDateTime.now();
var dateTime = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss").format(now);
var logFileName = LOG_FOLDER_NAME + dateTime + ".log";
// 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 {
var fh = new FileHandler(logFileName);
FileHandler fh = new FileHandler(logFileName);
fh.setLevel(level);
LOGGER.addHandler(fh);
} catch (Exception e) {
@ -57,8 +78,12 @@ public final class Logging {
}
}
/**
* 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() {
var folderPath = Path.of(LOG_FOLDER_NAME);
Path folderPath = of(LOG_FOLDER_NAME);
if (Files.isDirectory(folderPath))
return;

View File

@ -1,19 +1,19 @@
package me.teridax.jcash;
import me.teridax.jcash.gui.IconProvider;
import me.teridax.jcash.gui.Loader;
import me.teridax.jcash.gui.account.AccountController;
import me.teridax.jcash.gui.login.LoginController;
import me.teridax.jcash.gui.MainFrame;
import me.teridax.jcash.gui.Utils;
import me.teridax.jcash.lang.Locales;
import javax.swing.*;
import java.awt.*;
import java.io.IOException;
import java.util.Objects;
import java.util.logging.*;
import java.util.logging.Level;
import static javax.swing.JOptionPane.ERROR_MESSAGE;
import static javax.swing.JOptionPane.showMessageDialog;
import static me.teridax.jcash.Logging.LOGGER;
import static me.teridax.jcash.Logging.initializeSystemLogger;
import static me.teridax.jcash.lang.Translator.translate;
public final class Main {
@ -23,30 +23,97 @@ public final class Main {
private static Main instance;
/**
* Primary window of this program
* Primary class for controlling GUI of this application
*/
private final JFrame window;
private final MainFrame window;
private Main() {
// create main window and set defaults
this.window = new JFrame();
this.window.setTitle(translate("Cashmachine"));
this.window.setLocationByPlatform(true);
this.window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.window = new MainFrame();
}
/**
* Prompts the user a dialog to select a file to load the database from.
* If a valid database has been read a login screen will be shown.
* If no file was selected or the database was invalid the application will close.
*/
public void loadDatabase() {
try {
var bms = Loader.load();
this.window.setBms(bms);
} catch (Exception e) {
LOGGER.severe("Failed to load database: " + e.getMessage());
Utils.error("Failed to load database");
System.exit(1);
}
}
public static void main(String[] args) {
initializeSystemLogger(Level.FINE);
setPlatformDependingTheme();
loadExtraFont();
Locales.autodetectDefaultLocale();
// create main instance and show the login screen
instance();
getInstance().showLoginScreen();
getInstance().loadDatabase();
}
/**
* Loads the extra font file used on the login button
*/
private static void loadExtraFont() {
try {
var font = Font.createFont(Font.TRUETYPE_FONT, Objects.requireNonNull(IconProvider.class.getResourceAsStream("res/Circus.ttf")));
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
ge.registerFont(font);
} catch (IOException | FontFormatException | NullPointerException e) {
LOGGER.warning("Could not load font file: " + e.getMessage());
}
}
/**
* Set the look and feel via the ui manager.
* This function will select a look and feel so that the application will
* look most like a native application.
* It will select the systems look and feel for Windows and MacOS
* and GTK for unix based systems.
*/
private static void setPlatformDependingTheme() {
// default look and feel
var laf = UIManager.getCrossPlatformLookAndFeelClassName();
// runtime os
String os = System.getProperty("os.name").toLowerCase();
LOGGER.fine("Detected operating system: " + os);
// set look and feel class name depending on the platform we run on
if (os.contains("win") || os.contains("mac")) {
LOGGER.info("Detected Windows or MacOS based operating system, using system look and feel");
laf = UIManager.getSystemLookAndFeelClassName();
} else if (os.contains("nix") || os.contains("nux") || os.contains("aix") || os.contains("sunos")) {
LOGGER.info("Detected Unix/Linux based operating system, using GTK look and feel");
laf = "com.sun.java.swing.plaf.gtk.GTKLookAndFeel";
} else {
LOGGER.warning("Unable to detect operating system: " + os);
}
try {
LOGGER.info("Setting look and feel to class: " + laf);
UIManager.setLookAndFeel(laf);
} catch (ClassNotFoundException | UnsupportedLookAndFeelException e) {
LOGGER.severe("Look and feel class not found: " + e.getMessage());
} catch (InstantiationException | IllegalAccessException e) {
LOGGER.warning("Could not set look and feel: " + e.getMessage());
}
}
/**
* Get the main singleton instance of this program
*
* @return the singleton instance of this class
*/
public static Main getInstance() {
@ -56,9 +123,8 @@ public final class Main {
/**
* Attempts to create a new instance of the singleton class Main.
* This method throws an exception if the class is already instantiated.
*
* @throws IllegalStateException if the class is already instantiated
* @pre the static instance of this class should be null.
* @post the static instance of this class will be set
*/
public static void instance() {
if (null != instance)
@ -69,49 +135,16 @@ public final class Main {
Main.instance = new Main();
}
/**
* Shows the open dialog for selecting a database file. After selection, it then proceeds to prompt login.
* Afterward the selected account can be managed.
* This method is non-blocking and all work described is performed asynchronously on the AWT Event dispatcher.
*/
public void showLoginScreen() {
SwingUtilities.invokeLater(() -> {
LOGGER.finer("showing login screen");
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 -> {
LOGGER.finer("account selected: " + Objects.toString(account, "null"));
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) {
LOGGER.fine("Unable to show login mask: " + e.getMessage());
showMessageDialog(null, e.getMessage(), translate("Closing JCash"), ERROR_MESSAGE);
System.exit(0);
}
});
public JFrame getWindow() {
return this.window.getWindow();
}
/**
* Logs the user out of the database, hiding the main window.
* Logs the user out of the currently open account.
* This will show the login mask and clear the password field or the previous
* login attempt.
*/
public void logout() {
window.setContentPane(new JLabel(translate("you're logged out")));
window.setVisible(false);
this.window.logout();
}
}

View File

@ -4,6 +4,7 @@ import me.teridax.jcash.Logging;
import me.teridax.jcash.banking.accounts.Account;
import me.teridax.jcash.banking.accounts.Owner;
import me.teridax.jcash.banking.management.Profile;
import me.teridax.jcash.gui.Utils;
import java.util.*;
import java.util.regex.Pattern;
@ -34,6 +35,23 @@ public final class Bank {
this.accounts = new HashMap<>();
}
/**
* Validates the given BLZ. If the blz is not valid an exception is thrown.
*
* @param maybeBlz a string to be tested for being a blz
* @return returns the correctly formatted blz
* @throws IllegalArgumentException if the supplied argument is not a valid blz
*/
public static String validateBlz(String maybeBlz) throws IllegalArgumentException {
var pattern = Pattern.compile("[\\w-_]+");
var matcher = pattern.matcher(maybeBlz);
if (matcher.find())
return matcher.group();
throw new IllegalArgumentException("not a valid BLZ: " + maybeBlz);
}
public String getBlz() {
return blz;
}
@ -46,6 +64,7 @@ public final class Bank {
* Add a new account and its associated owner to this bank.
* If the owner is already present the account is added to the existing owner.
* If the account is already present for the owner the old account gets overwritten.
*
* @param owner the owner of the account
* @param account the account of the owner
*/
@ -70,6 +89,7 @@ public final class Bank {
/**
* Retrieve all accounts owned by the specific owner.
*
* @param owner the owner for which accounts are to be retrieved
* @return all accounts owned by the owner and managed by this bank
*/
@ -77,24 +97,9 @@ public final class Bank {
return accounts.get(owner).toArray(Account[]::new);
}
/**
* Validates the given BLZ. If the blz is not valid an exception is thrown.
* @param maybeBlz a string to be tested for being a blz
* @return returns the correctly formatted blz
* @throws IllegalArgumentException if the supplied argument is not a valid blz
*/
public static String validateBlz(String maybeBlz) throws IllegalArgumentException {
var pattern = Pattern.compile("[\\w-_]+");
var matcher = pattern.matcher(maybeBlz);
if (matcher.find())
return matcher.group();
throw new IllegalArgumentException("not a valid BLZ: " + maybeBlz);
}
/**
* Return a profile of the specified international bank account number.
*
* @param iban the number to create a profile for
* @return the profile for the iban account
*/

View File

@ -5,6 +5,8 @@ import me.teridax.jcash.decode.StringDecoder;
import java.util.Objects;
import static me.teridax.jcash.lang.Translator.translate;
/**
* Base class for bank accounts.
* Stores the iban, pin and balance.
@ -34,6 +36,7 @@ public abstract class Account {
/**
* Parses a row of a fixed amount of columns into an account.
* This function will attempt to create an instance of two classes which inherit from Account.
*
* @param columns array of 6 strings
* @return either an instance of {@link SavingsAccount} or an instance of {@link CurrentAccount}
* @throws IllegalArgumentException if the account type cannot be determined or the provided data is invalid
@ -43,8 +46,6 @@ public abstract class Account {
Objects.requireNonNull(columns);
Logging.LOGGER.finer("Parsing account from columns");
Logging.LOGGER.finer("Decoding account fields");
// deserialize fields
var iban = StringDecoder.decodeUniqueIdentificationNumber(columns[0]);
var pin = StringDecoder.decodeUniqueIdentificationNumber(columns[1]);
@ -92,6 +93,7 @@ public abstract class Account {
/**
* Returns true if the parameter is an instance of class {@link Account} and both their
* ibans are equal.
*
* @param obj the obj to compare to
* @return true if the parameter is an instance of class {@link Account} and their ibans match
*/
@ -107,14 +109,16 @@ public abstract class Account {
* Returns a description of the account in form a string.
* This method is not equal to {@link #toString()} but is intended to be
* a method used to retrieve a formatted representation for guis.
*
* @return a basic description of the account in form a string
*/
public String getDescription() {
return String.format("%s (%s)", iban, getClass().getSimpleName());
return String.format("%s (%s)", iban, translate(getClass().getSimpleName()));
}
/**
* Add a non-negative value onto the balance of this account.
*
* @param amount the amount of value to add
* @throws IllegalArgumentException if amount is negative
*/
@ -135,6 +139,7 @@ public abstract class Account {
/**
* Takeoff a certain amount of money from this accounts balance.
* Saturates the result if the value to subtract is greater than the balance present.
*
* @param amount the amount of money to subtract from the accounts balance
* @throws IllegalArgumentException if amount is greater than the balance present
*/

View File

@ -25,6 +25,7 @@ public final class CurrentAccount extends Account {
/**
* Takeoff a certain amount of money from this accounts balance.
* Saturates the result if the value to subtract is greater than the balance present.
*
* @param amount the amount of money to subtract from the accounts balance
* @throws IllegalArgumentException if amount is smaller than 0 or the overflow is greater than the overdraft.
*/

View File

@ -34,6 +34,7 @@ public final class Owner {
/**
* Create a new instance of this class parsed from the columns.
*
* @param columns the fields of this class as strings
* @return an instance of this class
* @throws IllegalArgumentException if the supplied columns is invalid

View File

@ -6,6 +6,8 @@ import me.teridax.jcash.banking.accounts.Owner;
import me.teridax.jcash.decode.StringDecoder;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
@ -23,6 +25,13 @@ 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
@ -36,6 +45,7 @@ public final class BankingManagementSystem {
/**
* Utility method for retrieving the tail of a string array.
* This method return all items which follow the n-th index denoted by index.
*
* @param array the array to take the tail of
* @param index the amount trailing indices to skip
* @return an array containing the last elements of the supplied array
@ -66,15 +76,16 @@ public final class BankingManagementSystem {
* </ol>
* The file can contain a header line which gets skipped when reading.
* Note that the first line is always skipped and the name of every column is not checked in any way.
*
* @param file the file to parse
* @throws IllegalArgumentException if the file cannot be read or the containing data is invalid
* @return a valid BMS
* @throws IllegalArgumentException if the file cannot be read or the containing data is invalid
*/
public static BankingManagementSystem loadFromCsv(Path file) throws IllegalArgumentException {
LOGGER.fine("parsing banking management system from file: " + Objects.toString(file, "null"));
try {
var bms = new BankingManagementSystem();
var content = Files.readString(file);
var content = getSource(file);
// read line by line
// and skip the first line
@ -112,17 +123,36 @@ public final class BankingManagementSystem {
return bms;
} catch (IOException e) {
LOGGER.severe("Could not read file: " + file + " due to: " + e.getMessage());
throw new IllegalArgumentException("Could not read file " + file, e);
} catch (IllegalArgumentException | NullPointerException e) {
LOGGER.severe("Could not parse file: " + file + " due to: " + e.getMessage());
throw new IllegalArgumentException("Could not parse file " + file, e);
}
}
/**
* Attempts to read the entire file into a string.
* This method tires out all encodings in {@link #ENCODINGS}
* @param file the file to read
* @throws IllegalArgumentException if the file cannot be read
* @return the content of the file
*/
private static String getSource(Path file) throws IllegalArgumentException {
Exception lastException = null;
for (var encoding : ENCODINGS) {
try {
return Files.readString(file, encoding);
} catch (IOException e) {
LOGGER.severe("Could not read file: " + file + " due to: " + e.getMessage());
lastException = e;
}
}
assert lastException != null;
throw new IllegalArgumentException("Invalid encoding, or IO exception: " + lastException.getMessage());
}
/**
* Return a bank with the given blz.
*
* @param blz the blz to search bank of
* @return the bank with this blz or none
*/

View File

@ -23,14 +23,14 @@ public class Profile {
* The bank that manages every account referenced by this profile
*/
private final Bank bank;
/**
* Primary or currently selected account.
*/
private Account primaryAccount;
/**
* All other account registered at a specific bank for the specified owner
*/
private final Account[] accounts;
/**
* Primary or currently selected account.
*/
private Account primaryAccount;
public Profile(Owner owner, Bank bank, Account account, Account[] accounts) {
if (!Arrays.asList(accounts).contains(account)) {
@ -48,21 +48,10 @@ public class Profile {
return primaryAccount;
}
public Owner getOwner() {
return owner;
}
public Bank getBank() {
return bank;
}
public Account[] getAccounts() {
return accounts;
}
/**
* Set the primary account of this profile based on a descriptive text.
* This method may not change anything if no account can be found with a matching description
*
* @param description the description to find a matching account for
*/
public void setPrimaryAccount(String description) {
@ -74,4 +63,16 @@ public class Profile {
}
Logging.LOGGER.warning("Account " + description + " not found in associated account list");
}
public Owner getOwner() {
return owner;
}
public Bank getBank() {
return bank;
}
public Account[] getAccounts() {
return accounts;
}
}

View File

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

View File

@ -1,33 +1,57 @@
package me.teridax.jcash.decode;
import org.junit.Test;
import me.teridax.jcash.lang.Locales;
import javax.swing.text.NumberFormatter;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Locale;
import java.util.Objects;
import java.util.regex.Pattern;
import static junit.framework.TestCase.assertEquals;
/**
* Utility class for converting various single line strings into a specific data type according
* to a format mostly dictated by a locale.
*/
public class StringDecoder {
public static NumberFormat getNumberFormat() {
return NumberFormat.getInstance(Locales.getDefaultLocale());
}
/**
* Locale to use when converting strings
* Returns a NumberFormatter for parsing double values in the appropriate locale.
* @return the number formatter
*/
private static final Locale LOCALE = Locale.GERMANY;
public static NumberFormatter getNumberFormatter(double maxValue) {
var formatter = new NumberFormatter();
formatter.setValueClass(Double.class);
formatter.setMinimum(0d);
formatter.setMaximum(maxValue);
formatter.setAllowsInvalid(true);
formatter.setCommitsOnValidEdit(true);
return formatter;
}
/**
* NumberFormat to use when converting strings
* Returns a NumberFormatter for parsing integer values in the appropriate locale.
* @return the number formatter
*/
public static final NumberFormat LOCAL_NUMBER_FORMAT = NumberFormat.getInstance(LOCALE);
public static NumberFormatter getIntegerNumberFormatter() {
var formatter = new NumberFormatter();
formatter.setValueClass(Integer.class);
formatter.setMinimum(0d);
formatter.setAllowsInvalid(true);
formatter.setCommitsOnValidEdit(true);
return formatter;
}
/**
* Attempts to convert the given string into a double value representing a percentage.
* The percentage is stored in the range [0,1] and can linearly be mapped to [0, 100] by multiplying with 100.
* The output value will be in the range [0, 100]. Strings formatted without a percentage
* symbol will be assumed to be normalized and thus multiplied by 100 to retrieve the result in percent.
*
* @param number the string to convert
* @return the double value
* @throws IllegalArgumentException when the format is invalid
@ -36,19 +60,22 @@ public class StringDecoder {
public static double decodePercent(String number) throws IllegalArgumentException, NullPointerException {
Objects.requireNonNull(number);
// trim the number and cut out optional percent symbols
var trimmed = number.trim();
// trim and cut out weird leading single quotes for numbers
var prepared = number.trim().replaceAll("^\\s*['`](?=\\d)", "");
var pattern = Pattern.compile("^([^%]+)?(%)?$", Pattern.CASE_INSENSITIVE);
var matcher = pattern.matcher(trimmed);
var matcher = pattern.matcher(prepared);
if (matcher.find()) {
// if no percentage symbol is given the number will be multiplied by 100%
var scale = 1e2;
// check if capture group 2 captured a percentage symbol
// if to we don't want to apply any scaling to the value
if (null != matcher.group(2))
scale = 1;
try {
return LOCAL_NUMBER_FORMAT.parse(matcher.group(1)).doubleValue() * scale;
return getNumberFormat().parse(matcher.group(1)).doubleValue() * scale;
} catch (ParseException ex) {
throw new IllegalArgumentException("Not a valid number: " + number, ex);
}
@ -59,6 +86,7 @@ public class StringDecoder {
/**
* Attempts to convert the given string into a currency value.
*
* @param currency the string to convert
* @return the double value
* @throws IllegalArgumentException when the format is invalid
@ -68,10 +96,10 @@ public class StringDecoder {
Objects.requireNonNull(currency);
// trim and cut out weird leading single quotes for numbers
var preparedUID = currency.trim().replaceAll("^\\s*['`](?=\\d)", "");
var prepared = currency.trim().replaceAll("^\\s*['`](?=\\d)", "");
try {
return LOCAL_NUMBER_FORMAT.parse(preparedUID).doubleValue();
return getNumberFormat().parse(prepared).doubleValue();
} catch (ParseException ex) {
throw new IllegalArgumentException("Not a valid currency in german locale: " + currency, ex);
}
@ -80,6 +108,7 @@ public class StringDecoder {
/**
* Attempts to convert the given string into universally unique number.
* This function does not check for duplicates. The number must be a positive integer.
*
* @param number the string to convert
* @return the integer serial number
* @throws IllegalArgumentException when the format is invalid
@ -93,9 +122,7 @@ public class StringDecoder {
// check if the string is a valid unsigned number
try {
LOCAL_NUMBER_FORMAT.setParseIntegerOnly(true);
var serialNumber = LOCAL_NUMBER_FORMAT.parse(preparedUID);
LOCAL_NUMBER_FORMAT.setParseIntegerOnly(false);
var serialNumber = getNumberFormat().parse(preparedUID);
if (serialNumber.intValue() < 0)
throw new IllegalArgumentException("Not a valid unique identification number: " + number);
@ -109,6 +136,7 @@ public class StringDecoder {
/**
* Attempts to convert the given string into a name.
* This method performs validation and trimming.
*
* @param name the string to convert
* @return the qualified name
* @throws IllegalArgumentException when the format is invalid
@ -119,7 +147,7 @@ public class StringDecoder {
var trimmed = name.trim();
var pattern = Pattern.compile("[^\\s]+", Pattern.CASE_INSENSITIVE);
var pattern = Pattern.compile("[^\\d]+", Pattern.CASE_INSENSITIVE);
var matcher = pattern.matcher(trimmed);
if (matcher.find()) {
return matcher.group();
@ -130,6 +158,7 @@ public class StringDecoder {
/**
* Attempts to convert the given string into a street and an optional house address.
*
* @param street the string to convert
* @return the address name
* @throws IllegalArgumentException when the format is invalid
@ -146,47 +175,4 @@ public class StringDecoder {
throw new IllegalArgumentException("not a void address");
}
}
@Test
public void testDecodeSuccessfulFunctions() {
assertEquals(decodePercent("1,003"), 100.3d, 1e-3d);
decodeUniqueIdentificationNumber("9578647895");
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.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(decodeCurrency("1,3€"), 1.3d);
assertEquals(decodeCurrency("145,34"), 145,34d);
assertEquals(decodeCurrency("0,45 €"), 0.45d);
}
@Test(expected = IllegalArgumentException.class)
public void testDecodeInvalidFunctions() {
decodeUniqueIdentificationNumber("q0948tvb6q047t 740 t74z0tz 784");
decodeUniqueIdentificationNumber("-39867.8475");
decodeUniqueIdentificationNumber("-398678475");
decodeUniqueIdentificationNumber(" ß9qu908t76q34798t6q734vb9843");
decodeName("John Doe");
decodeName("3490qt67v 0b34");
decodeName("Alexander9");
decodeName("-ga76re78g6$§");
decodeStreet("Bahnhofstraße -40/1");
decodeStreet("Gülleweg 9//567");
decodeStreet("23Echsengaße 67 / 4 Hofwetg 9");
}
}

View File

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

View File

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

View File

@ -1,13 +1,14 @@
package me.teridax.jcash.gui;
import me.teridax.jcash.Logging;
import me.teridax.jcash.Main;
import me.teridax.jcash.banking.management.BankingManagementSystem;
import me.teridax.jcash.lang.Translator;
import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import static javax.swing.JFileChooser.APPROVE_OPTION;
import static me.teridax.jcash.lang.Translator.translate;
/**
* Utility class for loading a BMS configuration from a csv file.
@ -17,13 +18,15 @@ public class Loader {
/**
* Filter that only allows for files with *.csv extension
*/
private static final FileNameExtensionFilter FILE_FILTER = new FileNameExtensionFilter("Comma separated value spreadsheet", "csv", "CSV");
private static final FileNameExtensionFilter FILE_FILTER = new FileNameExtensionFilter(translate("Comma separated value spreadsheet"), "csv", "CSV");
private Loader() {}
private Loader() {
}
/**
* Load a BMS from a csv file. Opens up a dialog which prompts the user to select a single file.
* Once the file is selected this function will try to parse the contents to a BMS and return the instance.
*
* @return a valid BMS instance loaded from a file
* @throws IllegalStateException When either no file is selected or the selected files content is invalid
*/
@ -35,7 +38,7 @@ public class Loader {
fileChooser.setDialogType(JFileChooser.OPEN_DIALOG);
fileChooser.setAcceptAllFileFilterUsed(false);
if (fileChooser.showDialog(null, Translator.translate("Load database")) == APPROVE_OPTION) {
if (fileChooser.showDialog(Main.getInstance().getWindow(), translate("Load database")) == APPROVE_OPTION) {
// parse file content
try {
return BankingManagementSystem.loadFromCsv(fileChooser.getSelectedFile().toPath());

View File

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

View File

@ -0,0 +1,86 @@
package me.teridax.jcash.gui;
import me.teridax.jcash.Main;
import me.teridax.jcash.lang.Locales;
import javax.swing.*;
import java.awt.*;
import java.text.Format;
import java.text.NumberFormat;
public class Utils {
/**
* Formats the string so that it will be displayed as a Heading 1 element by JLabels.
* This embeds the given string into two html tags.
* Note that eny html entities in the string will be formatted as valid HTML entities.
* Meaning they won't show up in as plain text.
* @param title the title to format.
* @return the given string embedded into <pre>&lt;html>&lt;h1>$string&lt;/h1>&lt;/html></pre>
*/
public static String addHeading(String title) {
return String.format("<html><h1>%s</h1></html>", title);
}
/**
* Add a new row of components to the specified target component.
* This will add label to the right side of the next row and the specified component to the left.
*
* @param constraints the constraint to use. Must be non-null
* @param target the base component to add a row to
* @param comp the component to add to the left side
* @param row the row to add the components to
* @param name the labels text to add to the left side
*/
public static void addGridBagRow(GridBagConstraints constraints, JComponent target, JComponent comp, int row, String name) {
constraints.gridwidth = 1;
constraints.gridx = 1;
constraints.gridy = row;
constraints.weightx = 0;
constraints.fill = GridBagConstraints.HORIZONTAL;
target.add(new JLabel(name, SwingConstants.RIGHT), constraints);
if (comp == null)
return;
constraints.gridx = 2;
constraints.gridy = row;
constraints.weightx = 1;
constraints.fill = GridBagConstraints.HORIZONTAL;
target.add(comp, constraints);
}
/**
* Add a new row of components to the specified target component.
* This will add label to the right side of the next row and the specified component to the left.
*
* @param constraints the constraint to use. Must be non-null
* @param target the base component to add a row to
* @param comp the component to add to the left side
* @param row the row to add the components to
* @param right the component to place on the left side
*/
public static void addGridBagRow(GridBagConstraints constraints, JComponent target, JComponent comp, int row, Component right) {
constraints.gridwidth = 1;
constraints.gridx = 1;
constraints.gridy = row;
constraints.weightx = 0;
constraints.fill = GridBagConstraints.HORIZONTAL;
target.add(right, constraints);
constraints.gridx = 2;
constraints.gridy = row;
constraints.weightx = 1;
constraints.fill = GridBagConstraints.HORIZONTAL;
target.add(comp, constraints);
}
/**
* Opens an error message dialog. This function will block the calling thread until the error message
* disposed.
* @param message the message to show to the user
*/
public static void error(String message) {
JOptionPane.showMessageDialog(Main.getInstance().getWindow(), message, "Error occurred", JOptionPane.ERROR_MESSAGE);
}
}

View File

@ -4,9 +4,9 @@ import me.teridax.jcash.Logging;
import me.teridax.jcash.Main;
import me.teridax.jcash.banking.management.BankingManagementSystem;
import me.teridax.jcash.banking.management.Profile;
import me.teridax.jcash.gui.deposit.DepositDialog;
import me.teridax.jcash.gui.takeoff.TakeoffDialog;
import me.teridax.jcash.gui.transfer.TransferDialog;
import me.teridax.jcash.gui.deposit.DepositController;
import me.teridax.jcash.gui.takeoff.TakeoffController;
import me.teridax.jcash.gui.transfer.TransferController;
/**
* Controller for controlling the gui of an account.
@ -18,35 +18,68 @@ public class AccountController {
*/
private final AccountView view;
private final Profile profile;
private Profile profile;
public AccountController(Profile profile, BankingManagementSystem bms) {
this.profile = profile;
public AccountController() {
this.view = new AccountView();
this.view.setProfile(profile);
this.data = new AccountData(bms);
createListeners(profile);
this.data = new AccountData();
}
private void createListeners(Profile profile) {
/**
* Sets the profile and BMS used to manage banking.
* @param profile the profile used to manage the account
* @param bms the BMS used access other banking accounts
*/
public void setProfile(Profile profile, BankingManagementSystem bms) {
this.profile = profile;
this.view.setProfile(profile);
this.data.setBms(bms);
this.createListeners();
}
/**
* Create listeners for GUI components
*/
private void createListeners() {
this.view.getAccountSelection().addActionListener(e -> changeAccount());
this.view.getLogout().addActionListener(e -> logout());
this.view.getDeposit().addActionListener(e -> depositMoney());
this.view.getTakeoff().addActionListener(e -> takeoffMoney());
this.view.getTransfer().addActionListener(e -> transferMoney());
}
this.view.getDeposit().addActionListener(e -> new DepositDialog(profile.getPrimaryAccount(), () -> this.view.setProfile(profile)));
/**
* Open dialog to deposit money
*/
private void depositMoney() {
new DepositController(profile.getPrimaryAccount());
this.view.updateAccountVariables(profile);
}
this.view.getTakeoff().addActionListener(e -> new TakeoffDialog(profile.getPrimaryAccount(), () -> this.view.setProfile(profile)));
/**
* Open dialog to transfer money
*/
private void transferMoney() {
new TransferController(profile.getPrimaryAccount(), data.getBms());
this.view.updateAccountVariables(profile);
}
this.view.getTransfer().addActionListener(e -> new TransferDialog(profile.getPrimaryAccount(), data.getBms(), () -> this.view.setProfile(profile)));
/**
* Open dialog to take off money
*/
private void takeoffMoney() {
new TakeoffController(profile.getPrimaryAccount());
this.view.updateAccountVariables(profile);
}
private void logout() {
Logging.LOGGER.fine("Logging out of account");
Main.getInstance().logout();
Main.getInstance().showLoginScreen();
}
/**
* Change the selected account.
*/
private void changeAccount() {
var description = ((String) this.view.getAccountSelection().getSelectedItem());
Logging.LOGGER.fine("Changing primary account selected: " + description);

View File

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

View File

@ -1,6 +1,7 @@
package me.teridax.jcash.gui.account;
import me.teridax.jcash.Logging;
import me.teridax.jcash.banking.accounts.Account;
import me.teridax.jcash.banking.accounts.CurrentAccount;
import me.teridax.jcash.banking.accounts.SavingsAccount;
import me.teridax.jcash.banking.management.Profile;
@ -8,8 +9,11 @@ import me.teridax.jcash.decode.StringDecoder;
import javax.swing.*;
import java.awt.*;
import java.util.Arrays;
import java.util.Comparator;
import static javax.swing.SwingConstants.RIGHT;
import static me.teridax.jcash.gui.Utils.addGridBagRow;
import static me.teridax.jcash.lang.Translator.translate;
public class AccountView extends JPanel {
@ -36,44 +40,25 @@ public class AccountView extends JPanel {
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) {
Logging.LOGGER.finer("Changing profile of account view");
var bank = profile.getBank();
var account = profile.getPrimaryAccount();
var owner = profile.getOwner();
this.blz.setText(bank.getBlz());
this.bankName.setText(bank.getName());
this.iban.setText(String.valueOf(account.getIban()));
this.name.setText(owner.getName() + " " + owner.getFamilyName());
this.address.setText(owner.getStreet() + " " + owner.getCity());
this.balance.setText(StringDecoder.LOCAL_NUMBER_FORMAT.format(account.getBalance()) + "");
this.type.setText(account.getClass().getSimpleName());
if (account instanceof CurrentAccount) {;
this.typeSpecialLabel.setText(translate("Overdraft"));
this.typeSpecialProperty.setText( StringDecoder.LOCAL_NUMBER_FORMAT.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());
}
this.updateAccountVariables(profile);
this.accountSelection.removeAllItems();
for (var otherAccount : profile.getAccounts()) {
this.accountSelection.addItem(otherAccount.getDescription());
}
this.accountSelection.setSelectedItem(account.getDescription());
var accounts = profile.getAccounts();
Arrays.stream(accounts).sorted(Comparator.comparingInt(Account::getIban)).forEach(a -> this.accountSelection.addItem(a.getDescription()));
this.accountSelection.setSelectedItem(profile.getPrimaryAccount().getDescription());
}
private void createLayout() {
var content = new JPanel(new GridBagLayout());
this.setLayout(new BorderLayout(16, 16));
this.setLayout(new BorderLayout(12, 12));
this.add(new JScrollPane(content), BorderLayout.CENTER);
var constraints = new GridBagConstraints();
@ -85,14 +70,14 @@ public class AccountView extends JPanel {
accountSelectionPanel.add(iban, BorderLayout.CENTER);
accountSelectionPanel.add(accountSelection, BorderLayout.EAST);
addInputRow(constraints, content, accountSelectionPanel, 1, new JLabel(translate("IBAN"), RIGHT));
addInputRow(constraints, content, name, 2, new JLabel(translate("Name/Family-name"), RIGHT));
addInputRow(constraints, content, address, 3, new JLabel(translate("Address"), RIGHT));
addInputRow(constraints, content, bankName, 4, new JLabel(translate("Bank"), RIGHT));
addInputRow(constraints, content, blz, 5, new JLabel(translate("BLZ"), RIGHT));
addInputRow(constraints, content, type, 6, new JLabel(translate("Account"), RIGHT));
addInputRow(constraints, content, typeSpecialProperty, 7, typeSpecialLabel);
addInputRow(constraints, content, balance, 8, new JLabel(translate("Balance"), RIGHT));
addGridBagRow(constraints, content, accountSelectionPanel, 1, translate("IBAN"));
addGridBagRow(constraints, content, name, 2, translate("Name/Family-name"));
addGridBagRow(constraints, content, address, 3, translate("Address"));
addGridBagRow(constraints, content, bankName, 4, translate("Bank"));
addGridBagRow(constraints, content, blz, 5, translate("BLZ"));
addGridBagRow(constraints, content, type, 6, translate("Account"));
addGridBagRow(constraints, content, typeSpecialProperty, 7, typeSpecialLabel);
addGridBagRow(constraints, content, balance, 8, translate("Balance"));
var buttonPanel = Box.createHorizontalBox();
buttonPanel.add(Box.createHorizontalStrut(4));
@ -136,30 +121,6 @@ public class AccountView extends JPanel {
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() {
return accountSelection;
}
@ -179,4 +140,40 @@ public class AccountView extends JPanel {
public JButton getTakeoff() {
return takeoff;
}
/**
* Writes the accessible class fields of the primary account
* into the text fields. Also updates the combo box for
* all associated accounts.
* @param profile the profile to update from
*/
public void updateAccountVariables(Profile profile) {
Logging.LOGGER.finer("Updating account view");
// temporarily extract data
var bank = profile.getBank();
var account = profile.getPrimaryAccount();
var owner = profile.getOwner();
this.blz.setText(bank.getBlz());
this.bankName.setText(bank.getName());
this.iban.setText(String.valueOf(account.getIban()));
this.name.setText(owner.getName() + " " + owner.getFamilyName());
this.address.setText(owner.getStreet() + " " + owner.getCity());
this.balance.setText(StringDecoder.getNumberFormat().format(account.getBalance()) + "");
// update account type specific fields
this.type.setText(translate(account.getClass().getSimpleName()));
if (account instanceof CurrentAccount) {
this.typeSpecialLabel.setText(translate("Overdraft"));
this.typeSpecialProperty.setText(StringDecoder.getNumberFormat().format(((CurrentAccount) account).getOverdraft()) + "");
} else if (account instanceof SavingsAccount) {
this.typeSpecialLabel.setText(translate("Interest rate"));
this.typeSpecialProperty.setText(((SavingsAccount) account).getInterest() + " %");
} else {
Logging.LOGGER.severe("Type of new primary account cannot be determined: " + account.getClass().getName());
}
}
}

View File

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

View File

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

View File

@ -2,30 +2,51 @@ package me.teridax.jcash.gui.deposit;
import me.teridax.jcash.Logging;
import me.teridax.jcash.decode.StringDecoder;
import me.teridax.jcash.gui.IconProvider;
import me.teridax.jcash.gui.InvalidInputException;
import javax.swing.*;
import java.awt.*;
import java.text.NumberFormat;
import java.text.ParseException;
import static me.teridax.jcash.lang.Translator.translate;
/**
* View class for displaying a dialog prompting the user to
* enter a valid amount to deposit at their account
*/
public class DepositView {
/**
* Window to use
*/
private JDialog dialog;
private JButton cancel;
/**
* Button for applying the deposit operation
*/
private JButton deposit;
/**
* Displays the validated value to deposit
*/
private JLabel enteredValue;
/**
* Displays the account balance after the deposit operation
*/
private JLabel balanceAfterDeposit;
private JFormattedTextField value;
public DepositView() {
createComponents();
public DepositView(double maxValue) {
createComponents(maxValue);
layoutComponents();
}
public void showDialog() {
dialog.setIconImage(IconProvider.getWindowIcon());
dialog.setModalityType(Dialog.ModalityType.APPLICATION_MODAL);
dialog.setTitle(translate("Deposit money"));
dialog.pack();
dialog.setSize(dialog.getWidth() * 2, dialog.getHeight());
dialog.setResizable(false);
dialog.setLocationRelativeTo(null);
dialog.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
@ -39,71 +60,103 @@ public class DepositView {
c.gridx = 0;
c.gridy = 0;
c.weightx = 1;
c.weighty = 1;
c.gridwidth = 3;
c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.CENTER;
c.insets = new Insets(4, 4, 4, 4);
dialog.getContentPane().add(new JLabel(translate("Deposit money")), c);
c.gridx = 0;
c.gridy = 1;
c.gridwidth = 1;
c.fill = GridBagConstraints.NONE;
c.anchor = GridBagConstraints.LAST_LINE_END;
c.weightx = 0;
c.insets = new Insets(6, 6, 6, 6);
dialog.getContentPane().add(new JLabel(translate("Value"), SwingConstants.RIGHT), c);
c.gridx = 1;
c.gridy = 1;
c.gridy = 0;
c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.LAST_LINE_END;
c.weightx = 0.5;
dialog.getContentPane().add(value, c);
c.gridx = 2;
c.gridy = 1;
c.gridy = 0;
c.fill = GridBagConstraints.NONE;
c.anchor = GridBagConstraints.LINE_START;
c.weightx = 0;
dialog.getContentPane().add(new JLabel(""), c);
c.gridx = 0;
c.gridy = 1;
c.gridwidth = 1;
c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.LAST_LINE_END;
dialog.getContentPane().add(new JLabel(translate("Value to deposit:"), SwingConstants.RIGHT), c);
c.gridx = 1;
c.gridy = 1;
c.gridwidth = 2;
c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.LAST_LINE_END;
dialog.getContentPane().add(enteredValue, c);
c.gridx = 0;
c.gridy = 2;
c.gridwidth = 1;
c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.LAST_LINE_END;
dialog.getContentPane().add(new JLabel(translate("Balance after deposit:"), SwingConstants.RIGHT), c);
c.gridx = 1;
c.gridy = 2;
c.gridwidth = 2;
c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.LAST_LINE_END;
dialog.getContentPane().add(balanceAfterDeposit, c);
var buttonPanel = new JPanel(new FlowLayout(FlowLayout.TRAILING));
buttonPanel.add(cancel);
buttonPanel.add(deposit);
c.gridx = 0;
c.gridy = 2;
c.gridy = 3;
c.gridwidth = 3;
c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.LAST_LINE_END;
c.insets = new Insets(10, 10, 10, 10);
dialog.getContentPane().add(buttonPanel, c);
}
private void createComponents() {
private void createComponents(double maxValue) {
this.dialog = new JDialog();
this.cancel = new JButton(translate("Cancel"));
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()));
}
public double getAmount() {
/**
* Returns the amount of money that should be deposited
* This value derives from the input of the user.
* @return the value to deposit
* @throws InvalidInputException if the user entered something invalid
*/
public double getAmount() throws InvalidInputException {
if (value.getText().isBlank())
return 0;
throw new InvalidInputException("currency value is blank or has been invalid whilst entered");
try {
return NumberFormat.getNumberInstance().parse(value.getText()).doubleValue();
return StringDecoder.getNumberFormat().parse(value.getText()).doubleValue();
} catch (ParseException e) {
Logging.LOGGER.severe("Amount text field contains invalid value: " + value);
throw new RuntimeException(e);
throw new InvalidInputException(e);
}
}
public JFormattedTextField getValue() {
return value;
}
public JButton getCancel() {
return cancel;
}
@ -115,4 +168,14 @@ public class DepositView {
public void dispose() {
this.dialog.dispose();
}
/**
* Sets the supplied amount to the preview GUI fields.
* @param amount the value to display for value to deposit
* @param after the value to display for balance after deposit
*/
public void setCommittedValue(double amount, double after) {
enteredValue.setText(StringDecoder.getNumberFormat().format(amount));
balanceAfterDeposit.setText(StringDecoder.getNumberFormat().format(after));
}
}

View File

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

View File

@ -2,14 +2,11 @@ package me.teridax.jcash.gui.login;
import me.teridax.jcash.Logging;
import me.teridax.jcash.banking.management.BankingManagementSystem;
import me.teridax.jcash.gui.Utils;
import java.awt.event.ActionEvent;
import java.util.Optional;
import static javax.swing.JOptionPane.ERROR_MESSAGE;
import static javax.swing.JOptionPane.showMessageDialog;
import static me.teridax.jcash.lang.Translator.translate;
public class LoginController {
private final LoginView view;
@ -17,13 +14,17 @@ public class LoginController {
private AccountSelectionListener listener;
public LoginController(BankingManagementSystem bms) {
public LoginController() {
this.view = new LoginView();
this.data = new LoginData(bms);
this.data = new LoginData();
addActionListeners();
}
public void setBankingManagementSystem(BankingManagementSystem bms) {
this.data.setBms(bms);
}
private void addActionListeners() {
this.view.getLogin().addActionListener(this::login);
}
@ -52,15 +53,13 @@ public class LoginController {
var blz = this.view.getBlz().getText();
var iban = this.getIban();
if (iban.isEmpty()) {
Logging.LOGGER.severe("IBAN is invalid: " + iban);
showMessageDialog(null, translate("Invalid IBAN"), translate("Faulty login attempt"), ERROR_MESSAGE);
Utils.error("invalid IBAN entered");
return;
}
var pin = this.getPin();
if (pin.isEmpty()) {
Logging.LOGGER.severe("PIN is invalid: " + pin);
showMessageDialog(null, translate("Invalid pin"), translate("Faulty login attempt"), ERROR_MESSAGE);
Utils.error("invalid PIN entered");
return;
}
@ -68,8 +67,7 @@ public class LoginController {
if (account.isPresent()) {
this.listener.onAccountSelected(account.get());
} else {
Logging.LOGGER.severe("invalid login credentials: " + iban + " / " + pin);
showMessageDialog(null, translate("Invalid login credentials"), translate("Faulty login attempt"), ERROR_MESSAGE);
Logging.LOGGER.warning("invalid login credentials: " + iban + " / " + pin);
}
}
@ -84,4 +82,8 @@ public class LoginController {
public LoginData getData() {
return data;
}
public void logout() {
this.view.getPin().setText("");
}
}

View File

@ -3,6 +3,7 @@ package me.teridax.jcash.gui.login;
import me.teridax.jcash.Logging;
import me.teridax.jcash.banking.management.BankingManagementSystem;
import me.teridax.jcash.banking.management.Profile;
import me.teridax.jcash.gui.Utils;
import java.util.Optional;
@ -11,14 +12,11 @@ import java.util.Optional;
*/
public class LoginData {
private final BankingManagementSystem bms;
public LoginData(BankingManagementSystem bms) {
this.bms = bms;
}
private BankingManagementSystem bms;
/**
* authenticate the specified account with the provided pin.
*
* @param blz the bank identifier
* @param iban the account identifier
* @param pin the pin for the account to authenticate with
@ -26,15 +24,29 @@ public class LoginData {
*/
public Optional<Profile> authenticateAccount(String blz, int iban, int pin) {
Logging.LOGGER.info("Authenticating account " + iban);
var optionalBank = bms.getBank(blz);
if (optionalBank.isEmpty())
if (optionalBank.isEmpty()) {
Utils.error("Unknown BLZ: " + blz);
return Optional.empty();
}
var profile = optionalBank.get().getAccount(iban);
return profile.filter(value -> value.getPrimaryAccount().getPin() == pin);
if (profile.isEmpty()) {
Utils.error("Unknown account: " + iban);
return Optional.empty();
}
public BankingManagementSystem getBms() {
return bms;
var account = profile.filter(value -> value.getPrimaryAccount().getPin() == pin);
if (account.isEmpty()) {
Utils.error("PIN does not match");
return Optional.empty();
}
return account;
}
public void setBms(BankingManagementSystem bms) {
this.bms = bms;
}
}

View File

@ -1,14 +1,30 @@
package me.teridax.jcash.gui.login;
import javax.swing.*;
import javax.swing.text.NumberFormatter;
import java.awt.*;
import java.text.NumberFormat;
import me.teridax.jcash.gui.IconProvider;
import javax.swing.*;
import javax.swing.text.*;
import java.awt.*;
import static me.teridax.jcash.gui.Utils.addGridBagRow;
import static me.teridax.jcash.gui.Utils.addHeading;
import static me.teridax.jcash.lang.Translator.translate;
/**
* GUI class for login into an account
*/
public class LoginView extends JPanel {
/**
* Maximum number of decimal digits that can be stored lossless by a 32-bit signed integer.
* N = log10(2^32-1) = 9,632959861146281
*/
private static final int MAX_PIN_DECIMAL_DIGITS = 9;
/**
* Number of pixels the banner image should be in width
*/
public static final int BANNER_WIDTH = 400;
private JFormattedTextField blz;
private JFormattedTextField iban;
private JPasswordField pin;
@ -20,63 +36,85 @@ public class LoginView extends JPanel {
}
private void layoutComponents() {
var content = new JPanel(new GridBagLayout());
var content = new JLabel();
content.setIcon(new ImageIcon(IconProvider.getBackground()));
content.setLayout(new BorderLayout());
this.setBorder(BorderFactory.createEmptyBorder(8,8,8,8));
this.setLayout(new BorderLayout(16, 16));
var loginPane = new JPanel(new GridBagLayout());
loginPane.setOpaque(true);
content.add(loginPane, BorderLayout.CENTER);
content.add(Box.createHorizontalStrut(BANNER_WIDTH), BorderLayout.WEST);
this.setLayout(new BorderLayout(32, 32));
this.add(new JScrollPane(content), BorderLayout.CENTER);
this.add(new JLabel(translate("Cashmachine")), BorderLayout.NORTH);
var constraints = new GridBagConstraints();
constraints.gridwidth = 4;
constraints.insets = new Insets(12, 12, 12, 12);
addInputRow(constraints, content, blz, 1, translate("BLZ"));
addInputRow(constraints, content, iban, 2, translate("IBAN"));
addInputRow(constraints, content, pin, 3, translate("PIN"));
addGridBagRow(constraints, loginPane, new JLabel(addHeading(translate("Cashmachine"))), 0, "");
addGridBagRow(constraints, loginPane, blz, 1, translate("BLZ"));
addGridBagRow(constraints, loginPane, iban, 2, translate("IBAN"));
addGridBagRow(constraints, loginPane, pin, 3, translate("PIN"));
constraints.gridy = 4;
constraints.anchor = GridBagConstraints.PAGE_END;
constraints.weightx = 0;
constraints.fill = GridBagConstraints.NONE;
constraints.insets = new Insets(12,12,12,12);
content.add(login, constraints);
constraints.insets = new Insets(0, 0, 0, 12);
loginPane.add(login, constraints);
}
private void createComponents() {
this.blz = new JFormattedTextField();
this.iban = new JFormattedTextField(getNumberFormat());
this.iban = new JFormattedTextField();
this.pin = new JPasswordField();
this.login = new JButton(translate("Login"));
// customize login button
// this may not work with every swing look and feel
this.login.setFont(new Font("Circus", Font.PLAIN, 28));
this.login.setBackground(Color.CYAN);
restrictPasswordToDigits();
restrictIbanInput();
}
private NumberFormatter getNumberFormat() {
var format = NumberFormat.getIntegerInstance();
format.setGroupingUsed(false);
/**
* Adds a document filter onto {@link #pin} that filters out everything that is not a digit.
* The filter also restricts the amount of digits that can be entered to {@link #MAX_PIN_DECIMAL_DIGITS}
*/
private void restrictPasswordToDigits() {
((AbstractDocument) this.pin.getDocument()).setDocumentFilter(new DocumentFilter() {
@Override
public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs)
throws BadLocationException {
String newText = fb.getDocument().getText(0, fb.getDocument().getLength()) + text;
var formatter = new NumberFormatter(format);
formatter.setValueClass(Integer.class);
formatter.setMinimum(0);
formatter.setMaximum(Integer.MAX_VALUE);
formatter.setAllowsInvalid(false);
return formatter;
if (newText.matches(String.format("\\d{1,%s}", MAX_PIN_DECIMAL_DIGITS))) {
super.replace(fb, offset, length, text, attrs);
}
}
});
}
private void addInputRow(GridBagConstraints constraints, JComponent target, JComponent comp, int row, String name) {
constraints.gridwidth = 1;
constraints.gridx = 1;
constraints.gridy = row;
constraints.weightx = 0;
constraints.fill = GridBagConstraints.HORIZONTAL;
target.add(new JLabel(name, SwingConstants.RIGHT), constraints);
/**
* Adds a document filter onto {@link #iban} that filters out everything that is not a digit.
* The filter also restricts the amount of digits that can be entered to {@link #MAX_PIN_DECIMAL_DIGITS}
*/
private void restrictIbanInput() {
((AbstractDocument) this.iban.getDocument()).setDocumentFilter(new DocumentFilter() {
@Override
public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs)
throws BadLocationException {
String newText = fb.getDocument().getText(0, fb.getDocument().getLength()) + text;
constraints.gridx = 2;
constraints.gridy = row;
constraints.weightx = 1;
constraints.fill = GridBagConstraints.HORIZONTAL;
target.add(comp, constraints);
if (newText.matches(String.format("\\d{1,%s}", MAX_PIN_DECIMAL_DIGITS))) {
super.replace(fb, offset, length, text, attrs);
}
}
});
}
public JTextField getBlz() {

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,18 +14,21 @@ public class TransferData {
/**
* Transfers a certain amount of money to the specified account of the specified bank
*
* @param amount the amount to transfer
* @param blz the bank that manages the account
* @param ibanString the internal bank account number to transfer money to
* @throws IllegalArgumentException if the bank or the account do not exist
*/
public void transferValue(double amount, String blz, String ibanString) throws IllegalArgumentException {
// get bank to transfer to
var bank = bms.getBank(blz);
if (bank.isEmpty()) {
Logging.LOGGER.warning("Bank not found: " + blz);
throw new IllegalArgumentException("Bank not found: " + blz);
}
// validate iban of target account
var iban = 0;
try {
iban = StringDecoder.decodeUniqueIdentificationNumber(ibanString);
@ -34,6 +37,7 @@ public class TransferData {
throw new IllegalArgumentException("IBAN has invalid format: " + ibanString);
}
// get account to transfer value to
var account = bank.get().getAccount(iban);
if (account.isEmpty()) {
Logging.LOGGER.warning("Account not found: " + iban);
@ -42,4 +46,32 @@ public class TransferData {
account.get().getPrimaryAccount().deposit(amount);
}
/**
* Validates the given BLZ. If no bank with the given BLZ can be found
* this method returns false. Otherwise, true is returned.
* @param blz the BLZ to validate
* @return true if the BLZ is valid and false otherwise
*/
public boolean validateBLZ(String blz) {
return bms.getBank(blz).isPresent();
}
/**
* Validates the given IBAN for the given BLZ. This method assumes the BLZ to be valid.
* If this is not the case, this function will throw an exception.
* Returns true if an account with the given IBAN was found for bank of the given BLZ.
* @param blz bank to search in
* @param ibanString account to search for
* @return true if the account was found false otherwise
*/
public boolean validateIBAN(String blz, String ibanString) {
var bank = bms.getBank(blz);
try {
var iban = StringDecoder.decodeUniqueIdentificationNumber(ibanString);
return bank.map(value -> value.getAccount(iban).isPresent()).orElse(false);
} catch (Exception e) {
return false;
}
}
}

View File

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

View File

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

View File

@ -1,5 +1,7 @@
package me.teridax.jcash.lang;
import me.teridax.jcash.Logging;
import java.util.Locale;
/**
@ -9,14 +11,15 @@ import java.util.Locale;
public class Locales {
/**
* Locale string used to identify a certain locale
* Default locale initialized to the fallback locale used by this application.
*/
private static String defaultLocale = "en_EN";
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
*/
@ -24,13 +27,14 @@ public class Locales {
var locale = language + "_" + country;
if (Translator.setTranslationLocale(locale)) {
defaultLocale = new Locale(language, country);
// apply locale to JVM
Locale.setDefault(new Locale(language, country));
defaultLocale = locale;
Locale.setDefault(defaultLocale);
}
Logging.LOGGER.info("Using locale: " + locale);
}
public static String getDefaultLocale() {
public static Locale getDefaultLocale() {
return defaultLocale;
}

View File

@ -6,12 +6,12 @@ import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
public final class Translator {
/**
* Precomputed index of the locale used to statically translate a phrase
* Static translator class.
* This is a very simple translator able to translate base tokens from english into a variety of
* configured languages.
*/
private static int localeTranslationIndex = 0;
public final class Translator {
/**
* List of all supported locales in the format: language_country. Examples: en_EN, fr_FR
@ -23,6 +23,10 @@ public final class Translator {
* 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
@ -64,6 +68,7 @@ public final class Translator {
/**
* 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
*/
@ -81,6 +86,7 @@ public final class Translator {
/**
* Returns an array of all available locales for this translator
*
* @return an array of all available locales for this translator
*/
public static String[] availableLocales() {
@ -90,6 +96,7 @@ public final class Translator {
/**
* 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
@ -107,6 +114,7 @@ public final class Translator {
/**
* 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
*/

View File

@ -1,48 +1,54 @@
en_EN,de_DE,es_ES,fr_FR,zh_Hans
Bank,Bank,Banco,Banque,银行
BLZ,BLZ,BLZ,CODE BANCAIRE,分类代码
PIN,PIN,PIN,CODE PIN,密码
Balance,Kontostand,Saldo,Solde du compte,账户余额
BLZ,BLZ,BLZ,BLZ,BLZ
PIN,PIN,PIN,PIN,密码
Balance,Kontostand,Saldo,Solde,余额
Account type,Kontoart,Tipo de cuenta,Type de compte,账户类型
Interest,Zins,Interés,Intérêt,利息
Overdraft,Überziehungsbetrag,Importe del descubierto,Montant du découvert,透支金额
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,客户姓名
Name,Vorname,Nombre,Prénom,姓名
Name,Name,Nombre,Nom du client,姓名
Name,Name,Nombre,Nom et prénom,姓名
Street,Straße,Calle,Rue,街道
PLZ,PLZ,PLZ,NPA,邮政编码
City,Ort,Ubicación,Ville,城市
Password,Passwort,contraseña,Mot de passe,密码
Login,Anmelden,Inicio de sesión,S'inscrire,登录
Current account,Girokonto,Cuenta corriente,Compte courant,活期账户
Savings account,Sparkonto,Cuenta de ahorro,Compte d'épargne,储蓄账户
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,desconectarse,Se désinscrire,退出登录
Transfer,Überweisen,transferencia,Virement bancaire,转账
Deposit,Einzahlen,depósito,Dépôt,存款
Take off,Abheben,despegar,Retrait,取款
Value,Betrag,Importe,Montant,金额
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 auswählen,Seleccionar base de datos,Sélectionner la base de données,选择数据库
Invalid account,Ungültiges Benutzerkonto,Cuenta de usuario no válida,Compte utilisateur non valide,用户账户无效
Could not transfer,Überweiung fehlgeschlagen,Transferencia fallida,Échec du transfert,转账失败
Transfer,Überweisen,Transferencia,Transfert,转帐
invalid amount,Ungültiger Betrag,Importe no válido,Montant non valide,金额无效
currency must not be blank,Betrag darf nicht leer sein,El importe no debe estar vacío,Le montant ne doit pas être vide,金额不能为空
Transfer money,Geld überweisen,Transferencia de dinero,Transférer de l'argent,汇款
Could not take off money,Geld konnte nicht abgehoben werden,No se ha podido retirar dinero,L'argent n'a pas pu être retiré,无法取款
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,Retirar,Retrait,取款
Currency must not be blank,Betrag darf nicht leer sein,El importe no debe estar vacío,Le montant ne doit pas être vide,金额不能为空
Cashmachine,Bankautomat,CAJERO,Distributeur automatique de billets,ATM
Invalid IBAN,Ungültige IBAN,IBAN no válido,IBAN non valide,无效的IBAN
Faulty login attempt,Ungültiger Authentifizierungsverzuch,Solicitud de autenticación no válida,Demande d'authentification non valide,验证请求无效
Invalid PIN,Ungültiger PIN,PIN no válido,Code PIN non valide,密码无效
Invalid login credentials,Ungültiges Passwort oder Nutzername,Contraseña o nombre de usuario no válidos,Mot de passe ou nom d'utilisateur non valide,密码或用户名无效
Deposit money,Geld einzahlen,Depositar dinero,Dépôt d'argent,存款金额
Interest rate,Zinsbetrag,Importe de los intereses,Montant des intérêts,利息金额
Name/Family-name,Vorname/Name,Nombre y apellidos,Prénom/nom,名/姓
Address,Adresse,Dirección,Adresse,地址
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,JCash está cerrado,JCash est fermé,JCash 已关闭
you're logged out,Du bist abgemeldet,Ha cerrado la sesión,Tu es déconnecté,您已注销
Closing JCash,JCash wird geschlossen,Cerrar JCash,Clôture de JCash,关闭 JCash
you're logged out,Sie sind abgemeldet,has cerrado sesión,Vous êtes déconnecté,您已退出登录
Comma separated value spreadsheet,Kommagetrennte Werte-Tabelle,Hoja de cálculo de valores separados por comas,Tableur de valeurs séparées par des virgules,逗号分隔值电子表格
Value to transfer:,Zu übertragender Wert:,Valor a transferir:,Valeur à transférer :,要转账的金额:
Balance after transfer:,Kontostand nach Überweisung:,Saldo después de la transferencia:,Solde après transfert :,转账后的余额
Balance after takeoff:,Kontostand nach dem Abheben:,Saldo después de retirar:,Solde après décollage :,取出后的余额
Value to deposit:,Einzuzahlender Wert:,Valor a ingresar:,Valeur à déposer :,存款价值
Value to takeoff:,Auszuhalender Wert:,Valor a despegar:,Valeur à l'enlèvement :,起飞价值

1 en_EN de_DE es_ES fr_FR zh_Hans
2 Bank Bank Banco Banque 银行
3 BLZ BLZ BLZ CODE BANCAIRE BLZ 分类代码 BLZ
4 PIN PIN PIN CODE PIN PIN 密码
5 Balance Kontostand Saldo Solde du compte Solde 账户余额 余额
6 Account type Kontoart Tipo de cuenta Type de compte 账户类型
7 Interest Zins Zinsen Interés Intérêt Intérêts 利息
8 Overdraft Überziehungsbetrag Überziehungskredit Importe del descubierto Descubierto Montant du découvert Découvert 透支金额 透支
9 Customer number Kundennummer Número de cliente Numéro de client 客户编号
10 Name Name Nombre Nom Nom du client 客户姓名 姓名
11 Name Vorname Name Nombre Prénom Nom et prénom 姓名
12 Street Straße Calle Rue 街道
13 PLZ PLZ PLZ NPA PLZ 邮政编码 PLZ
14 City Ort Ubicación Ciudad Ville 城市
15 Password Passwort Kennwort contraseña Contraseña Mot de passe 密码
16 Login Anmelden Anmeldung Inicio de sesión S'inscrire Connexion 登录
17 Current account CurrentAccount Girokonto Cuenta corriente Compte courant 活期账户
18 Savings account SavingsAccount Sparkonto Cuenta de ahorro CuentaAhorro Compte d'épargne 储蓄账户
19 Address Adresse Dirección Adresse 地址
20 Logout Abmelden desconectarse Cerrar sesión Se désinscrire Déconnexion 退出登录 注销
21 Transfer Überweisen Überweisung transferencia Transferir Virement bancaire Virement 转账
22 Deposit Einzahlen depósito Ingresar Dépôt 存款
23 Take off Abheben despegar Retirar Retrait Enlever 取款
24 Value Betrag Wert Importe Valor Montant Valeur 金额 价值
25 Cancel Abbrechen Cancelar Annuler 取消
26 Load database Datenbank auswählen Datenbank laden Seleccionar base de datos Cargar base de datos Sélectionner la base de données Charger la base de données 选择数据库 加载数据库
27 Invalid account Ungültiges Benutzerkonto Ungültiges Konto Cuenta de usuario no válida Cuenta no válida Compte utilisateur non valide Compte non valide 用户账户无效 无效账户
28 Could not transfer Überweiung fehlgeschlagen Konnte nicht übertragen werden Transferencia fallida No se ha podido transferir Échec du transfert Impossible de transférer 转账失败 无法转账
29 Transfer Überweisen Überweisung Transferencia Transfert Transférer 转帐 转账
30 invalid amount Ungültiger Betrag Importe no válido Montant non valide montant non valide 金额无效 无效金额
31 currency must not be blank Betrag darf nicht leer sein Währung darf nicht leer sein El importe no debe estar vacío La divisa no debe estar en blanco Le montant ne doit pas être vide la devise ne doit pas être vide 金额不能为空 货币不得为空
32 Transfer money Geld überweisen Transferencia de dinero Transférer de l'argent 汇款 转账金额
33 Could not take off money Geld konnte nicht abgehoben werden Konnte Geld nicht abheben No se ha podido retirar dinero No se ha podido retirar el dinero L'argent n'a pas pu être retiré Impossible de retirer de l'argent 无法取款
34 Takeoff money Geld abheben Retirar dinero Retirer de l'argent 取款
35 Takeoff Abheben Retirar Despegue Retrait Décoller 取款 起飞
36 Currency must not be blank Betrag darf nicht leer sein Die Währung darf nicht leer sein El importe no debe estar vacío La divisa no debe estar en blanco Le montant ne doit pas être vide La monnaie ne doit pas être en blanc 金额不能为空 货币不得为空
37 Cashmachine Bankautomat Geldautomat CAJERO Cajero Distributeur automatique de billets Cashmachine ATM 提款机
38 Invalid IBAN Ungültige IBAN IBAN no válido IBAN non valide 无效的IBAN IBAN 无效
39 Faulty login attempt Ungültiger Authentifizierungsverzuch Fehlerhafter Anmeldeversuch Solicitud de autenticación no válida Intento de acceso erróneo Demande d'authentification non valide Tentative de connexion erronée 验证请求无效 尝试登录失败
40 Invalid PIN Ungültiger PIN Ungültige PIN PIN no válido Code PIN non valide PIN invalide 密码无效
41 Invalid login credentials Ungültiges Passwort oder Nutzername Ungültige Anmeldedaten Contraseña o nombre de usuario no válidos Credenciales de inicio de sesión no válidas Mot de passe ou nom d'utilisateur non valide Identifiants de connexion invalides 密码或用户名无效 登录凭证无效
42 Deposit money Geld einzahlen Depositar dinero Dépôt d'argent 存款金额 存款
43 Interest rate Zinsbetrag Zinssatz Importe de los intereses Tipo de interés Montant des intérêts Taux d'intérêt 利息金额 利率
44 Name/Family-name Vorname/Name Name/Familienname Nombre y apellidos Prénom/nom Nom/Prénom de famille 名/姓 姓名
45 Address Adresse Anschrift Dirección Adresse 地址
46 Account Konto Cuenta Compte 账户
47 Closing JCash JCash wird geschlossen JCash está cerrado Cerrar JCash JCash est fermé Clôture de JCash JCash 已关闭 关闭 JCash
48 you're logged out Du bist abgemeldet Sie sind abgemeldet Ha cerrado la sesión has cerrado sesión Tu es déconnecté Vous êtes déconnecté 您已注销 您已退出登录
49 Comma separated value spreadsheet Kommagetrennte Werte-Tabelle Hoja de cálculo de valores separados por comas Tableur de valeurs séparées par des virgules 逗号分隔值电子表格
50 Value to transfer: Zu übertragender Wert: Valor a transferir: Valeur à transférer : 要转账的金额:
51 Balance after transfer: Kontostand nach Überweisung: Saldo después de la transferencia: Solde après transfert : 转账后的余额
52 Balance after takeoff: Kontostand nach dem Abheben: Saldo después de retirar: Solde après décollage : 取出后的余额
53 Value to deposit: Einzuzahlender Wert: Valor a ingresar: Valeur à déposer : 存款价值
54 Value to takeoff: Auszuhalender Wert: Valor a despegar: Valeur à l'enlèvement : 起飞价值