diff --git a/README.md b/README.md
index d20aaf9..7071577 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
+[](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")
+ );
+ }
+}