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
SimpleStringPropertyfor the data I want to display within theTableView, butSimpleStringPropertyisn't serializable. So as a workaround, I createdStringfields 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;
}