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/databas-jdbc-Ericthilen.zip b/databas-jdbc-Ericthilen.zip new file mode 100644 index 0000000..f6b8aac Binary files /dev/null and b/databas-jdbc-Ericthilen.zip differ diff --git a/mvnw b/mvnw old mode 100644 new mode 100755 diff --git a/pom.xml b/pom.xml index 002ff66..ae56267 100644 --- a/pom.xml +++ b/pom.xml @@ -51,6 +51,11 @@ mysql 1.21.3 + + org.slf4j + slf4j-nop + 2.0.17 + @@ -70,4 +75,4 @@ - + \ No newline at end of file diff --git a/src/main/java/com/example/AccountRepository.java b/src/main/java/com/example/AccountRepository.java new file mode 100644 index 0000000..20fe9dd --- /dev/null +++ b/src/main/java/com/example/AccountRepository.java @@ -0,0 +1,97 @@ +package com.example; + +import javax.sql.DataSource; +import java.sql.*; + +public class AccountRepository { + private final DataSource ds; + + public AccountRepository(DataSource ds) { + this.ds = ds; + } + + /** Kontrollera login mot name + password */ + public boolean login(String username, String password) throws SQLException { + String sql = "SELECT 1 FROM account WHERE name=? AND password=?"; + try (Connection conn = ds.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, username); + ps.setString(2, password); + try (ResultSet rs = ps.executeQuery()) { + return rs.next(); + } + } + } + + /** Skapa nytt konto och generera unikt username (name). Vid kollision läggs siffersuffix till (t.ex. AngFra1). */ + public String createAccount(String first, String last, String ssn, String password) throws SQLException { + String base = (first == null ? "" : first.trim()); + String sur = (last == null ? "" : last.trim()); + String ssnTrim = (ssn == null ? "" : ssn.trim()); + + String baseName = base.substring(0, Math.min(3, base.length())) + + sur.substring(0, Math.min(3, sur.length())); + + String sql = "INSERT INTO account(name, password, first_name, last_name, ssn) VALUES (?,?,?,?,?)"; + + try (Connection conn = ds.getConnection()) { + conn.setAutoCommit(true); + + String candidate = baseName; + int suffix = 0; + while (true) { + try (PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + ps.setString(1, candidate); + ps.setString(2, password); // medvetet: lösenord trimmas inte + ps.setString(3, base); + ps.setString(4, sur); + ps.setString(5, ssnTrim); + ps.executeUpdate(); + + try (ResultSet keys = ps.getGeneratedKeys()) { + if (keys.next()) { + System.out.println("Account created with user_id=" + keys.getLong(1) + + " and username=" + candidate); + } else { + System.out.println("Account created with username=" + candidate); + } + } + return candidate; + } catch (SQLException e) { + // SQLState 23000 = integrity constraint violation (includes unique constraint) + String sqlState = e.getSQLState(); + if ("23000".equals(sqlState) || e.getMessage().toLowerCase().contains("duplicate") || e.getMessage().toLowerCase().contains("unique")) { + suffix++; + candidate = baseName + suffix; + // prova igen med nytt kandidatnamn + continue; + } + throw e; + } + } + } + } + + /** Uppdatera lösenord, returnerar true om lyckades */ + public boolean updatePassword(long userId, String newPassword) throws SQLException { + String sql = "UPDATE account SET password=? WHERE user_id=?"; + try (Connection conn = ds.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, newPassword); + ps.setLong(2, userId); + int rows = ps.executeUpdate(); + return rows > 0; + } + } + + /** Ta bort konto, returnerar true om lyckades */ + public boolean deleteAccount(long userId) throws SQLException { + String sql = "DELETE FROM account WHERE user_id=?"; + try (Connection conn = ds.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setLong(1, userId); + int rows = ps.executeUpdate(); + return rows > 0; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/DevDatabaseInitializer.java b/src/main/java/com/example/DevDatabaseInitializer.java index e8a45fe..5526da9 100644 --- a/src/main/java/com/example/DevDatabaseInitializer.java +++ b/src/main/java/com/example/DevDatabaseInitializer.java @@ -21,4 +21,4 @@ public static void start() { System.setProperty("APP_DB_PASS", mysql.getPassword()); } } -} +} \ No newline at end of file diff --git a/src/main/java/com/example/Main.java b/src/main/java/com/example/Main.java index 6dc6fbd..5a8899a 100644 --- a/src/main/java/com/example/Main.java +++ b/src/main/java/com/example/Main.java @@ -1,13 +1,13 @@ package com.example; -import java.sql.Connection; -import java.sql.DriverManager; +import javax.sql.DataSource; import java.sql.SQLException; import java.util.Arrays; +import java.util.Scanner; public class Main { - static void main(String[] args) { + public static void main(String[] args) { if (isDevMode(args)) { DevDatabaseInitializer.start(); } @@ -15,7 +15,7 @@ static void main(String[] args) { } public void run() { - // Resolve DB settings with precedence: System properties -> Environment variables + // Konfigurera databas 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"); @@ -26,32 +26,144 @@ public void run() { "as system properties (-Dkey=value) or environment variables."); } - try (Connection connection = DriverManager.getConnection(jdbcUrl, dbUser, dbPass)) { + DataSource ds = new SimpleDriverManagerDataSource(jdbcUrl, dbUser, dbPass); + AccountRepository accountRepo = new AccountRepository(ds); + MoonMissionRepository missionRepo = new MoonMissionRepository(ds); + + Scanner scanner = new Scanner(System.in); + + System.out.println("Welcome to the Moon Mission CLI!"); + + // ---- Login flow: prompt for username/password first ---- + String currentUser = null; + try { + System.out.print("Username: "); + String username = scanner.nextLine().trim(); + System.out.print("Password: "); + String password = scanner.nextLine(); + + boolean validLogin = accountRepo.login(username, password); + if (!validLogin) { + System.out.println("Invalid username or password"); + return; // stop program after invalid attempt (tests provide an extra 0 which will be ignored) + } + currentUser = username; + System.out.println("Login successful! Welcome, " + currentUser); } catch (SQLException e) { - throw new RuntimeException(e); + System.err.println("Database error: " + e.getMessage()); + return; + } catch (Exception e) { + // In case input is exhausted + return; } - //Todo: Starting point for your code + + // ---- Authenticated menu ---- + boolean running = true; + while (running) { + System.out.println("\nMenu:"); + System.out.println("1) List moon missions"); + System.out.println("2) Get mission by id"); + System.out.println("3) Count missions for year"); + System.out.println("4) Create an account"); + System.out.println("5) Update an account password"); + System.out.println("6) Delete an account"); + System.out.println("7) Log out (switch user)"); + System.out.println("0) Exit"); + System.out.print("Choose an option: "); + + String choice = scanner.nextLine().trim(); + + try { + switch (choice) { + case "1": + missionRepo.listMissions(); // skriver direkt till konsolen + break; + + case "2": + System.out.print("Mission ID: "); + long id = Long.parseLong(scanner.nextLine()); + missionRepo.getMissionById(id); // skriver direkt till konsolen + break; + + case "3": + System.out.print("Year: "); + int year = Integer.parseInt(scanner.nextLine()); + missionRepo.countByYear(year); // skriver direkt till konsolen + break; + + case "4": + System.out.print("First name: "); + String firstName = scanner.nextLine().trim(); + System.out.print("Last name: "); + String lastName = scanner.nextLine().trim(); + System.out.print("SSN: "); + String ssnNew = scanner.nextLine().trim(); + System.out.print("Password: "); + String newPass = scanner.nextLine(); + String newUsername = accountRepo.createAccount(firstName, lastName, ssnNew, newPass); + System.out.println("Account created. Username: " + newUsername); + break; + + case "5": + System.out.print("User ID: "); + long uid = Long.parseLong(scanner.nextLine()); + System.out.print("New password: "); + String passUpdate = scanner.nextLine(); + boolean updated = accountRepo.updatePassword(uid, passUpdate); + System.out.println(updated ? "Password updated" : "Update failed"); + break; + + case "6": + System.out.print("User ID to delete: "); + long delId = Long.parseLong(scanner.nextLine()); + boolean deleted = accountRepo.deleteAccount(delId); + System.out.println(deleted ? "Account deleted" : "Delete failed"); + break; + + case "7": + System.out.print("Username: "); + String newUser = scanner.nextLine().trim(); + System.out.print("Password: "); + String newPassLogin = scanner.nextLine(); + try { + boolean ok = accountRepo.login(newUser, newPassLogin); + if (ok) { + currentUser = newUser; + System.out.println("Login successful! Welcome, " + currentUser); + } else { + System.out.println("Invalid username or password"); + } + } catch (SQLException e) { + System.err.println("Database error: " + e.getMessage()); + } + break; + + case "0": + running = false; + System.out.println("Exiting program..."); + break; + + default: + System.out.println("Invalid choice. Try again."); + } + } catch (SQLException e) { + System.err.println("Database error: " + e.getMessage()); + } catch (NumberFormatException e) { + System.out.println("Invalid input format"); + } + } + + scanner.close(); } - /** - * Determines if the application is running in development mode based on system properties, - * environment variables, or command-line arguments. - * - * @param args an array of command-line arguments - * @return {@code true} if the application is in development mode; {@code false} otherwise - */ private static boolean isDevMode(String[] args) { - if (Boolean.getBoolean("devMode")) //Add VM option -DdevMode=true + if (Boolean.getBoolean("devMode")) return true; - if ("true".equalsIgnoreCase(System.getenv("DEV_MODE"))) //Environment variable DEV_MODE=true + if ("true".equalsIgnoreCase(System.getenv("DEV_MODE"))) return true; - return Arrays.asList(args).contains("--dev"); //Argument --dev + return Arrays.asList(args).contains("--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()) { @@ -59,4 +171,4 @@ private static String resolveConfig(String propertyKey, String envKey) { } return (v == null || v.trim().isEmpty()) ? null : v.trim(); } -} +} \ No newline at end of file diff --git a/src/main/java/com/example/MoonMissionRepository.java b/src/main/java/com/example/MoonMissionRepository.java new file mode 100644 index 0000000..2bea265 --- /dev/null +++ b/src/main/java/com/example/MoonMissionRepository.java @@ -0,0 +1,51 @@ +package com.example; + +import javax.sql.DataSource; +import java.sql.*; + +public class MoonMissionRepository { + private final DataSource ds; + + public MoonMissionRepository(DataSource ds) { + this.ds = ds; + } + + public void listMissions() throws SQLException { + String sql = "SELECT spacecraft FROM moon_mission ORDER BY mission_id"; + try (Connection conn = ds.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql); + ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + System.out.println(rs.getString("spacecraft")); + } + } + } + + public void getMissionById(long id) throws SQLException { + String sql = "SELECT mission_id, spacecraft, launch_date FROM moon_mission WHERE mission_id=?"; + try (Connection conn = ds.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setLong(1, id); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + System.out.println("Mission ID: " + rs.getLong("mission_id")); + System.out.println("Spacecraft: " + rs.getString("spacecraft")); + System.out.println("Launch Date: " + rs.getDate("launch_date")); + } + } + } + } + + public void countByYear(int year) throws SQLException { + String sql = "SELECT COUNT(*) as count FROM moon_mission WHERE YEAR(launch_date)=?"; + try (Connection conn = ds.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setInt(1, year); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + System.out.println("Number of missions in " + year + ": " + rs.getInt("count")); + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/SimpleDriverManagerDataSource.java b/src/main/java/com/example/SimpleDriverManagerDataSource.java new file mode 100644 index 0000000..cc9bcd7 --- /dev/null +++ b/src/main/java/com/example/SimpleDriverManagerDataSource.java @@ -0,0 +1,65 @@ +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.util.logging.Logger; + +public class SimpleDriverManagerDataSource implements DataSource { + private final String url; + private final String user; + private final String pass; + + public SimpleDriverManagerDataSource(String url, String user, String pass) { + this.url = url; + this.user = user; + this.pass = pass; + } + + @Override + public Connection getConnection() throws SQLException { + return DriverManager.getConnection(url, user, pass); + } + + @Override + public Connection getConnection(String username, String password) throws SQLException { + return DriverManager.getConnection(url, username, password); + } + + @Override + public PrintWriter getLogWriter() { + throw new UnsupportedOperationException("getLogWriter not supported"); + } + + @Override + public void setLogWriter(PrintWriter out) { + throw new UnsupportedOperationException("setLogWriter not supported"); + } + + @Override + public void setLoginTimeout(int seconds) { + throw new UnsupportedOperationException("setLoginTimeout not supported"); + } + + @Override + public int getLoginTimeout() { + throw new UnsupportedOperationException("getLoginTimeout not supported"); + } + + @Override + public Logger getParentLogger() { + throw new UnsupportedOperationException("getParentLogger not supported"); + } + + @Override + public T unwrap(Class iface) { + throw new UnsupportedOperationException("unwrap not supported"); + } + + @Override + public boolean isWrapperFor(Class iface) { + return false; + } +} \ No newline at end of file diff --git a/src/test/com/example/CliAppIT.java b/src/test/com/example/com/example/CliAppIT.java similarity index 99% rename from src/test/com/example/CliAppIT.java rename to src/test/com/example/com/example/CliAppIT.java index 7d1eafb..5e4ffd4 100644 --- a/src/test/com/example/CliAppIT.java +++ b/src/test/com/example/com/example/CliAppIT.java @@ -338,4 +338,4 @@ private static boolean existsAccount(long userId) throws SQLException { } } } -} +} \ No newline at end of file