0
\$\begingroup\$

Somewhat experienced with JavaFX, but this is my first potentially large JavaFX project that I recently started and wanted some feedback on anything that could be done better or more efficiently thus far, before continuing.

Note: I am aware that there are a few functions not in use, I'm just leaving them in for future use.

main does nothing really special.

package Java.Application;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception{
        Parent root = FXMLLoader.load(getClass().getResource("/Resources/MainWindow.fxml"));
        primaryStage.setTitle("Finance Manager");
        primaryStage.setScene(new Scene(root, 1200, 700));
        //primaryStage.setMaximized(true);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

The MainController, on run the application will determine which file in a directory was most recently used, the files information is then parsed and loaded into the UI. The directory is filled with .fmd files, which is just a custom little file format I created, which stands for finance manager data. This is the first time I've created a file format for an application and would especially enjoy thoughts on this part of the application. I will include the inside of a .fmd file at the end.

Specific thought: the setLabels() method's readability is just awful, but maybe that is just me.

package Java.Controllers;

import Java.Application.*;
import javafx.collections.FXCollections;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import java.io.*;
import java.net.URL;
import java.text.ParseException;
import java.util.List;
import java.util.ResourceBundle;

public class MainController implements Initializable{
    public Label profileName, yearlyIncome, monthlyIncome, weeklyIncome, dailyIncome,
            yearlyAfterExpenses, monthlyAfterExpenses, weeklyAfterExpenses, dailyAfterExpenses;
    public TableColumn<Statement, String> colExpense, colYearly, colMonthly, colWeekly, colDaily, colNotes;
    public TableView<Statement> expenseTable;

    private String PATH = "Profiles/"; // TODO: Allow profile path to be changed to a different default.

    @Override
    public void initialize(URL location, ResourceBundle resource){
        setTableCellValues(); // Sets table columns with correct properties.
        if(Directory.getSize(PATH) > 0){
            try {
                loadFile(FileTools.getMostRecentFile(PATH));
            } catch (IOException ex) {
                System.err.println("File Failed To Load.");
            } catch (ParseException ex){
                System.err.println("Date Could'nt Be Parsed.");
            }
        }
    }

    private void setTableCellValues() {
        colExpense.setCellValueFactory(cellData -> cellData.getValue().nameProperty());
        colYearly.setCellValueFactory(cellData -> cellData.getValue().yearlyProperty());
        colMonthly.setCellValueFactory(cellData -> cellData.getValue().monthlyProperty());
        colWeekly.setCellValueFactory(cellData -> cellData.getValue().weeklyProperty());
        colDaily.setCellValueFactory(cellData -> cellData.getValue().dailyProperty());
        colNotes.setCellValueFactory(cellData -> cellData.getValue().noteProperty());
    }

    private void loadFile(File file){
        try{
            FmdParser parser = new FmdParser(file);
            List<Statement> incomeStatements = parser.getAll("income");
            List<Statement> expenseStatements = parser.getAll("expense");

            setLabels(file, incomeStatements, expenseStatements); // Sets labels appropriately.
            expenseTable.setItems(FXCollections.observableArrayList(expenseStatements));
        } catch (IOException ex) {
            System.err.println("File Failed To Load.");
        }
    }

    private void setLabels(File file, List<Statement> incomes, List<Statement> expenses){
        double yearly = StatementTools.getYearlySummation(incomes);
        double monthly = StatementTools.getMonthlySummation(incomes);
        double weekly = StatementTools.getWeeklySummation(incomes);
        double daily = StatementTools.getDailySummation(incomes);

        profileName.setText("Profile Name: " + FileTools.getFileName(file));
        yearlyIncome.setText(String.format("Yearly Income: $%.2f", yearly));
        monthlyIncome.setText(String.format("Monthly Income: $%.2f", monthly));
        weeklyIncome.setText(String.format("Weekly Income: $%.2f", weekly));
        dailyIncome.setText(String.format("Daily Income: $%.2f", daily));

        yearlyAfterExpenses.setText(String.format("After Expenses: $%.2f", yearly - StatementTools.getYearlySummation(expenses)));
        monthlyAfterExpenses.setText(String.format("After Expenses: $%.2f", monthly - StatementTools.getMonthlySummation(expenses)));
        weeklyAfterExpenses.setText(String.format("After Expenses: $%.2f", weekly - StatementTools.getWeeklySummation(expenses)));
        dailyAfterExpenses.setText(String.format("After Expenses: $%.2f", daily - StatementTools.getDailySummation(expenses)));
    }
}

FmdParser is used to parse the .fmd files information:

package Java.Application;

import java.io.*;
import java.util.ArrayList;
import java.util.List;

/**
 * Created on 8/4/2016.
 *
 */
public class FmdParser{
    File fmdFile;

    public FmdParser(File file){
        if(FileTools.getFileExtension(file).equals("fmd")){
            fmdFile = file;
        } else {
            throw new IllegalArgumentException("Unsupported File Format");
        }
    }

    public List<Statement> getAll(String type) throws IOException {
        BufferedReader reader = new BufferedReader(new FileReader(fmdFile));
        List<Statement> statements = new ArrayList<>();

        String line;
        while ((line = reader.readLine()) != null) {
            String[] parsed = line.split(":");
            if(parsed[0].equals(type)){
                String[] attributes = parsed[1].split(",");
                statements.add(new Statement(
                        attributes[0],
                        Double.valueOf(attributes[1]),
                        Double.valueOf(attributes[2]),
                        Double.valueOf(attributes[3]),
                        Double.valueOf(attributes[4]),
                        attributes[5]));
            }
        }
        return statements;
    }
}

The Statement class groups all of the information that starts with expense: or income: in the .fmd file. The information in the class can then be calculated, or loaded into a TableView if need be.

package Java.Application;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

/**
 * Created on 8/4/2016.
 *
 */
public class Statement {
    double yearly, monthly, weekly, daily;
    private StringProperty nameProperty, yearlyProperty, monthlyProperty, weeklyProperty, dailyProperty, noteProperty;

    public Statement(String name, double yearly, double monthly, double weekly, double daily, String note){
        // Double variables, used in mathematical purposes.
        this.yearly = yearly;
        this.monthly = monthly;
        this.weekly = weekly;
        this.daily = daily;

        // StringProperty variables, used for storing information in javafx view tables.
        this.nameProperty = new SimpleStringProperty(name);
        this.yearlyProperty = new SimpleStringProperty(String.valueOf(yearly));
        this.monthlyProperty = new SimpleStringProperty(String.valueOf(monthly));
        this.weeklyProperty = new SimpleStringProperty(String.valueOf(weekly));
        this.dailyProperty = new SimpleStringProperty(String.valueOf(daily));
        this.noteProperty = new SimpleStringProperty(note);
    }

    /**
     * Used to return the specified variable of the Statement instance.
     *
     */
    public String getName() {
        return nameProperty.get();
    }

    public double getYearly() {
        return yearly;
    }

    public double getMonthly() {
        return monthly;
    }

    public double getWeekly() {
        return weekly;
    }

    public double getDaily() {
        return daily;
    }

    public String getNote() {
        return noteProperty.get();
    }

    public StringProperty nameProperty() {
        return nameProperty;
    }

    public StringProperty yearlyProperty() {
        return yearlyProperty;
    }

    public StringProperty monthlyProperty() {
        return monthlyProperty;
    }

    public StringProperty weeklyProperty() {
        return weeklyProperty;
    }

    public StringProperty dailyProperty() {
        return dailyProperty;
    }

    public StringProperty noteProperty() {
        return noteProperty;
    }
}

StatementTools is a class containing static methods that are used strictly for calculating statement information.

Specific thought: it's a shame to have so many methods doing pretty much the exact same thing, but I couldn't think of a way to compress the four down into one efficiently.

package Java.Application;

import java.util.List;
import java.util.stream.Collectors;

/**
 * Created on 8/5/2016.
 *
 */
public class StatementTools {

    public static double getDailySummation(List<Statement> statements) {
        return statements.stream().collect(Collectors.summingDouble(Statement::getDaily));
    }

    public static double getMonthlySummation(List<Statement> statements) {
        return statements.stream().collect(Collectors.summingDouble(Statement::getMonthly));
    }

    public static double getWeeklySummation(List<Statement> statements) {
        return statements.stream().collect(Collectors.summingDouble(Statement::getWeekly));
    }

    public static double getYearlySummation(List<Statement> statements) {
        return statements.stream().collect(Collectors.summingDouble(Statement::getYearly));
    }

}

FileTools is a class used for extracting data from files:

package Java.Application;

import java.io.*;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.text.ParseException;
import java.util.Date;
import java.util.List;

/**
 * Created on 8/4/2016.
 */
public class FileTools {
    public static List<String> getFileContents(File file) throws IOException {
        return Files.readAllLines(file.toPath(), Charset.forName("utf-8"));
    }

    // Only compatible with windows files.
    public static String getFileExtension(File file){
        String fileName = file.getName();
        return fileName.substring(fileName.lastIndexOf(".") + 1);
    }

    public static String getFileName(File file){
        String fileName = file.getName();
        return fileName.substring(0, fileName.lastIndexOf("."));
    }

    public static File getMostRecentFile(String path) throws IOException, ParseException {
        File[] files = Directory.getFiles(path);

        switch(files.length){
            case 0 : return null;
            case 1 : return files[0];
        }

        File mostRecentFile = files[0];
        Date mostRecentDate = new Date(mostRecentFile.lastModified());
        for(int i = 1; i < files.length; i++){
            Date possibleMostRecentDate = new Date(files[i].lastModified());

            if(mostRecentDate.before(possibleMostRecentDate)){
                mostRecentFile = files[i];
                mostRecentDate = new Date(mostRecentFile.lastModified());
            }
        }
        return mostRecentFile;
    }

    public static String readLine(String path, int line) throws IOException {
        BufferedReader reader = new BufferedReader(new FileReader(path));

        // Reads line until wanted line number is reached.
        String l = reader.readLine();
        for(int i = 1; i <= line; i++){
            l = reader.readLine();
        }
        reader.close();
        return l;
    }

}

Directory is a small class containing methods for working with directories:

package Java.Application;

import java.io.File;

/**
 * Created on 8/4/2016.
 */
public class Directory {

    public static int getSize(String path){
        return new File(path).listFiles().length;
    }

    public static File[] getFiles(String path){
        return new File(path).listFiles();
    }
}

An example of the inside of a .fmd file:

File name: Bob Smith.fmd

income:Programming,100000,8333.33,1851.85,273.97,None
income:Holidays,1000,83.33,19.8,2.8,None
expense:Cell Phone,1200,100,23.81,3.40,None
expense:Internet,1800,150,35.71,5.10,None

I didn't post the fxml file as I think I've posted more than enough already, but if you want to see it, just comment.

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

Disclaimer: I have no experience with JavaFX, so I'll focus on other points. Feel free to ask for more details regarding specific advice if needed.

Naming Conventions

Package names should be lowercase and not start with "java".

Consistency

Sometimes you have a whitespace between your braces (") {"), sometimes you don't ("){"). This itself is really a minor issue, but makes me think that you currently do not auto-format your code. As this generally makes life easier I suggest to let your IDE format your code for you.

Example from your code:

try {
    loadFile(FileTools.getMostRecentFile(PATH));
} catch (IOException ex) {
    System.err.println("File Failed To Load.");
} catch (ParseException ex){
    System.err.println("Date Could'nt Be Parsed.");
}

Exception Handling

Use a logger instead of System.err and also include the exception itself in the logging output. Both makes it a lot easier to maintain your application and find (potential) issues without having to debug everything each time.

Have a look at the java.util.logging package.

Variable Declarations

Java Code Conventions suggest to have one declaration per line, i.e.:

public Label profileName;
public Label yearlyIncome;
public Label monthlyIncome;

Instead of

public Label profileName, yearlyIncome, monthlyIncome;

Resource Handling

I haven't really verified it, but at first glance it looks like you might have a few potential resource leaks.

Have a look at the try-with-resources statement

Charsets

Instead of using Charset.forName("utf-8") you can use StandardCharsets.UTF_8since Java 7.

Directory Listing

In certain cases new File().listFiles() may return null (cf. the corresponding javadoc), so you may run into NullPointerExceptions with your Directory class.

Have a look at Java 7's new java.nio.file package for more advanced and modern methods.

Calculations

When writing anything that requires correct calculations (e.g. a finance manager), do not use double. There are basically two ways to do this right:

  • Store everything as the smallest unit (i.e. only cents) and use long
  • Use BigDecimal
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.