3
\$\begingroup\$

I'm fairly new to Java, and I've been working on the following program. It's a basic CRUD app utilizing JavaFX and MVC design pattern. I'm seeking advice because the class I took on Java only covered the fundamentals of syntax. So I need to improve when it comes to proper design and standards. Even though my program works, I don't know if I'm doing things the right way.

A couple of specific points of concern:

  • I'm getting a recommendation from the IDE that I should generify AddAnimalController. However, I'm not exactly sure how this should be done in this context.
  • I ran into a bit of a hurdle regarding serialization. I must use SimpleStringProperty for the data I want to display within the TableView, but SimpleStringProperty isn't serializable. So as a workaround, I created String fields to store a copy of such properties for serialization. It works, but I'm sure there's a more professional way of solving this problem.

Any advice on my code or things I should focus on to improve my skills is greatly appreciated.

Core classes:

MainViewController

package com.crud.gsjavafx.controllers;

import com.crud.gsjavafx.models.AnimalList;
import com.crud.gsjavafx.models.RescueAnimal;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.stage.Stage;
import java.io.IOException;
import java.net.URL;
import java.util.Optional;
import java.util.ResourceBundle;

/** Controller for MainView. Handles ListView display as well as initiating actions on listView. */
public class MainViewController implements Initializable {
    @FXML private TableView<RescueAnimal> tableView;
    @FXML private TableColumn<RescueAnimal, String> colName;
    @FXML private TableColumn<RescueAnimal, String> colSpecies;
    @FXML private TableColumn<RescueAnimal, String> colLocation;

    /** Initialize ListView with the saved ArrayList, AnimalList.allAnimals. */
    @Override
    public void initialize(URL url, ResourceBundle resourceBundle) {
        try {
            AnimalList.initializeList();
            colName.setCellValueFactory(data -> data.getValue().animalNameProperty());
            colSpecies.setCellValueFactory(data -> data.getValue().animalSpeciesProperty());
            colLocation.setCellValueFactory(data -> data.getValue().locationProperty());
            colName.setCellFactory(TextFieldTableCell.forTableColumn());
            colSpecies.setCellFactory(TextFieldTableCell.forTableColumn());
            colLocation.setCellFactory(TextFieldTableCell.forTableColumn());
            tableView.setItems(AnimalList.allAnimals);
        } catch(Exception e) {
            e.printStackTrace();
        }

        // Set double-click event.
        tableView.setOnMouseClicked(click -> {
            if (click.getClickCount() == 2) {
                editAnimalWindow(getSelection());
            }
        });
    }

    public RescueAnimal getSelection() {
        return tableView.getSelectionModel().getSelectedItem();
    }

    /** Handles action for the 'Add' Button. */
    public void addNewButton() {
        editAnimalWindow(null);
    }

    /** Handles action for the 'Edit' Button. */
    public void editButton() {
        editAnimalWindow(getSelection());
    }

    /** Opens new window to edit selected animal. */
    public void editAnimalWindow(RescueAnimal selectedAnimal) {
        try {
            FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/crud/gsjavafx/addAnimalView.fxml"));
            Parent root = loader.load();
            if (selectedAnimal != null) {
                AddAnimalController controller = loader.getController();
                controller.setSelectedAnimal(selectedAnimal);
                controller.setFields();
            }
            Stage stage = new Stage();
            if (selectedAnimal != null) {
                stage.setTitle("Updating: " + selectedAnimal.getName());
            } else {
                stage.setTitle("Add New Animal to Record:");
            }
            stage.setScene(new Scene(root));
            stage.show();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /** Deletes selected animal after confirmation. */
    public void deleteSelection() {
        RescueAnimal selectedAnimal = getSelection();

        Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
        alert.setTitle("Confirm deletion:");
        alert.setHeaderText("Warning: Clicking 'OK' will remove " + selectedAnimal.getName() + " from record.");
        alert.setContentText("Click 'OK' to continue or 'Cancel' to cancel this request.");
        Optional<ButtonType> result = alert.showAndWait();

        if (result.get() == ButtonType.OK) {
            AnimalList.allAnimals.remove(selectedAnimal);
            AnimalList.saveAnimalList();
        } else {
            alert.close();
        }
    }
}

AddAnimalController

package com.crud.gsjavafx.controllers;

import com.crud.gsjavafx.models.AnimalList;
import com.crud.gsjavafx.models.RescueAnimal;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.GridPane;
import javafx.stage.Window;
import java.net.URL;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.ResourceBundle;

/** Create and add animal to AnimalList.allAnimals then serialize. */
public class AddAnimalController implements Initializable {
    @FXML private GridPane grid;
    @FXML Button saveButton;
    @FXML TextField animalName;
    @FXML TextField animalType;
    @FXML ChoiceBox<String> gender;
    @FXML Spinner<Integer> age;
    @FXML Spinner<Integer> weight;
    @FXML DatePicker acquisitionDate;
    @FXML TextField animalLocation;
    @FXML Spinner<Integer> trainingStatus;
    @FXML CheckBox reserved;
    private RescueAnimal selectedAnimal;
    private final ArrayList<InputValidationController<Node>> nodes = new ArrayList<>();

    /** Allows MainViewController to set the instance that's being edited. */
    public void setSelectedAnimal(RescueAnimal selectedAnimal) {
        this.selectedAnimal = selectedAnimal;
    }

    @Override
    public void initialize(URL url, ResourceBundle resourceBundle) {
        for (Node node : grid.getChildren()) {
            if (node instanceof Spinner) {
                InputValidationController<Node> fieldController = new InputValidationController<>((Spinner<Integer>) node);
                nodes.add(fieldController);
            } else if (node instanceof TextField) {
                InputValidationController<Node> fieldController = new InputValidationController<>((TextField) node);
                nodes.add(fieldController);
            } else if (node instanceof DatePicker) {
                InputValidationController<Node> fieldController = new InputValidationController<>((DatePicker) node);
                nodes.add(fieldController);
            }
        }
    }

    /** Populates fields with the passed instance. */
    public void setFields() {
        final int maxAge = 100;
        final int maxWeight = 999;
        final int maxTrainingLevel = 5;
        animalName.setText(selectedAnimal.getName());
        animalType.setText(selectedAnimal.getAnimalType());
        gender.setValue(selectedAnimal.getGender());
        age.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(selectedAnimal.getAge(),
                maxAge));
        weight.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(selectedAnimal.getWeight(),
                maxWeight));
        acquisitionDate.setValue(LocalDate.parse(selectedAnimal.getAcquisitionDate()));
        animalLocation.setText(selectedAnimal.getLocation());
        trainingStatus.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(
                selectedAnimal.getTrainingStatus(), maxTrainingLevel));
        reserved.setSelected(selectedAnimal.getReserved());
    }

    /** Handles action for Save button */
    public void save() {
        for (InputValidationController<Node> node : nodes) {
            if (node.getError()) {
                Alert alert = new Alert(Alert.AlertType.ERROR);
                alert.setTitle("Error: Save Failed");
                alert.setHeaderText("Please review the form and try again.");
                alert.setContentText("Ensure all fields are set and contain valid data.");
                alert.show();
                return;
            }
        }
        saveAnimal();
    }

    public void saveAnimal() {
        if (selectedAnimal != null) {
            selectedAnimal.setName(animalName.getText());
            selectedAnimal.setAnimalType(animalType.getText());
            selectedAnimal.setGender(gender.getValue());
            selectedAnimal.setAge(Integer.parseInt(age.getValue().toString()));
            selectedAnimal.setWeight(Integer.parseInt(weight.getValue().toString()));
            selectedAnimal.setAcquisitionDate(acquisitionDate.getValue().toString());
            selectedAnimal.setLocation(animalLocation.getText());
            selectedAnimal.setTrainingStatus(Integer.parseInt(trainingStatus.getValue().toString()));
            selectedAnimal.setReserved(reserved.isSelected());
            selectedAnimal.setSerializableName(animalName.getText());
            selectedAnimal.setSerializableSpecies(animalType.getText());
            selectedAnimal.setSerializableLocation(animalLocation.getText());
        } else {
            RescueAnimal newAnimal = new RescueAnimal(animalName.getText(), animalType.getText(),
                    gender.getValue(), Integer.parseInt(age.getValue().toString()),
                    Integer.parseInt(weight.getValue().toString()), acquisitionDate.getValue().toString(),
                    animalLocation.getText(), Integer.parseInt(trainingStatus.getValue().toString()),
                    reserved.isSelected());

            AnimalList.setAllAnimals(newAnimal);
        }
        AnimalList.saveAnimalList();
        closeWindow();
    }

    public void closeWindow() {
        Window window = animalName.getScene().getWindow();
        window.hide();
    }
}

InputValidationController

package com.crud.gsjavafx.controllers;

import javafx.scene.Node;
import javafx.scene.control.*;
import java.time.LocalDate;

public class InputValidationController<T extends Node> {
    private boolean error;
    private final Tooltip toolTip = new Tooltip();

    /** TextField constructor. */
    public InputValidationController (TextField field) {
       validString(field);
       toolTip.setText("Input is required and may not contain digits.");
       field.setTooltip(toolTip);
   }

   /** DatePicker Constructor. */
    public InputValidationController (DatePicker date) {
        validDate(date);
        toolTip.setText("Cannot choose a future date.");
        date.setTooltip(toolTip);
    }

    /** Spinner Constructor. */
    public InputValidationController (Spinner<Integer> numField) {
        validNumber(numField);
        toolTip.setText("Input may not contain digits and must be within specified range.");
        numField.setTooltip(toolTip);
    }

    private void validString(TextField field) {
       if (field.getText() == null) {
           error = true;
       }

       field.setTextFormatter(new TextFormatter<>(c -> {
           if (c.isContentChange()) {
               if (c.getControlNewText().length() == 0) {
                   error = true;
                   raiseWarning(field);
                   return c;
               }
               if (c.getControlNewText().isEmpty()) {
                   return c;
               }
               if (c.getControlNewText().length() > 20) {
                   return null;
               }
               if (!c.getControlNewText().matches("^[A-Za-z]+$")) {
                   raiseWarning(field);
                   return null;
               } else {
                   suppressError();
                   return c;
               }
           }
           return c;
       }));
    }

    private void validDate (DatePicker date) {
        LocalDate today = LocalDate.now();
        date.setValue(today);
        date.setOnAction(e -> {
           if (today.isBefore(date.getValue())) {
               raiseWarning(date);
               date.setValue(today);
           } else {
               suppressError();
           }
       });
    }

    private void validNumber(Spinner<Integer> numField) {
        numField.getEditor().textProperty().addListener((observable, oldV, newV) -> {
            if (!newV.matches("\\d*") || newV.length() == 0) {
                raiseWarning(numField);
                numField.getEditor().setText("0");
            }
        });
    }

    private void raiseWarning(Node node) {
       double x = node.getScene().getWindow().getX() + node.getLayoutX();
       double y = node.getScene().getWindow().getY() + node.getLayoutY();
       toolTip.setAutoHide(true);
       toolTip.show(node, x, y);
    }

    private void suppressError() {
       error = false;
    }

    public boolean getError() {
        return this.error;
    }
}

Serializer (util)

package com.crud.gsjavafx.utils;

import com.crud.gsjavafx.models.AnimalList;
import com.crud.gsjavafx.models.RescueAnimal;
import javafx.collections.ObservableList;
import java.io.*;
import java.util.ArrayList;

/** Serialize and deserialize ArrayList AnimalList.allAnimals (see: {@link AnimalList}). */
public final class Serializer {
    private static final String PATH = "data.bin";

    /** Serializer is static-only, and not to be instantiated. */
    private Serializer() {}

    public static void serialize(ObservableList<RescueAnimal> observableListAnimals) throws IOException {
        try(var serializer = new ObjectOutputStream(new FileOutputStream(PATH, false))) {
            ArrayList<RescueAnimal> convertedList = new ArrayList<>(observableListAnimals);
            serializer.writeObject(convertedList);
        }
    }

    public static ArrayList<RescueAnimal> deserialize() throws IOException, ClassNotFoundException {
        try(ObjectInputStream objectIn = new ObjectInputStream(new FileInputStream(PATH))) {
            ArrayList<RescueAnimal> deserializedArrayList = new ArrayList<>();
            for (RescueAnimal animal : (ArrayList<RescueAnimal>) objectIn.readObject()) {
                RescueAnimal deserializedAnimal = new RescueAnimal(
                        animal.getSerializableName(), animal.getSerializableSpecies(), animal.getGender(),
                        animal.getAge(), animal.getWeight(), animal.getAcquisitionDate(),
                        animal.getSerializableLocation(), animal.getTrainingStatus(), animal.getReserved()
                );
                deserializedArrayList.add(deserializedAnimal);
            }
            return deserializedArrayList;
        }
    }
}

AnimalList (model helper so to speak)

package com.crud.gsjavafx.models;

import com.crud.gsjavafx.utils.Serializer;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import java.io.IOException;

/**
 * A global list of RescueAnimal objects; stores all animals in an ArrayList for serialization.
 */
public final class AnimalList {
    public static ObservableList<RescueAnimal> allAnimals = FXCollections.observableArrayList();

    /** AnimalList is static-only, and not to be instantiated. */
    private AnimalList() {}

    /**
     * Populate public static field, allAnimals, on program start.
     *
     * @return the deserialized ArrayList composed of RescueAnimal(s).
     */
    public static void initializeList() {
        try {
            allAnimals.addAll(Serializer.deserialize());
        } catch(Exception e) {
        }
    }

    /** Add new animal to the list, allAnimals. */
    public static void setAllAnimals(RescueAnimal animal) {
        allAnimals.add(animal);
    }

    /** Serialize the list, allAnimals. */
    public static void saveAnimalList() {
        try {
            Serializer.serialize(allAnimals);
        } catch(IOException e) {
        }
    }
}

RescueAnimal (main model - for brevity, I didn't include getters and setters, but if I should please let me know)

package com.crud.gsjavafx.models;

import javafx.beans.property.SimpleStringProperty;
import java.io.Serial;
import java.io.Serializable;

/**
 *
 */
public class RescueAnimal implements Serializable {
    @Serial private static final long serialVersionUID = 1L;
    transient private SimpleStringProperty animalName;
    transient private SimpleStringProperty animalSpecies;
    transient private SimpleStringProperty location;
    private String serializableName;
    private String serializableSpecies;
    private String gender;
    private int age;
    private int weight;
    private String acquisitionDate;
    private String serializableLocation;
    private int trainingStatus;
    private boolean reserved;

    public RescueAnimal(String name, String species, String gender, int age, int weight,
                        String acquisitionDate, String location, int trainingStatus,
                        boolean reserved) {
        // SimpleStringProperty.
        this.animalName = new SimpleStringProperty(name);
        this.animalSpecies = new SimpleStringProperty(species);
        this.location = new SimpleStringProperty(location);
        // String substitutes for SimpleStringProperty for serialization purposes.
        this.serializableName = getName();
        this.serializableSpecies = getAnimalType();
        this.serializableLocation = getLocation();
        // Standard String fields not used in TableView.
        this.gender = gender;
        this.age = age;
        this.weight = weight;
        this.acquisitionDate = acquisitionDate;
        this.trainingStatus = trainingStatus;
        this.reserved = reserved;
    }
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

I skimmed through your code and could not find anything from skimming. Regarding serialization, if you must use Serializable, you should avoid using the JavaFX properties. If you don't have to use Serializable, keep the properties and use something like JSON instead. Have a look at @Jame_D's answer here.

\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.