diff --git a/README.md b/README.md index d20aaf9..7071577 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/339Lr3BJ) ### How the tests work (and Docker requirement) This project ships with an end‑to‑end CLI integration test suite that uses Testcontainers to spin up a temporary MySQL database. diff --git a/pom.xml b/pom.xml index 002ff66..ecefccc 100644 --- a/pom.xml +++ b/pom.xml @@ -51,6 +51,21 @@ mysql 1.21.3 + + org.apache.commons + commons-dbcp2 + 2.13.0 + + + org.slf4j + slf4j-nop + 2.0.17 + + + org.hibernate.orm + hibernate-core + 7.1.11.Final + diff --git a/src/main/java/com/example/DataSource.java b/src/main/java/com/example/DataSource.java new file mode 100644 index 0000000..1c27cd1 --- /dev/null +++ b/src/main/java/com/example/DataSource.java @@ -0,0 +1,50 @@ +package com.example; + +import org.apache.commons.dbcp2.BasicDataSource; +import java.sql.Connection; +import java.sql.SQLException; + +public class DataSource { + + private static final BasicDataSource source = new BasicDataSource(); + private static final String jdbcUrl = resolveConfig("APP_JDBC_URL", "APP_JDBC_URL"); + private static final String dbUser = resolveConfig("APP_DB_USER", "APP_DB_USER"); + private static final String dbPass = resolveConfig("APP_DB_PASS", "APP_DB_PASS"); + + static { + source.setUrl(jdbcUrl); + source.setUsername(dbUser); + source.setPassword(dbPass); + source.addConnectionProperty("hibernate.hbm2ddl.auto", "update"); + source.setDefaultAutoCommit(true); + } + + public static Connection getConnection() throws SQLException { + return source.getConnection(); + } + + public static String getJdbcUrl(){ + return jdbcUrl; + } + + public static String getDbUser(){ + return dbUser; + } + + public static String getDbPass(){ + return dbPass; + } + + + /** + * Reads configuration with precedence: Java system property first, then environment variable. + * Returns trimmed value or null if neither source provides a non-empty value. + */ + private static String resolveConfig(String propertyKey, String envKey) { + String v = System.getProperty(propertyKey); + if (v == null || v.trim().isEmpty()) { + v = System.getenv(envKey); + } + return (v == null || v.trim().isEmpty()) ? null : v.trim(); + } +} diff --git a/src/main/java/com/example/Main.java b/src/main/java/com/example/Main.java index 6dc6fbd..ef2e841 100644 --- a/src/main/java/com/example/Main.java +++ b/src/main/java/com/example/Main.java @@ -1,36 +1,138 @@ package com.example; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; +import repositories.AccountRepo; +import repositories.MoonMissionRepo; + +import java.sql.*; import java.util.Arrays; +import java.util.Scanner; public class Main { - static void main(String[] args) { + private final Scanner scanner = new Scanner(System.in); + + static void main(String[] args) throws SQLException { if (isDevMode(args)) { DevDatabaseInitializer.start(); } new Main().run(); + } public void run() { - // Resolve DB settings with precedence: System properties -> Environment variables - String jdbcUrl = resolveConfig("APP_JDBC_URL", "APP_JDBC_URL"); - String dbUser = resolveConfig("APP_DB_USER", "APP_DB_USER"); - String dbPass = resolveConfig("APP_DB_PASS", "APP_DB_PASS"); - - if (jdbcUrl == null || dbUser == null || dbPass == null) { + if (DataSource.getJdbcUrl() == null || DataSource.getDbUser() == null || DataSource.getDbPass() == null) { throw new IllegalStateException( "Missing DB configuration. Provide APP_JDBC_URL, APP_DB_USER, APP_DB_PASS " + "as system properties (-Dkey=value) or environment variables."); } - try (Connection connection = DriverManager.getConnection(jdbcUrl, dbUser, dbPass)) { - } catch (SQLException e) { - throw new RuntimeException(e); + AccountRepo accountRepo = new AccountRepo(); + MoonMissionRepo moonMissionRepo = new MoonMissionRepo(); + + //Only run program if sign-in is valid + boolean executeProgram = validateSignIn(accountRepo); + while (executeProgram) { + + displayMenu(); + System.out.print("Your choice: "); + String choice = scanner.nextLine().trim(); + + switch (choice) { + case "0" -> executeProgram = false; + case "1" -> displayAllMoonMissions(moonMissionRepo); + case "2" -> getMissionFromID(moonMissionRepo); + case "3" -> displayMissionsForAYear(moonMissionRepo); + case "4" -> createAccount(accountRepo); + case "5" -> updatePassword(accountRepo); + case "6" -> deleteAccount(accountRepo); + default -> System.out.println("Invalid entry, please try again."); + } + } + } + + public void createAccount(AccountRepo accountRepo) { + System.out.print("Enter your first name: "); + String firstName = scanner.nextLine().trim(); + if(firstName.isBlank() || firstName.length() < 3){ + System.out.println("Error. First name must be at least 3 characters."); + return; + } + + System.out.print("Enter your lastname: "); + String lastName = scanner.nextLine().trim(); + if(lastName.isBlank() || lastName.length() < 3){ + System.out.println("Error. Lastname must be at least 3 characters."); + return; + } + + System.out.print("Enter your social security number (yymmdd-nnnn): "); + String ssn = scanner.nextLine().trim(); + if(!ssn.matches("^\\d{6}-\\d{4}")){ + System.out.println("Error! Must be in format yymmdd-xxxx"); + return; } - //Todo: Starting point for your code + + System.out.print("Choose a password: "); + String password = scanner.nextLine().trim(); + if(!validPassword(password)){ + return; + } + + accountRepo.createAccount(firstName, lastName, password, ssn); + } + + private void deleteAccount(AccountRepo accountRepo) { + System.out.print("Enter userId for the account that you would like to delete: "); + int userId = Integer.parseInt(scanner.nextLine().trim()); + + accountRepo.deleteAccount(userId); + } + + private boolean validPassword(String password){ + if(password.isBlank() || password.length() < 6){ + System.out.println("Error. Password must be at least 6 characters long."); + return false; + } + return true; + } + + private void updatePassword(AccountRepo accountRepo) { + System.out.print("Enter user id: "); + int userId = Integer.parseInt(scanner.nextLine().trim()); + + System.out.print("Choose a new password: "); + String password = scanner.nextLine().trim(); + if(!validPassword(password)){ + return; + } + + accountRepo.updatePassword(userId, password); + } + + private void displayMissionsForAYear(MoonMissionRepo moonMissionRepo) { + System.out.print("Enter a year: "); + String year = scanner.nextLine().trim(); + if(!year.matches("^(19|20)\\d{2}$")){ + System.out.println("Invalid year"); + return; + } + + moonMissionRepo.allMissionsConductedInYear(year); + } + + private void getMissionFromID(MoonMissionRepo moonMissionRepo) { + System.out.print("Enter mission-id: "); + int id = Integer.parseInt(scanner.nextLine().trim()); + + moonMissionRepo.getMissionFromID(id); + System.out.println(); + } + + private void displayAllMoonMissions(MoonMissionRepo moonMissionRepo) { + String column = "spacecraft"; + System.out.println("-----Spacecrafts-----\n"); + moonMissionRepo.displayColumn(column); + System.out.println(); } /** @@ -48,15 +150,35 @@ private static boolean isDevMode(String[] args) { return Arrays.asList(args).contains("--dev"); //Argument --dev } - /** - * Reads configuration with precedence: Java system property first, then environment variable. - * Returns trimmed value or null if neither source provides a non-empty value. - */ - private static String resolveConfig(String propertyKey, String envKey) { - String v = System.getProperty(propertyKey); - if (v == null || v.trim().isEmpty()) { - v = System.getenv(envKey); + private boolean validateSignIn(AccountRepo accountRepo){ + while (true) { + System.out.print("Username: "); + String username = scanner.nextLine().trim(); + System.out.print("Password: "); + String password = scanner.nextLine().trim(); + + if(accountRepo.validateLogIn(username, password)) + return true; + + System.out.print("Invalid username or password. Press 0 to exit or any other key to return to sign in: "); + String choice = scanner.nextLine().trim(); + System.out.println(); + if (choice != null && choice.trim().equals("0")) + return false; } - return (v == null || v.trim().isEmpty()) ? null : v.trim(); } -} + + private static void displayMenu() { + System.out.println(" MENU\n" + + "==================================\n" + + "\n" + + "1) List moon missions\n" + + "2) Get a moon mission by mission_id\n" + + "3) Count missions for a given year\n" + + "4) Create an account\n" + + "5) Update an account password\n" + + "6) Delete an account\n" + + "0) Exit\n" + + " "); + } +} \ No newline at end of file diff --git a/src/main/java/repositories/AccountInter.java b/src/main/java/repositories/AccountInter.java new file mode 100644 index 0000000..9ea1eb4 --- /dev/null +++ b/src/main/java/repositories/AccountInter.java @@ -0,0 +1,42 @@ +package repositories; + +public interface AccountInter { + + /** + * Validates username and password. + * @param password connected to username. + * @param username of account that will be validated. + * @return true if user exists in database, otherwise return false. + */ + boolean validateLogIn(String username, String password); + + /** + * Creates an account + * + * @param firstName users first name. + * @param lastName users last name. + * @param password user chooses a password. + * @param ssn users social security number. + * Print confirmation if account was successfully created, otherwise print error-message + */ + void createAccount(String firstName, String lastName, String password, String ssn); + + + /** + * Deletes an existing account. + * @param userId primary key used to find account. + * If account is not found print error message, + * otherwise print confirmation + */ + void deleteAccount(int userId); + + /** + * Updates password of existing account + * @param userID unique key used to find account + * @param password new password + * Prints confirmation when password successfully changed, + * otherwise print error-message + */ + void updatePassword(int userID, String password); + +} diff --git a/src/main/java/repositories/AccountRepo.java b/src/main/java/repositories/AccountRepo.java new file mode 100644 index 0000000..d40abe5 --- /dev/null +++ b/src/main/java/repositories/AccountRepo.java @@ -0,0 +1,96 @@ +package repositories; + +import com.example.DataSource; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class AccountRepo implements AccountInter{ + + @Override + public boolean validateLogIn(String username, String password) { + String userQuery = "select * from account where name = ? and password = ?"; + + try (Connection con = DataSource.getConnection(); + PreparedStatement pS = con.prepareStatement(userQuery)) { + + pS.setString(1, username); + pS.setString(2, password); + ResultSet result = pS.executeQuery(); + return result.next(); + + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + public void createAccount(String firstName, String lastName, String password, String ssn) { + String addUser = "insert into account (name, password, first_name, last_name, ssn) " + + "values (?, ?, ?, ?, ?)"; + + try (Connection con = DataSource.getConnection(); + PreparedStatement pS = con.prepareStatement(addUser)) { + + String username = firstName.substring(0, 3) + lastName.substring(0, 3); + pS.setString(1, username); + pS.setString(2, password); + pS.setString(3, firstName); + pS.setString(4, lastName); + pS.setString(5, ssn); + int rowsAffected = pS.executeUpdate(); + + if(rowsAffected == 0) + System.out.println("Error. Something went wrong when creating the account."); + else + System.out.println("Account with username " + username + " successfully created."); + + + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + public void deleteAccount(int userId) { + String deleteUser = "delete from account where user_id = ?"; + + try (Connection con = DataSource.getConnection(); + PreparedStatement pS = con.prepareStatement(deleteUser)) { + + pS.setInt(1, userId); + int rowsDeleted = pS.executeUpdate(); + + if (rowsDeleted > 0) + System.out.println("Account with id " + userId + " successfully deleted"); + else + System.out.println("Error! No account found with id: " + userId); + + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + public void updatePassword(int userID, String password) { + String updatePassword = "update account set password = ? where user_id = ?"; + + try (Connection con = DataSource.getConnection(); + PreparedStatement pS = con.prepareStatement(updatePassword)) { + + pS.setString(1, password); + pS.setInt(2, userID); + int rowsUpdated = pS.executeUpdate(); + + if (rowsUpdated > 0) + System.out.println("Password successfully updated."); + else + System.out.println("Error! Something went wrong when updating the password."); + + } catch (SQLException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/repositories/MoonMissionInter.java b/src/main/java/repositories/MoonMissionInter.java new file mode 100644 index 0000000..e1781d5 --- /dev/null +++ b/src/main/java/repositories/MoonMissionInter.java @@ -0,0 +1,23 @@ +package repositories; + +public interface MoonMissionInter { + /** + * Display all information from chosen column, if * then show all columns + * @param columnName the column that you would like to display + */ + void displayColumn(String columnName); + + /** + * Retrieves all information about a mission when you enter an ID + * @param missionID unique for each mission + */ + void getMissionFromID(int missionID); + + /** + * Counts how many missions have been conducted in a year + * @param year the chosen year + * Prints number of missions even if they were 0 + */ + void allMissionsConductedInYear(String year); + +} diff --git a/src/main/java/repositories/MoonMissionRepo.java b/src/main/java/repositories/MoonMissionRepo.java new file mode 100644 index 0000000..417994c --- /dev/null +++ b/src/main/java/repositories/MoonMissionRepo.java @@ -0,0 +1,80 @@ +package repositories; + +import com.example.DataSource; + +import java.sql.*; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +public class MoonMissionRepo implements MoonMissionInter{ + @Override + public void displayColumn(String columnName) { + String query = "select * from moon_mission"; + + try (Connection con = DataSource.getConnection(); + PreparedStatement pS = con.prepareStatement(query)) { + + ResultSet result = pS.executeQuery(); + while (result.next()) { + String name = result.getString(columnName); + System.out.println(name); + } + + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + public void getMissionFromID(int missionId) { + String query = "select * from moon_mission where mission_id = ?"; + + try (Connection con = DataSource.getConnection(); + PreparedStatement pS = con.prepareStatement(query)) { + + pS.setInt(1, missionId); + ResultSet result = pS.executeQuery(); + + if (result.next()) { + ResultSetMetaData meta = result.getMetaData(); + int columns = meta.getColumnCount(); + + System.out.println("\n-------- Mission Details --------\n"); + for (int i = 1; i <= columns; i++) { + System.out.println(meta.getColumnName(i) + ": " + result.getString(i)); + } + + } else { + System.out.println("Could not find mission with id: " + missionId); + } + + } catch (SQLException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + @Override + public void allMissionsConductedInYear(String year) { + String query = "select count(*) from moon_mission where launch_date like ?"; + + try (Connection con = DataSource.getConnection(); + PreparedStatement pS = con.prepareStatement(query)) { + + int numMissions = 0; + pS.setString(1, year.trim() + "%"); + ResultSet result = pS.executeQuery(); + if (result.next()){ + numMissions = result.getInt(1); + } + + System.out.println("There were " + numMissions + " moon missions registered during " + year + ".\n"); + + } catch (SQLException e) { + e.printStackTrace(); + throw new RuntimeException(); + } + } + + +}