From 12020a2feb678267c2dcc45b50301d129ac6cee5 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Tue, 10 Feb 2026 16:35:49 -0700 Subject: [PATCH 1/4] Fixed DB connection and java version issue --- .gitignore | 5 ++- pom.xml | 9 +++-- .../controllers/MainLayoutController.java | 3 +- .../petshopdesktop/database/ConnectionDB.java | 33 ++++++++++++------- 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 480bdf52..4a8c77b5 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,7 @@ build/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store + +## Database related +connectionpetstore.properties diff --git a/pom.xml b/pom.xml index 8cb9a1ba..6da4edce 100644 --- a/pom.xml +++ b/pom.xml @@ -8,9 +8,9 @@ PetShopDesktop 1.0-SNAPSHOT PetShopDesktop - UTF-8 + 25.0.2 5.12.1 @@ -18,12 +18,12 @@ org.openjfx javafx-controls - 21.0.6 + ${javafx.version} org.openjfx javafx-fxml - 21.0.6 + ${javafx.version} @@ -66,8 +66,7 @@ default-cli - org.example.petshopdesktop/org.example.petshopdesktop.PetShopApplication - + org.example.petshopdesktop/org.example.petshopdesktop.PetShopApplication app app app diff --git a/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java b/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java index a5a3060d..431d38d4 100644 --- a/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java +++ b/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java @@ -111,7 +111,8 @@ public class MainLayoutController { spContentArea.getChildren().clear(); spContentArea.getChildren().add(view); } catch (Exception e) { - System.out.println("Error loading view: " + fxmlFile); + System.err.println("Error loading view: " + fxmlFile); + e.printStackTrace(); } } diff --git a/src/main/java/org/example/petshopdesktop/database/ConnectionDB.java b/src/main/java/org/example/petshopdesktop/database/ConnectionDB.java index 2b0bad1d..3435c7ac 100644 --- a/src/main/java/org/example/petshopdesktop/database/ConnectionDB.java +++ b/src/main/java/org/example/petshopdesktop/database/ConnectionDB.java @@ -2,6 +2,9 @@ package org.example.petshopdesktop.database; import java.io.FileInputStream; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; @@ -9,8 +12,7 @@ import java.util.Properties; public class ConnectionDB { /** - * Method to try and connect to the database sing cnnection.properties located in the - * root of C drive + * Method to try and connect to the database using connectionpetstore.properties. * @return Connection to the database */ public static Connection getConnection(){ @@ -18,10 +20,23 @@ public class ConnectionDB { String user = ""; String password = ""; - try{ - //Read connection.properties file - FileInputStream fis = new FileInputStream("c:\\connectionpetstore.properties"); //location of connection can be changed here - Properties prop = new Properties(); + Properties prop = new Properties(); + Path propsPath; + + String explicitPath = System.getenv("PETSTORE_DB_PROPS"); + if (explicitPath != null && !explicitPath.isBlank()) { + propsPath = Paths.get(explicitPath); + } else { + Path cwd = Paths.get(System.getProperty("user.dir"), "connectionpetstore.properties"); + Path xdg = Paths.get(System.getProperty("user.home"), ".config", "petstore", "connectionpetstore.properties"); + Path legacyWindows = Paths.get("./connectionpetstore.properties"); + + if (Files.exists(cwd)) propsPath = cwd; + else if (Files.exists(xdg)) propsPath = xdg; + else propsPath = legacyWindows; + } + + try (FileInputStream fis = new FileInputStream(propsPath.toString())) { prop.load(fis); url = prop.getProperty("url"); user = prop.getProperty("user"); @@ -31,12 +46,8 @@ public class ConnectionDB { throw new RuntimeException("Problem with reading connection info: "+e.getMessage()); } - Connection conn = null; - try{ - //try to get connection with the data taken from connection.properties - conn = DriverManager.getConnection(url,user,password); - return conn; + return DriverManager.getConnection(url,user,password); } catch (SQLException e) { throw new RuntimeException("Problem with database connection: "+e.getMessage()); From bcb383c40a77a48c098fe6e4c4b33dec8deb0515 Mon Sep 17 00:00:00 2001 From: augmentedpotato Date: Wed, 11 Feb 2026 08:40:31 -0700 Subject: [PATCH 2/4] Pets CRUD --- connectionpetstore.properties | 3 + .../controllers/PetController.java | 187 ++++++++++++++-- .../PetDialogController.java | 203 ++++++++++++++++++ .../petshopdesktop/database/PetDB.java | 145 +++++++++++++ .../example/petshopdesktop/models/Pet.java | 109 ++++++++++ 5 files changed, 633 insertions(+), 14 deletions(-) create mode 100644 connectionpetstore.properties create mode 100644 src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PetDialogController.java create mode 100644 src/main/java/org/example/petshopdesktop/database/PetDB.java create mode 100644 src/main/java/org/example/petshopdesktop/models/Pet.java diff --git a/connectionpetstore.properties b/connectionpetstore.properties new file mode 100644 index 00000000..426ecafb --- /dev/null +++ b/connectionpetstore.properties @@ -0,0 +1,3 @@ +url=jdbc:mysql://127.0.0.1:3306/Petstoredb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC +user=petapp +password=petapppass \ No newline at end of file diff --git a/src/main/java/org/example/petshopdesktop/controllers/PetController.java b/src/main/java/org/example/petshopdesktop/controllers/PetController.java index 26834d72..7eb55cb8 100644 --- a/src/main/java/org/example/petshopdesktop/controllers/PetController.java +++ b/src/main/java/org/example/petshopdesktop/controllers/PetController.java @@ -1,11 +1,24 @@ package org.example.petshopdesktop.controllers; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; -import javafx.scene.control.Button; -import javafx.scene.control.TableColumn; -import javafx.scene.control.TableView; -import javafx.scene.control.TextField; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.stage.Modality; +import javafx.stage.Stage; +import org.example.petshopdesktop.controllers.dialogcontrollers.PetDialogController; +import org.example.petshopdesktop.database.PetDB; +import org.example.petshopdesktop.database.ProductDB; +import org.example.petshopdesktop.models.Pet; + +import java.io.IOException; +import java.sql.SQLException; +import java.sql.SQLIntegrityConstraintViolationException; +import java.util.Optional; public class PetController { @@ -19,45 +32,191 @@ public class PetController { private Button btnEdit; @FXML - private TableColumn colPetAge; + private TableColumn colPetAge; @FXML - private TableColumn colPetBreed; + private TableColumn colPetBreed; @FXML - private TableColumn colPetId; + private TableColumn colPetId; @FXML - private TableColumn colPetName; + private TableColumn colPetName; @FXML - private TableColumn colPetPrice; + private TableColumn colPetPrice; @FXML - private TableColumn colPetSpecies; + private TableColumn colPetSpecies; @FXML - private TableColumn colPetStatus; + private TableColumn colPetStatus; @FXML - private TableView tvPets; + private TableView tvPets; @FXML private TextField txtSearch; @FXML void btnAddClicked(ActionEvent event) { - + mode = "Add"; + openDialog(null,mode); } @FXML void btnDeleteClicked(ActionEvent event) { + int numRows = 0; + Pet selectedPet = tvPets.getSelectionModel().getSelectedItem(); + //ask user to confirm + Alert question = new Alert(Alert.AlertType.CONFIRMATION); + question.setHeaderText("Please confirm delete"); + question.setContentText("Are you sure you want to delete this pet?"); + Optional result = question.showAndWait(); //show alert and wait for response + + //if confirmed,start deletion + if (result.isPresent() && result.get() == ButtonType.OK) { + int petId = selectedPet.getPetId(); + + //try deleting + try{ + numRows = PetDB.deletePet(petId); + } + catch (SQLIntegrityConstraintViolationException e){ + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Database Operation Error"); + alert.setContentText("Delete failed\n" + + "the selected pet is being referred in another table"); + alert.showAndWait(); + return; + } + catch (SQLException e) { + throw new RuntimeException(e); + } + + //prompt user of any errors + if (numRows == 0){ + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Database Operation Error"); + alert.setContentText("Delete failed"); + alert.showAndWait(); + } + else{ + //prompt user of delete conformation + Alert alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.setHeaderText("Database Operation Confirmed"); + alert.setContentText("Delete successful"); + alert.showAndWait(); + //refresh display and reset inputs + displayPets(); + btnDelete.setDisable(true); + btnEdit.setDisable(true); + txtSearch.setText(""); + } + } } @FXML void btnEditClicked(ActionEvent event) { + Pet selectedPet = tvPets.getSelectionModel().getSelectedItem(); + if(selectedPet != null){ + mode = "Edit"; + openDialog(selectedPet,mode); + } } -} + private ObservableList data = FXCollections.observableArrayList(); + String mode = null; + + @FXML + void initialize() { + btnEdit.setDisable(true); + btnDelete.setDisable(true); + + colPetId.setCellValueFactory(new PropertyValueFactory("petId")); + colPetName.setCellValueFactory(new PropertyValueFactory("petName")); + colPetSpecies.setCellValueFactory(new PropertyValueFactory("petSpecies")); + colPetBreed.setCellValueFactory(new PropertyValueFactory("petBreed")); + colPetAge.setCellValueFactory(new PropertyValueFactory("petAge")); + colPetStatus.setCellValueFactory(new PropertyValueFactory("petStatus")); + colPetPrice.setCellValueFactory(new PropertyValueFactory("petPrice")); + + displayPets(); + + tvPets.getSelectionModel().selectedItemProperty().addListener( + (observable, oldValue, newValue) -> { + btnEdit.setDisable(false); + btnDelete.setDisable(false); + }); + + txtSearch.textProperty().addListener((observable, oldValue, newValue) -> { + displayFilteredPet(newValue); + }); + } + + private void displayFilteredPet(String filter) { + data.clear(); + try{ + if (txtSearch.getText() == null || txtSearch.getText().isEmpty()){ + displayPets(); + } + else { + data = PetDB.getFilteredPets(filter); + tvPets.setItems(data); + } + } catch (Exception e) { + System.out.println("Error while fetching table data: " + e.getMessage()); + } + } + + private void displayPets() { + data.clear(); + + try{ + data = PetDB.getPets(); + } + catch(SQLException e){ + System.out.println("Error while fetching table data: " + e.getMessage()); + } + + tvPets.setItems(data); + } + + private void openDialog(Pet pet, String mode){ + //Get new view + FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/pet-dialog-view.fxml")); + Scene scene = null; + try{ + scene = new Scene(fxmlLoader.load()); + } catch (IOException e) { + throw new RuntimeException(e); + } + PetDialogController dialogController = fxmlLoader.getController(); //controller associated with this view + dialogController.setMode(mode); + + //Open the dialog depending on the mode + if(mode.equals("Edit")){ + //Make it display pet details in dialog + dialogController.displayPetDetails(pet); + } + Stage dialogStage = new Stage(); + dialogStage.initModality(Modality.APPLICATION_MODAL); //make it modal + if(mode.equals("Add")){ + dialogStage.setTitle("Add Pet"); + } + else { + dialogStage.setTitle("Edit Pet"); + } + dialogStage.setScene(scene); + dialogStage.showAndWait(); + + //When dialog closes update table view and disable edit and delete buttons, and reset search bar + displayPets(); + btnDelete.setDisable(true); + btnEdit.setDisable(true); + txtSearch.setText(""); + } + +} \ No newline at end of file diff --git a/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PetDialogController.java b/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PetDialogController.java new file mode 100644 index 00000000..45be6c3c --- /dev/null +++ b/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PetDialogController.java @@ -0,0 +1,203 @@ +package org.example.petshopdesktop.controllers.dialogcontrollers; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.EventHandler; +import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.input.MouseEvent; +import javafx.stage.Stage; +import org.example.petshopdesktop.DTOs.ProductDTO; +import org.example.petshopdesktop.Validator; +import org.example.petshopdesktop.database.PetDB; +import org.example.petshopdesktop.models.Category; +import org.example.petshopdesktop.models.Pet; + +import java.sql.SQLException; + +public class PetDialogController { + + @FXML + private Button btnCancel; + + @FXML + private Button btnSave; + + @FXML + private ComboBox cbPetStatus; + + @FXML + private Label lblMode; + + @FXML + private Label lblPetId; + + @FXML + private TextField txtPetAge; + + @FXML + private TextField txtPetBreed; + + @FXML + private TextField txtPetName; + + @FXML + private TextField txtPetPrice; + + @FXML + private TextField txtPetSpecies; + + private String mode = null; + + private ObservableList statusList = FXCollections.observableArrayList( + "Available", "Adopted" + ); + + @FXML + void initialize() { + + cbPetStatus.setItems(statusList); //set status combobox + + //Set up mouse handlers for buttons + btnSave.setOnMouseClicked(new EventHandler() { + @Override + public void handle(MouseEvent mouseEvent) { + buttonSaveClicked(mouseEvent); + } + }); + + btnCancel.setOnMouseClicked(new EventHandler() { + @Override + public void handle(MouseEvent mouseEvent) { + closeStage(mouseEvent); + } + }); + } + + private void buttonSaveClicked(MouseEvent mouseEvent) { + int numRow = 0; + String errorMsg = ""; + + //Check validation (input required) + errorMsg += Validator.isPresent(txtPetName.getText(), "Pet Name"); + errorMsg += Validator.isPresent(txtPetAge.getText(), "Age"); + errorMsg += Validator.isPresent(txtPetBreed.getText(), "Breed"); + errorMsg += Validator.isPresent(txtPetSpecies.getText(), "Species"); + errorMsg += Validator.isPresent(txtPetPrice.getText(), "Price"); + if (cbPetStatus.getSelectionModel().getSelectedItem() == null){ + errorMsg += "Status is required"; + } + + //Check validation (length size) + errorMsg += Validator.isLessThanVarChars(txtPetName.getText(), "Pet Name", 50); + errorMsg += Validator.isLessThanVarChars(txtPetSpecies.getText(), "Species", 50); + errorMsg += Validator.isLessThanVarChars(txtPetBreed.getText(), "Breed", 50); + errorMsg += Validator.isLessThanVarChars(txtPetPrice.getText(), "Price", 12); + errorMsg += Validator.isLessThanVarChars(txtPetAge.getText(), "Age", 11); + + //Check validation (format) + errorMsg += Validator.isNonNegativeDouble(txtPetPrice.getText(), "Price"); + errorMsg += Validator.isNonNegativeInteger(txtPetAge.getText(), "Age"); + + if(errorMsg.isEmpty()){ + Pet pet = collectPet(); + if(mode.equals("Add")) { + try{ + numRow = PetDB.insertPet(pet); + } + catch (SQLException e) { + throw new RuntimeException(e); + } + } + else { + try { + numRow = PetDB.updatePet(pet.getPetId(), pet); + } + catch (SQLException e) { + throw new RuntimeException(e); + } + } + + //if no rows were affected then there was an error (prompt user of error) + if (numRow == 0){ + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Database Operation Error"); + alert.setContentText(mode + " failed"); + alert.showAndWait(); + closeStage(mouseEvent); + } + else { + //tell the user operation was successful + Alert alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.setHeaderText("Database Operation Confirmed"); + alert.setContentText(mode + " succeeded"); + alert.showAndWait(); + closeStage(mouseEvent); + } + } + else{ + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Input Error"); + alert.setContentText(errorMsg); + alert.showAndWait(); + } + } + + private Pet collectPet() { + int petId =0; + Pet pet = null; + + if(lblPetId.isVisible()){ + petId = Integer.parseInt(lblPetId.getText().split(": ")[1]); + } + pet = new Pet( + petId, + txtPetName.getText(), + txtPetSpecies.getText(), + txtPetBreed.getText(), + Integer.parseInt(txtPetAge.getText()), + cbPetStatus.getValue(), + Double.parseDouble(txtPetPrice.getText()) + ); + return pet; + } + + private void closeStage(MouseEvent mouseEvent) { + Node node = (Node) mouseEvent.getSource(); + Stage stage = (Stage) node.getScene().getWindow(); + stage.close(); + } + + public void displayPetDetails(Pet pet){ + if (pet!=null){ + lblPetId.setText("ID: " + pet.getPetId()); + txtPetName.setText(pet.getPetName()); + txtPetSpecies.setText(pet.getPetSpecies()); + txtPetBreed.setText(pet.getPetBreed()); + txtPetAge.setText(pet.getPetAge() + ""); + txtPetPrice.setText(pet.getPetPrice() + ""); + + //get the right combobox selection + for (String status : cbPetStatus.getItems()) { + if(status.equals(pet.getPetStatus())){ + cbPetStatus.getSelectionModel().select(status); + } + } + + + } + } + + public void setMode(String mode) { + this.mode = mode; + lblMode.setText(mode + " Pet"); + if(mode.equals("Add")) { + lblPetId.setVisible(false); + } + else if(mode.equals("Edit")) { + lblPetId.setVisible(true); + } + } + +} diff --git a/src/main/java/org/example/petshopdesktop/database/PetDB.java b/src/main/java/org/example/petshopdesktop/database/PetDB.java new file mode 100644 index 00000000..d1b7ca37 --- /dev/null +++ b/src/main/java/org/example/petshopdesktop/database/PetDB.java @@ -0,0 +1,145 @@ +package org.example.petshopdesktop.database; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import org.example.petshopdesktop.models.Pet; + +import java.sql.*; + +public class PetDB { + public static ObservableList getPets() throws SQLException { + //Connect to the database + ObservableList pets = FXCollections.observableArrayList(); + Connection conn = ConnectionDB.getConnection(); + + //Execute Query + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM pet"); + + //While there is still data add pets to the list + while(rs.next()){ + Pet pet = new Pet( + rs.getInt(1), + rs.getString(2), + rs.getString(3), + rs.getString(4), + rs.getInt(5), + rs.getString(6), + rs.getDouble(7) + ); + pets.add(pet); + } + + //close connection and return pets + conn.close(); + return pets; + } + + public static ObservableList getFilteredPets(String filter) throws SQLException { + //Connect to the database + ObservableList pets = FXCollections.observableArrayList(); + Connection conn = ConnectionDB.getConnection(); + + //Get SQL query for filtered word + String sql = "SELECT * FROM pet " + + " WHERE " + + "petName LIKE ? OR " + + "petSpecies LIKE ? OR " + + "petBreed LIKE ? OR " + + "petAge LIKE ? OR " + + "petStatus LIKE ? OR " + + "petPrice LIKE ?"; + + String filteredString = "%" + filter + "%"; + + PreparedStatement stmt = conn.prepareStatement(sql); + stmt.setString(1, filteredString); + stmt.setString(2, filteredString); + stmt.setString(3, filteredString); + stmt.setString(4, filteredString); + stmt.setString(5, filteredString); + stmt.setString(6, filteredString); + + ResultSet rs = stmt.executeQuery(); + + while(rs.next()){ + Pet pet = new Pet( + rs.getInt(1), + rs.getString(2), + rs.getString(3), + rs.getString(4), + rs.getInt(5), + rs.getString(6), + rs.getDouble(7) + ); + pets.add(pet); + } + + conn.close(); + return pets; + } + + public static int insertPet(Pet pet) throws SQLException { + int numRows = 0; + + Connection conn = ConnectionDB.getConnection(); + String sql = "INSERT INTO pet (petId, petName, petSpecies, petBreed, petAge, petStatus, petPrice)" + + " VALUES (?, ?, ?, ?, ?, ?, ?)"; + + PreparedStatement stmt = conn.prepareStatement(sql); + stmt.setInt(1, pet.getPetId()); + stmt.setString(2, pet.getPetName()); + stmt.setString(3, pet.getPetSpecies()); + stmt.setString(4, pet.getPetBreed()); + stmt.setInt(5, pet.getPetAge()); + stmt.setString(6, pet.getPetStatus()); + stmt.setDouble(7, pet.getPetPrice()); + + numRows = stmt.executeUpdate(); + conn.close(); + + return numRows; + } + + public static int updatePet(int petId, Pet pet) throws SQLException { + int numRows = 0; + + Connection conn = ConnectionDB.getConnection(); + String sql = "UPDATE pet SET " + + " petName = ?, " + + " petSpecies = ?, " + + " petBreed = ?, " + + " petAge = ?, " + + " petStatus = ?, " + + " petPrice = ? " + + " WHERE petId = ?"; + PreparedStatement stmt = conn.prepareStatement(sql); + stmt.setString(1, pet.getPetName()); + stmt.setString(2, pet.getPetSpecies()); + stmt.setString(3, pet.getPetBreed()); + stmt.setInt(4, pet.getPetAge()); + stmt.setString(5, pet.getPetStatus()); + stmt.setDouble(6, pet.getPetPrice()); + stmt.setInt(7, petId); + + numRows = stmt.executeUpdate(); + conn.close(); + + return numRows; + } + + public static int deletePet(int petId) throws SQLException { + int numRows = 0; + Connection conn = ConnectionDB.getConnection(); + + String sql = "DELETE FROM pet WHERE petId = ?"; + PreparedStatement stmt = conn.prepareStatement(sql); + + stmt.setInt(1, petId); + + numRows = stmt.executeUpdate(); + conn.close(); + + return numRows; + } +} diff --git a/src/main/java/org/example/petshopdesktop/models/Pet.java b/src/main/java/org/example/petshopdesktop/models/Pet.java new file mode 100644 index 00000000..e1f2e3fb --- /dev/null +++ b/src/main/java/org/example/petshopdesktop/models/Pet.java @@ -0,0 +1,109 @@ +package org.example.petshopdesktop.models; + +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleStringProperty; + +public class Pet { + private SimpleIntegerProperty petId; + private SimpleStringProperty petName; + private SimpleStringProperty petSpecies; + private SimpleStringProperty petBreed; + private SimpleIntegerProperty petAge; + private SimpleStringProperty petStatus; + private SimpleDoubleProperty petPrice; + + public Pet(int petId, String petName, String petSpecies, String petBreed, int petAge, String petStatus, double petPrice) { + this.petId = new SimpleIntegerProperty(petId); + this.petName = new SimpleStringProperty(petName); + this.petSpecies = new SimpleStringProperty(petSpecies); + this.petBreed = new SimpleStringProperty(petBreed); + this.petAge = new SimpleIntegerProperty(petAge); + this.petStatus = new SimpleStringProperty(petStatus); + this.petPrice = new SimpleDoubleProperty(petPrice); + } + + public int getPetId() { + return petId.get(); + } + + public void setPetId(int petId) { + this.petId.set(petId); + } + + public SimpleIntegerProperty petIdProperty() { + return petId; + } + + public String getPetName() { + return petName.get(); + } + + public void setPetName(String petName) { + this.petName.set(petName); + } + + public SimpleStringProperty petNameProperty() { + return petName; + } + + public String getPetSpecies() { + return petSpecies.get(); + } + + public void setPetSpecies(String petSpecies) { + this.petSpecies.set(petSpecies); + } + + public SimpleStringProperty petSpeciesProperty() { + return petSpecies; + } + + public String getPetBreed() { + return petBreed.get(); + } + + public void setPetBreed(String petBreed) { + this.petBreed.set(petBreed); + } + + public SimpleStringProperty petBreedProperty() { + return petBreed; + } + + public int getPetAge() { + return petAge.get(); + } + + public void setPetAge(int petAge) { + this.petAge.set(petAge); + } + + public SimpleIntegerProperty petAgeProperty() { + return petAge; + } + + public String getPetStatus() { + return petStatus.get(); + } + + public void setPetStatus(String petStatus) { + this.petStatus.set(petStatus); + } + + public SimpleStringProperty petStatusProperty() { + return petStatus; + } + + public double getPetPrice() { + return petPrice.get(); + } + + public void setPetPrice(double petPrice) { + this.petPrice.set(petPrice); + } + + public SimpleDoubleProperty petPriceProperty() { + return petPrice; + } +} From a12af3f0ee9dee7c041883e9f3a21dd1d321a285 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sat, 21 Feb 2026 13:27:50 -0700 Subject: [PATCH 3/4] Implement RBAC with login screen for admin and staff roles - Add Role enum (ADMIN, STAFF) and UserSession singleton - Add User model and UserDB with SHA2-256 authentication - Add login screen (login-view.fxml + LoginController) - Update main-layout-view.fxml: dynamic username/role labels, logout button - Update MainLayoutController: hide Inventory/Suppliers/Product-Suppliers for STAFF, show username/role, handle logout - Update PetShopApplication to start with login screen and initialize users table - Update module-info.java to open/export auth package --- src/main/java/module-info.java | 2 + .../petshopdesktop/PetShopApplication.java | 10 ++- .../org/example/petshopdesktop/auth/Role.java | 6 ++ .../petshopdesktop/auth/UserSession.java | 44 ++++++++++ .../controllers/LoginController.java | 67 ++++++++++++++++ .../controllers/MainLayoutController.java | 45 +++++++++++ .../petshopdesktop/database/UserDB.java | 71 ++++++++++++++++ .../example/petshopdesktop/models/User.java | 27 +++++++ .../example/petshopdesktop/login-view.fxml | 80 +++++++++++++++++++ .../petshopdesktop/main-layout-view.fxml | 15 +++- 10 files changed, 363 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/example/petshopdesktop/auth/Role.java create mode 100644 src/main/java/org/example/petshopdesktop/auth/UserSession.java create mode 100644 src/main/java/org/example/petshopdesktop/controllers/LoginController.java create mode 100644 src/main/java/org/example/petshopdesktop/database/UserDB.java create mode 100644 src/main/java/org/example/petshopdesktop/models/User.java create mode 100644 src/main/resources/org/example/petshopdesktop/login-view.fxml diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 7459e876..d1f07dd9 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -8,7 +8,9 @@ module org.example.petshopdesktop { opens org.example.petshopdesktop to javafx.fxml; opens org.example.petshopdesktop.controllers.dialogcontrollers to javafx.fxml; opens org.example.petshopdesktop.controllers to javafx.fxml; + opens org.example.petshopdesktop.auth to javafx.fxml; exports org.example.petshopdesktop; exports org.example.petshopdesktop.controllers; + exports org.example.petshopdesktop.auth; } \ No newline at end of file diff --git a/src/main/java/org/example/petshopdesktop/PetShopApplication.java b/src/main/java/org/example/petshopdesktop/PetShopApplication.java index 8a8fcd65..06def37b 100644 --- a/src/main/java/org/example/petshopdesktop/PetShopApplication.java +++ b/src/main/java/org/example/petshopdesktop/PetShopApplication.java @@ -4,15 +4,21 @@ import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.stage.Stage; +import org.example.petshopdesktop.database.UserDB; import java.io.IOException; public class PetShopApplication extends Application { @Override public void start(Stage stage) throws IOException { - FXMLLoader fxmlLoader = new FXMLLoader(PetShopApplication.class.getResource("main-layout-view.fxml")); + try { + UserDB.initializeTable(); + } catch (Exception e) { + System.err.println("Warning: could not initialize users table: " + e.getMessage()); + } + FXMLLoader fxmlLoader = new FXMLLoader(PetShopApplication.class.getResource("login-view.fxml")); Scene scene = new Scene(fxmlLoader.load()); - stage.setTitle("Pet Shop Manager"); + stage.setTitle("Pet Shop Manager - Login"); stage.setScene(scene); stage.show(); } diff --git a/src/main/java/org/example/petshopdesktop/auth/Role.java b/src/main/java/org/example/petshopdesktop/auth/Role.java new file mode 100644 index 00000000..64f38459 --- /dev/null +++ b/src/main/java/org/example/petshopdesktop/auth/Role.java @@ -0,0 +1,6 @@ +package org.example.petshopdesktop.auth; + +public enum Role { + ADMIN, + STAFF +} diff --git a/src/main/java/org/example/petshopdesktop/auth/UserSession.java b/src/main/java/org/example/petshopdesktop/auth/UserSession.java new file mode 100644 index 00000000..748c9546 --- /dev/null +++ b/src/main/java/org/example/petshopdesktop/auth/UserSession.java @@ -0,0 +1,44 @@ +package org.example.petshopdesktop.auth; + +public class UserSession { + + private static UserSession instance; + + private String username; + private Role role; + + private UserSession() {} + + public static UserSession getInstance() { + if (instance == null) { + instance = new UserSession(); + } + return instance; + } + + public void login(String username, Role role) { + this.username = username; + this.role = role; + } + + public void logout() { + this.username = null; + this.role = null; + } + + public String getUsername() { + return username; + } + + public Role getRole() { + return role; + } + + public boolean isAdmin() { + return Role.ADMIN.equals(role); + } + + public boolean isLoggedIn() { + return username != null && role != null; + } +} diff --git a/src/main/java/org/example/petshopdesktop/controllers/LoginController.java b/src/main/java/org/example/petshopdesktop/controllers/LoginController.java new file mode 100644 index 00000000..43bb3334 --- /dev/null +++ b/src/main/java/org/example/petshopdesktop/controllers/LoginController.java @@ -0,0 +1,67 @@ +package org.example.petshopdesktop.controllers; + +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.stage.Stage; +import org.example.petshopdesktop.auth.UserSession; +import org.example.petshopdesktop.database.UserDB; +import org.example.petshopdesktop.models.User; + +import java.sql.SQLException; + +public class LoginController { + + @FXML + private TextField txtUsername; + + @FXML + private PasswordField txtPassword; + + @FXML + private Label lblError; + + @FXML + void btnLoginClicked(ActionEvent event) { + String username = txtUsername.getText().trim(); + String password = txtPassword.getText(); + + if (username.isEmpty() || password.isEmpty()) { + lblError.setText("Please enter username and password."); + return; + } + + try { + User user = UserDB.authenticate(username, password); + if (user == null) { + lblError.setText("Invalid username or password."); + txtPassword.clear(); + return; + } + + UserSession.getInstance().login(user.getUsername(), user.getRole()); + openMainLayout(); + + } catch (SQLException e) { + lblError.setText("Database error: " + e.getMessage()); + } + } + + private void openMainLayout() { + try { + FXMLLoader loader = new FXMLLoader( + getClass().getResource("/org/example/petshopdesktop/main-layout-view.fxml")); + Scene scene = new Scene(loader.load()); + Stage stage = (Stage) txtUsername.getScene().getWindow(); + stage.setScene(scene); + stage.setTitle("Pet Shop Manager"); + } catch (Exception e) { + lblError.setText("Error loading application: " + e.getMessage()); + e.printStackTrace(); + } + } +} diff --git a/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java b/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java index 431d38d4..3584a0cc 100644 --- a/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java +++ b/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java @@ -4,8 +4,12 @@ import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; +import javafx.scene.Scene; import javafx.scene.control.Button; +import javafx.scene.control.Label; import javafx.scene.layout.StackPane; +import javafx.stage.Stage; +import org.example.petshopdesktop.auth.UserSession; public class MainLayoutController { @@ -18,6 +22,9 @@ public class MainLayoutController { @FXML private Button btnInventory; + @FXML + private Button btnLogout; + @FXML private Button btnPets; @@ -36,6 +43,12 @@ public class MainLayoutController { @FXML private Button btnSuppliers; + @FXML + private Label lblUsername; + + @FXML + private Label lblRole; + @FXML private StackPane spContentArea; @@ -93,11 +106,43 @@ public class MainLayoutController { updateButtons(btnSuppliers); } + @FXML + void btnLogoutClicked(ActionEvent event) { + UserSession.getInstance().logout(); + try { + FXMLLoader loader = new FXMLLoader( + getClass().getResource("/org/example/petshopdesktop/login-view.fxml")); + Scene scene = new Scene(loader.load()); + Stage stage = (Stage) btnLogout.getScene().getWindow(); + stage.setScene(scene); + stage.setTitle("Pet Shop Manager - Login"); + } catch (Exception e) { + System.err.println("Error loading login view: " + e.getMessage()); + e.printStackTrace(); + } + } + @FXML public void initialize() { + applyRBAC(); loadView("pet-view.fxml"); } + private void applyRBAC() { + UserSession session = UserSession.getInstance(); + + lblUsername.setText(session.getUsername()); + lblRole.setText(session.getRole().toString()); + + boolean isAdmin = session.isAdmin(); + btnInventory.setVisible(isAdmin); + btnInventory.setManaged(isAdmin); + btnSuppliers.setVisible(isAdmin); + btnSuppliers.setManaged(isAdmin); + btnProductSuppliers.setVisible(isAdmin); + btnProductSuppliers.setManaged(isAdmin); + } + /** * Load a view when a button is clicked on the navigation * @param fxmlFile the fxmlFile name to be loaded diff --git a/src/main/java/org/example/petshopdesktop/database/UserDB.java b/src/main/java/org/example/petshopdesktop/database/UserDB.java new file mode 100644 index 00000000..8909d211 --- /dev/null +++ b/src/main/java/org/example/petshopdesktop/database/UserDB.java @@ -0,0 +1,71 @@ +package org.example.petshopdesktop.database; + +import org.example.petshopdesktop.auth.Role; +import org.example.petshopdesktop.models.User; + +import java.sql.*; + +public class UserDB { + + /** + * Authenticate a user by username and password. + * Passwords are stored as SHA-256 hex digests in the database. + * + * @param username the username to authenticate + * @param password the plaintext password + * @return the User if credentials are valid, or null if authentication fails + */ + public static User authenticate(String username, String password) throws SQLException { + String sql = "SELECT user_id, username, role FROM users " + + "WHERE username = ? AND password_hash = SHA2(?, 256)"; + + try (Connection conn = ConnectionDB.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setString(1, username); + ps.setString(2, password); + + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + int userId = rs.getInt("user_id"); + String uname = rs.getString("username"); + Role role = Role.valueOf(rs.getString("role").toUpperCase()); + return new User(userId, uname, role); + } + } + } + return null; + } + + /** + * Create the users table and seed default admin/staff accounts if they do not exist. + * Passwords are stored as SHA2-256 hashes. + */ + public static void initializeTable() throws SQLException { + String createTable = """ + CREATE TABLE IF NOT EXISTS users ( + user_id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(100) NOT NULL UNIQUE, + password_hash CHAR(64) NOT NULL, + role ENUM('ADMIN','STAFF') NOT NULL + ) + """; + + String seedAdmin = """ + INSERT IGNORE INTO users (username, password_hash, role) + VALUES ('admin', SHA2('admin123', 256), 'ADMIN') + """; + + String seedStaff = """ + INSERT IGNORE INTO users (username, password_hash, role) + VALUES ('staff', SHA2('staff123', 256), 'STAFF') + """; + + try (Connection conn = ConnectionDB.getConnection(); + Statement st = conn.createStatement()) { + st.executeUpdate(createTable); + st.executeUpdate(seedAdmin); + st.executeUpdate(seedStaff); + } + } +} diff --git a/src/main/java/org/example/petshopdesktop/models/User.java b/src/main/java/org/example/petshopdesktop/models/User.java new file mode 100644 index 00000000..02089be4 --- /dev/null +++ b/src/main/java/org/example/petshopdesktop/models/User.java @@ -0,0 +1,27 @@ +package org.example.petshopdesktop.models; + +import org.example.petshopdesktop.auth.Role; + +public class User { + private int userId; + private String username; + private Role role; + + public User(int userId, String username, Role role) { + this.userId = userId; + this.username = username; + this.role = role; + } + + public int getUserId() { + return userId; + } + + public String getUsername() { + return username; + } + + public Role getRole() { + return role; + } +} diff --git a/src/main/resources/org/example/petshopdesktop/login-view.fxml b/src/main/resources/org/example/petshopdesktop/login-view.fxml new file mode 100644 index 00000000..65199c95 --- /dev/null +++ b/src/main/resources/org/example/petshopdesktop/login-view.fxml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml b/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml index 9579dff6..983d07a9 100644 --- a/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml +++ b/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml @@ -16,12 +16,12 @@ - From bbf06456f5b12c4a35b02c88681116e0957fd002 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 22 Feb 2026 17:39:42 -0700 Subject: [PATCH 4/4] comments --- .../org/example/petshopdesktop/auth/Role.java | 7 +++++++ .../example/petshopdesktop/auth/UserSession.java | 15 +++++++++++++++ .../controllers/LoginController.java | 9 +++++++++ .../controllers/MainLayoutController.java | 13 +++++++++++++ .../example/petshopdesktop/database/UserDB.java | 9 +++++++++ 5 files changed, 53 insertions(+) diff --git a/src/main/java/org/example/petshopdesktop/auth/Role.java b/src/main/java/org/example/petshopdesktop/auth/Role.java index 64f38459..e631ddf7 100644 --- a/src/main/java/org/example/petshopdesktop/auth/Role.java +++ b/src/main/java/org/example/petshopdesktop/auth/Role.java @@ -1,6 +1,13 @@ package org.example.petshopdesktop.auth; +/* +Petshop Desktop +Purpose: Application role definitions used by session state and role based access control. +*/ public enum Role { + // Administrative access, includes system management screens. ADMIN, + + // Staff access, limited to day to day operational screens. STAFF } diff --git a/src/main/java/org/example/petshopdesktop/auth/UserSession.java b/src/main/java/org/example/petshopdesktop/auth/UserSession.java index 748c9546..2cc4538a 100644 --- a/src/main/java/org/example/petshopdesktop/auth/UserSession.java +++ b/src/main/java/org/example/petshopdesktop/auth/UserSession.java @@ -1,14 +1,24 @@ package org.example.petshopdesktop.auth; +/* +Petshop Desktop +Purpose: In memory session state for the authenticated user. +Notes: Session is process local and cleared on logout or application restart. +*/ public class UserSession { + // Singleton instance used to share session state across controllers. private static UserSession instance; + // Current authenticated username, null when logged out. private String username; + + // Current authenticated role, null when logged out. private Role role; private UserSession() {} + // Lazily initialised singleton accessor. public static UserSession getInstance() { if (instance == null) { instance = new UserSession(); @@ -16,11 +26,13 @@ public class UserSession { return instance; } + // Stores identity and role for the active session. public void login(String username, Role role) { this.username = username; this.role = role; } + // Clears session state and returns the application to an unauthenticated state. public void logout() { this.username = null; this.role = null; @@ -34,10 +46,13 @@ public class UserSession { return role; } + // Convenience check for administrative privileges. + // Role.ADMIN.equals(role) remains safe when role is null. public boolean isAdmin() { return Role.ADMIN.equals(role); } + // Session is considered active only when both username and role are set. public boolean isLoggedIn() { return username != null && role != null; } diff --git a/src/main/java/org/example/petshopdesktop/controllers/LoginController.java b/src/main/java/org/example/petshopdesktop/controllers/LoginController.java index 43bb3334..440c5686 100644 --- a/src/main/java/org/example/petshopdesktop/controllers/LoginController.java +++ b/src/main/java/org/example/petshopdesktop/controllers/LoginController.java @@ -14,6 +14,10 @@ import org.example.petshopdesktop.models.User; import java.sql.SQLException; +/* +Petshop Desktop +Purpose: Authentication controller responsible for validating credentials and initialising the user session. +*/ public class LoginController { @FXML @@ -27,15 +31,18 @@ public class LoginController { @FXML void btnLoginClicked(ActionEvent event) { + // Input normalisation keeps authentication behaviour consistent. String username = txtUsername.getText().trim(); String password = txtPassword.getText(); + // Basic validation to avoid unnecessary database calls. if (username.isEmpty() || password.isEmpty()) { lblError.setText("Please enter username and password."); return; } try { + // Credential verification returns a fully populated User on success. User user = UserDB.authenticate(username, password); if (user == null) { lblError.setText("Invalid username or password."); @@ -43,6 +50,7 @@ public class LoginController { return; } + // Session state is stored in memory for use by controllers and UI RBAC. UserSession.getInstance().login(user.getUsername(), user.getRole()); openMainLayout(); @@ -53,6 +61,7 @@ public class LoginController { private void openMainLayout() { try { + // View transition into the post login application shell. FXMLLoader loader = new FXMLLoader( getClass().getResource("/org/example/petshopdesktop/main-layout-view.fxml")); Scene scene = new Scene(loader.load()); diff --git a/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java b/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java index 3584a0cc..a910432e 100644 --- a/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java +++ b/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java @@ -11,6 +11,10 @@ import javafx.scene.layout.StackPane; import javafx.stage.Stage; import org.example.petshopdesktop.auth.UserSession; +/* +Petshop Desktop +Purpose: Main application shell controller, includes navigation and UI level role based access control. +*/ public class MainLayoutController { @FXML @@ -108,6 +112,7 @@ public class MainLayoutController { @FXML void btnLogoutClicked(ActionEvent event) { + // Logout clears session state before returning to the login view. UserSession.getInstance().logout(); try { FXMLLoader loader = new FXMLLoader( @@ -124,16 +129,22 @@ public class MainLayoutController { @FXML public void initialize() { + // RBAC state is applied once during initial layout load. applyRBAC(); + + // Default landing view after successful authentication. loadView("pet-view.fxml"); } private void applyRBAC() { UserSession session = UserSession.getInstance(); + // Session identity is displayed in the header for clarity and auditing. lblUsername.setText(session.getUsername()); lblRole.setText(session.getRole().toString()); + // UI level RBAC hides admin only navigation entries for non admin users. + // setManaged(false) removes the node from layout calculations to avoid empty spacing. boolean isAdmin = session.isAdmin(); btnInventory.setVisible(isAdmin); btnInventory.setManaged(isAdmin); @@ -141,6 +152,8 @@ public class MainLayoutController { btnSuppliers.setManaged(isAdmin); btnProductSuppliers.setVisible(isAdmin); btnProductSuppliers.setManaged(isAdmin); + + // Privileged operations should still be enforced within the relevant controllers and database methods. } /** diff --git a/src/main/java/org/example/petshopdesktop/database/UserDB.java b/src/main/java/org/example/petshopdesktop/database/UserDB.java index 8909d211..3b5f78b4 100644 --- a/src/main/java/org/example/petshopdesktop/database/UserDB.java +++ b/src/main/java/org/example/petshopdesktop/database/UserDB.java @@ -5,6 +5,10 @@ import org.example.petshopdesktop.models.User; import java.sql.*; +/* +Petshop Desktop +Purpose: User authentication and role lookup against the users table. +*/ public class UserDB { /** @@ -29,7 +33,11 @@ public class UserDB { if (rs.next()) { int userId = rs.getInt("user_id"); String uname = rs.getString("username"); + + // Role values are stored in the database as strings and normalised to match the enum. + // Table constraints limit role values, Role.valueOf is expected to be safe under normal operation. Role role = Role.valueOf(rs.getString("role").toUpperCase()); + return new User(userId, uname, role); } } @@ -51,6 +59,7 @@ public class UserDB { ) """; + // Default accounts support initial development and testing, credentials should be rotated or removed for deployment. String seedAdmin = """ INSERT IGNORE INTO users (username, password_hash, role) VALUES ('admin', SHA2('admin123', 256), 'ADMIN')