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/src/main/java/com/example/Main.java b/src/main/java/com/example/Main.java index 6dc6fbd..7f6a30f 100644 --- a/src/main/java/com/example/Main.java +++ b/src/main/java/com/example/Main.java @@ -1,12 +1,22 @@ package com.example; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; +import com.repo.AccountRepository; +import com.repo.DTO.AccountDTO; +import com.repo.DTO.MissionDTO; +import com.repo.MoonMissionRepository; + +import javax.sql.DataSource; +import java.sql.*; import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.Scanner; +import java.util.regex.Pattern; public class Main { + private final Scanner scanner = new Scanner(System.in); + static void main(String[] args) { if (isDevMode(args)) { DevDatabaseInitializer.start(); @@ -26,13 +36,328 @@ public void run() { "as system properties (-Dkey=value) or environment variables."); } - try (Connection connection = DriverManager.getConnection(jdbcUrl, dbUser, dbPass)) { - } catch (SQLException e) { - throw new RuntimeException(e); + DataSource ds = new SimpleDriverManagerDataSource( + System.getProperty("APP_JDBC_URL"), + System.getProperty("APP_DB_USER"), + System.getProperty("APP_DB_PASS") + ); + + + + AccountRepository ac = new AccountRepository(ds); + MoonMissionRepository mmc = new MoonMissionRepository(ds); + + + + boolean authorized = login(ac); + + + if (!authorized) { + System.out.println("Invalid username or password"); + System.out.println("press 0 to exit"); + while(true){ + String exit = scanner.nextLine().trim(); + if(exit.equals("0")){ + return; + } + } + } + + + while(true) { + int option = promptMenu(); + + switch (option) { + case 1 -> listMissions(mmc); + case 2 -> getMission(mmc); + case 3 -> missionsCountYear(mmc); + case 4 -> createAccount(ac); + case 5 -> updatePassword(ac); + case 6 -> deleteAccount(ac); + case 0 -> { + return; + } + + default -> System.out.println("Invalid choice.\n"); + } + } + + + + } + + /** + * Prompts username and password, checks if the combination is present in accounts + * + * @return true if the name/password combo exists + * false if either name/password isn't present + */ + private boolean login(AccountRepository ac) { + System.out.println("Username: "); + String unm = scanner.nextLine(); + System.out.println("Password: "); + String pw = scanner.nextLine(); + + return ac.matchCredentials(unm, pw); + } + + /** + * Prompts the menu options and returns a user input integer + * @return integer >= 0 + * @see #getValidInt(String) + */ + private int promptMenu(){ + System.out.print("\n" + + "1) List moon missions (prints spacecraft names from `moon_mission`).\n" + + "2) Get a moon mission by mission_id (prints details for that mission).\n" + + "3) Count missions for a given year (prompts: year; prints the number of missions launched that year).\n" + + "4) Create an account (prompts: first name, last name, ssn, password; prints confirmation).\n" + + "5) Update an account password (prompts: user_id, new password; prints confirmation).\n" + + "6) Delete an account (prompts: user_id; prints confirmation).\n" + + "0) Exit.\n"); + + return getValidInt("Enter Choice: "); + } + + + /** + * Lists all spacecraft from the moon_mission table + */ + private void listMissions(MoonMissionRepository mmc){ + List missions = mmc.getAllMissions(); + + for(MissionDTO mission : missions){ + System.out.println(mission.spacecraft()); + } + } + + + /** + * Prompts for a mission ID and prints its data is available + * @see #getValidInt(String) + */ + private void getMission(MoonMissionRepository mmc){ + int id = getValidInt("Mission Id: "); + + Optional mission = mmc.getMissionById(id); + if(mission.isPresent()) { + MissionDTO m = mission.get(); + System.out.println( + "\nSpacecraft: " + m.spacecraft() + + "\nLaunch date: " + m.launchDate() + + "\nCarrier rocket: " + m.carrierRocket() + + "\nOperator: " + m.operator() + + "\nMission type: " + m.missionType() + + "\nOutcome: " + m.outcome()); + } + else { + System.out.println("\nMission not found."); + } + } + + /** + * Prompts for a year and prints how many rows in moon_mission was launched then + * @see #getValidInt(String) + */ + private void missionsCountYear(MoonMissionRepository mmc){ + int year = getValidInt("Mission Year: "); + + System.out.println("\nMissions in " + year + ": " + mmc.missionCount(year)); + + } + + /** + * Gives a flow for creating a new account, asking for First and Last name, SSN and password + * default accountname is assigned if available and promted for if not + * @see #getValidName(String) + * @see #getValidSSN(String) + * @see #getValidPassword(String) + */ + private void createAccount(AccountRepository ac) { + String fn = getValidName("First Name: "); + String ln = getValidName("Last Name: "); + String ssn = getValidSSN("SSN: "); + String pw = getValidPassword("Password: "); + + String accName; //Default accountname is first three letters of first and last name + if(fn.length() <= 3 && ln.length() <=3){ //if both first and last name are 3 or fewer letters + accName = fn + ln;} //accountname is both full combined + + else if (fn.length() <= 3){ //if only first name is 3 or fewer letters + accName = fn + ln.substring(0, 2); //accountname is full first name and first 3 from last name + } + else if(ln.length() <= 3){ //if only last name is 3 or fewer letters + accName = fn.substring(0, 2) + ln; //accountname is first 3 from first name and full last name + } + else{ //if both are longer than 3 accountname follows default pattern + accName = fn.substring(0, 2) + ln.substring(0, 2); + } + + //Check if the accountname exists + Optional nameCheck; + + while(true) { + nameCheck = ac.getAccountByName(accName); + + if(nameCheck.isEmpty()){ //if accountname is available continue + break; + } + else{ + accName = getValidName("Account Name: "); //if not prompt for a new accountname and check again + //todo add help method for accountname + } + } + + //Create and add account + AccountDTO newAccount = new AccountDTO(0, accName, pw, fn, ln, ssn); + + ac.createAccount(newAccount); + + if(ac.accountExists(accName)){ + System.out.println("\nAccount created"); + } + else{ + System.out.println("\nAccount creation failed."); + } + + } + + + /** + * Updates password after prompting for an ID and new password + * @see #getValidInt(String) + * @see #getValidPassword(String) + */ + private void updatePassword (AccountRepository ac) { + int id = getValidInt("User id: "); //todo add check if account is present + + if(ac.accountExists(id)){ + String newPassword = getValidPassword("New password: "); + + ac.updatePassword(id, newPassword); + + if(ac.matchCredentials(ac.getAccountByID(id).get().name(), newPassword)){ + System.out.println("Password updated"); + } + else { + System.out.println("Password update failed."); + } + } + else{ + System.out.println("Account not found."); + } + + } + + /** + * Deletes account after prompting for an ID + * @see #getValidInt(String) + */ + private void deleteAccount(AccountRepository ac) { + int id = getValidInt("User id: "); + + if(ac.accountExists(id)){ + ac.deleteAccount(id); + if(!ac.accountExists(id)){ + System.out.println("Account deleted"); + } + else { + System.out.println("Account deletion failed."); + } + } + else { + System.out.println("Account not found."); + } + } + + /** + * Help method to get a valid and positive integer from input + * @param prompt message to explain what is asked + * @return integer >= 0 + */ + private int getValidInt(String prompt){ + while(true){ + try { + System.out.println("\n" + prompt); + int option = Integer.parseInt(scanner.nextLine()); + + if (option >= 0) { + return option; + } + else { + System.out.println("Please enter a positive integer.\n"); + } + } + catch (NumberFormatException e){ + System.out.println("Please enter a valid integer\n"); + } + } + } + + /** + * Help method to get a valid string that only contains letters from input + * @param prompt message to explain what is asked + * @return Capitalized string with only letters + */ + private String getValidName(String prompt){ + while(true){ + System.out.println("\n" + prompt); + String name = scanner.nextLine().trim(); + + if (name.isBlank()) { + System.out.println("\nCannot be blank"); + } + else if(!Pattern.matches("^[a-zA-Z]+$", name)){ + System.out.println("\nMust only contain letters"); + } + else{ + return name.substring(0, 1).toUpperCase() + name.substring(1).toLowerCase(); + } + } + } + + /** + * Help method to get a correctly formated SSN from input + * @param prompt message to explain what is asked + * @return String with YYMMDD-XXXX formatting + */ + private String getValidSSN(String prompt){ + while(true){ + System.out.println("\n" + prompt); + String ssn = scanner.nextLine().trim(); + + if (ssn.isBlank()) { + System.out.println("\nCannot be blank"); + } + else if(!Pattern.matches("^\\d{6}-\\d{4}$", ssn)){ + System.out.println("\nMust follow pattern YYMMDD-XXXX"); + } + else { + return ssn; + } + } + } + + /** + * Help method to get a valid password from input + * @param prompt message to explain what is asked + * @return string of at least 6 characters + */ + private String getValidPassword(String prompt){ + while(true){ + System.out.println("\n" + prompt); + String pw = scanner.nextLine(); + + if(pw.length() < 6){ + System.out.println("Password must be at least 6 characters"); + } + else{ + return pw; + } } - //Todo: Starting point for your code } + /** * Determines if the application is running in development mode based on system properties, * environment variables, or command-line arguments. diff --git a/src/main/java/com/example/SimpleDriverManagerDataSource.java b/src/main/java/com/example/SimpleDriverManagerDataSource.java new file mode 100644 index 0000000..e0df2f5 --- /dev/null +++ b/src/main/java/com/example/SimpleDriverManagerDataSource.java @@ -0,0 +1,68 @@ +package com.example; + +import javax.sql.DataSource; +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.logging.Logger; + +public class SimpleDriverManagerDataSource implements DataSource { + + private final String url; + private final String username; + private final String password; + + + public SimpleDriverManagerDataSource(String url, String username, String password) { + this.url = url; + this.username = username; + this.password = password; + } + + @Override + public Connection getConnection() throws SQLException { + return DriverManager.getConnection(url, username, password); + } + + @Override + public Connection getConnection(String username, String password) throws SQLException { + return DriverManager.getConnection(url, username, password); + } + + @Override + public PrintWriter getLogWriter() throws SQLException { + return null; + } + + @Override + public void setLogWriter(PrintWriter out) throws SQLException { + + } + + @Override + public void setLoginTimeout(int seconds) throws SQLException { + + } + + @Override + public int getLoginTimeout() throws SQLException { + return 0; + } + + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + return null; + } + + @Override + public T unwrap(Class iface) throws SQLException { + return null; + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return false; + } +} diff --git a/src/main/java/com/repo/AccountRepo.java b/src/main/java/com/repo/AccountRepo.java new file mode 100644 index 0000000..12b6096 --- /dev/null +++ b/src/main/java/com/repo/AccountRepo.java @@ -0,0 +1,28 @@ +package com.repo; + + +import com.repo.DTO.AccountDTO; + +import java.util.List; +import java.util.Optional; + +public interface AccountRepo { + + Optional getAccountByName(String name); + + Optional getAccountByID(int userid); + + boolean accountExists(String name); + + boolean accountExists(int userid); + + boolean matchCredentials(String name, String password); + + void createAccount(AccountDTO account); + + void updatePassword(int userid, String password); + + void deleteAccount(int userid); + + List getAllAccounts(); +} diff --git a/src/main/java/com/repo/AccountRepository.java b/src/main/java/com/repo/AccountRepository.java new file mode 100644 index 0000000..f3f1e5c --- /dev/null +++ b/src/main/java/com/repo/AccountRepository.java @@ -0,0 +1,145 @@ +package com.repo; + +import com.repo.DTO.AccountDTO; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class AccountRepository implements AccountRepo{ + private final DataSource dataSource; + + public AccountRepository(DataSource dataSource) { + this.dataSource = dataSource; + } + + @Override + public Optional getAccountByName(String name) { + try(Connection c = dataSource.getConnection(); + PreparedStatement ps = c.prepareStatement("select * from account where name = ?")){ + ps.setString(1, name); + + ResultSet rs = ps.executeQuery(); + if(rs.next()){ + return Optional.of(mapAccount(rs)); + } + } + catch(SQLException e){ + throw new RuntimeException(e); + } + return Optional.empty(); + } + + @Override + public Optional getAccountByID(int userid) { + try(Connection c = dataSource.getConnection(); + PreparedStatement ps = c.prepareStatement("select * from account where user_id = ?")){ + ps.setInt(1, userid); + + ResultSet rs = ps.executeQuery(); + if(rs.next()){ + return Optional.of(mapAccount(rs)); + } + } + catch(SQLException e){ + throw new RuntimeException(e); + } + return Optional.empty(); + } + + @Override + public boolean accountExists(String name) { + return getAccountByName(name).isPresent(); + } + + @Override + public boolean accountExists(int userid) { + return getAccountByID(userid).isPresent(); + } + + @Override + public boolean matchCredentials(String name, String password) { + Optional account = getAccountByName(name); + + return account.isPresent() && account.get().password().equals(password); + } + + @Override + public void createAccount(AccountDTO account) { + try(Connection c = dataSource.getConnection(); + PreparedStatement ps = c.prepareStatement("insert into account (name, password, first_name, last_name, ssn) values (?, ?, ?, ?, ?)")){ + ps.setString(1, account.name()); + ps.setString(2, account.password()); + ps.setString(3, account.firstName()); + ps.setString(4, account.lastName()); + ps.setString(5, account.ssn()); + + ps.executeUpdate(); + } + catch(SQLException e){ + throw new RuntimeException(e); + } + } + + @Override + public void updatePassword(int userid, String password) { + try(Connection c = dataSource.getConnection(); + PreparedStatement ps = c.prepareStatement("update account set password = ? where user_id = ?")){ + ps.setString(1, password); + ps.setInt(2, userid); + + ps.executeUpdate(); + } + catch(SQLException e){ + throw new RuntimeException(e); + } + } + + @Override + public void deleteAccount(int userid) { + try(Connection c = dataSource.getConnection(); + PreparedStatement ps = c.prepareStatement("delete from account where user_id = ?")){ + ps.setInt(1, userid); + + ps.executeUpdate(); + } + catch(SQLException e){ + throw new RuntimeException(e); + } + } + + @Override + public List getAllAccounts() { + List accounts = new ArrayList<>(); + + try(Connection c = dataSource.getConnection(); + PreparedStatement ps = c.prepareStatement("select * from account")){ + + ResultSet rs = ps.executeQuery(); + while(rs.next()){ + accounts.add(mapAccount(rs)); + } + } + catch(SQLException e){ + throw new RuntimeException(e); + } + return accounts; + } + + private AccountDTO mapAccount(ResultSet rs) throws SQLException { + return new AccountDTO( + rs.getInt("user_id"), + rs.getString("name"), + rs.getString("password"), + rs.getString("first_name"), + rs.getString("last_name"), + rs.getString("ssn") + ); + } + +} diff --git a/src/main/java/com/repo/DTO/AccountDTO.java b/src/main/java/com/repo/DTO/AccountDTO.java new file mode 100644 index 0000000..8849113 --- /dev/null +++ b/src/main/java/com/repo/DTO/AccountDTO.java @@ -0,0 +1,10 @@ +package com.repo.DTO; + +public record AccountDTO ( + int userid, + String name, + String password, + String firstName, + String lastName, + String ssn){ +} diff --git a/src/main/java/com/repo/DTO/MissionDTO.java b/src/main/java/com/repo/DTO/MissionDTO.java new file mode 100644 index 0000000..6c1a2e0 --- /dev/null +++ b/src/main/java/com/repo/DTO/MissionDTO.java @@ -0,0 +1,13 @@ +package com.repo.DTO; + +import java.time.LocalDate; + +public record MissionDTO ( + int missionid, + String spacecraft, + LocalDate launchDate, + String carrierRocket, + String operator, + String missionType, + String outcome){ +} diff --git a/src/main/java/com/repo/MoonMissionRepo.java b/src/main/java/com/repo/MoonMissionRepo.java new file mode 100644 index 0000000..6c7fab8 --- /dev/null +++ b/src/main/java/com/repo/MoonMissionRepo.java @@ -0,0 +1,16 @@ +package com.repo; + + +import com.repo.DTO.MissionDTO; + +import java.util.List; +import java.util.Optional; + +public interface MoonMissionRepo { + + List getAllMissions(); + + Optional getMissionById(int missionid); + + int missionCount(int year); +} diff --git a/src/main/java/com/repo/MoonMissionRepository.java b/src/main/java/com/repo/MoonMissionRepository.java new file mode 100644 index 0000000..98338db --- /dev/null +++ b/src/main/java/com/repo/MoonMissionRepository.java @@ -0,0 +1,84 @@ +package com.repo; + +import com.repo.DTO.MissionDTO; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class MoonMissionRepository implements MoonMissionRepo{ + private final DataSource dataSource; + + public MoonMissionRepository(DataSource dataSource) { + this.dataSource = dataSource; + } + + @Override + public List getAllMissions() { + List missions = new ArrayList<>(); + + try(Connection c = dataSource.getConnection(); + PreparedStatement ps = c.prepareStatement("select * from moon_mission")){ + + ResultSet rs = ps.executeQuery(); + while(rs.next()){ + missions.add(mapMission(rs)); + } + } + catch(SQLException e){ + throw new RuntimeException(e); + } + return missions; + } + + @Override + public Optional getMissionById(int missionid) { + try(Connection c = dataSource.getConnection(); + PreparedStatement ps = c.prepareStatement("select * from moon_mission where missionid = ?")){ + ps.setInt(1, missionid); + + ResultSet rs = ps.executeQuery(); + if(rs.next()){ + return Optional.of(mapMission(rs)); + } + } + catch(SQLException e){ + throw new RuntimeException(e); + } + return Optional.empty(); + } + + @Override + public int missionCount(int year) { + try (Connection c = dataSource.getConnection(); + PreparedStatement ps = c.prepareStatement( + "select count(*) from moon_mission where year(launch_date) = ?")) { + ps.setInt(1, year); + + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + return rs.getInt("count(*)"); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + return 0; + } + + private MissionDTO mapMission(ResultSet rs) throws SQLException { + return new MissionDTO( + rs.getInt("mission_id"), + rs.getString("spacecraft"), + rs.getDate("launch_date").toLocalDate(), + rs.getString("carrier_rocket"), + rs.getString("operator"), + rs.getString("mission_type"), + rs.getString("outcome") + ); + } +}