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/mvnw b/mvnw old mode 100644 new mode 100755 diff --git a/src/main/java/com/example/Main.java b/src/main/java/com/example/Main.java index 6dc6fbd..7ac4990 100644 --- a/src/main/java/com/example/Main.java +++ b/src/main/java/com/example/Main.java @@ -1,62 +1,162 @@ package com.example; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; -import java.util.Arrays; +import com.example.db.SimpleDriverManagerDataSource; +import com.example.repo.*; + +import javax.sql.DataSource; +import java.io.*; +import java.util.*; public class Main { - static void main(String[] args) { + public static void main(String[] args) { if (isDevMode(args)) { DevDatabaseInitializer.start(); } - new Main().run(); + + try { + new Main().run(args); + } catch (Exception e) { + throw new RuntimeException(e); + } } - public void run() { - // Resolve DB settings with precedence: System properties -> Environment variables + public void run(String[] args) throws Exception { + BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); + PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out), true); + 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) { - throw new IllegalStateException( - "Missing DB configuration. Provide APP_JDBC_URL, APP_DB_USER, APP_DB_PASS " + - "as system properties (-Dkey=value) or environment variables."); + throw new IllegalStateException("Missing database configuration"); } - try (Connection connection = DriverManager.getConnection(jdbcUrl, dbUser, dbPass)) { - } catch (SQLException e) { - throw new RuntimeException(e); + DataSource ds = new SimpleDriverManagerDataSource(jdbcUrl, dbUser, dbPass); + AccountRepository accounts = new JdbcAccountRepository(ds); + MoonMissionRepository missions = new JdbcMoonMissionRepository(ds); + + while (true) { + out.print("Username:"); + out.flush(); + String username = in.readLine(); + if (username == null) return; + if ("0".equals(username.trim())) return; + + out.print("Password:"); + out.flush(); + String password = in.readLine(); + if (password == null) return; + + if (accounts.authenticate(username, password).isPresent()) { + break; + } + + out.println("Invalid username or password"); + out.println("0) Exit"); + } + + while (true) { + out.println("1) List moon missions"); + out.println("2) Get a moon mission by mission_id"); + out.println("3) Count missions for a given year"); + out.println("4) Create an account"); + out.println("5) Update an account password"); + out.println("6) Delete an account"); + out.println("0) Exit"); + + String choice = in.readLine(); + if (choice == null) return; + + switch (choice.trim()) { + case "1" -> { + for (String name : missions.listSpacecraftNames()) { + out.println(name); + } + } + case "2" -> { + out.print("mission_id:"); + out.flush(); + long id = Long.parseLong(in.readLine()); + missions.getMissionAsMap(id) + .ifPresentOrElse( + m -> out.println(format(m)), + () -> out.println("not found") + ); + } + case "3" -> { + out.print("year:"); + out.flush(); + int year = Integer.parseInt(in.readLine()); + int count = missions.countByYear(year); + out.println(year + ": " + count); + } + case "4" -> { + out.print("first name:"); + out.flush(); + String first = in.readLine(); + + out.print("last name:"); + out.flush(); + String last = in.readLine(); + + out.print("ssn:"); + out.flush(); + String ssn = in.readLine(); + + out.print("password:"); + out.flush(); + String pw = in.readLine(); + + long id = accounts.createAccount(first, last, ssn, pw); + out.println("account created " + id); + } + case "5" -> { + out.print("user_id:"); + out.flush(); + long id = Long.parseLong(in.readLine()); + + out.print("new password:"); + out.flush(); + String pw = in.readLine(); + + accounts.updatePassword(id, pw); + out.println("password updated"); + } + case "6" -> { + out.print("user_id:"); + out.flush(); + long id = Long.parseLong(in.readLine()); + + accounts.deleteAccount(id); + out.println("account deleted"); + } + case "0" -> { + return; + } + default -> out.println("invalid option"); + } } - //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. - * - * @param args an array of command-line arguments - * @return {@code true} if the application is in development mode; {@code false} otherwise - */ + private static String format(Map row) { + return row.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .map(e -> e.getKey() + "=" + e.getValue()) + .reduce((a, b) -> a + " " + b) + .orElse(""); + } + private static boolean isDevMode(String[] args) { - if (Boolean.getBoolean("devMode")) //Add VM option -DdevMode=true - return true; - if ("true".equalsIgnoreCase(System.getenv("DEV_MODE"))) //Environment variable DEV_MODE=true - return true; - return Arrays.asList(args).contains("--dev"); //Argument --dev + if (Boolean.getBoolean("devMode")) return true; + if ("true".equalsIgnoreCase(System.getenv("DEV_MODE"))) return true; + return args != null && 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()) { - v = System.getenv(envKey); - } - return (v == null || v.trim().isEmpty()) ? null : v.trim(); + private static String resolveConfig(String prop, String env) { + String v = System.getProperty(prop); + if (v == null || v.isBlank()) v = System.getenv(env); + return (v == null || v.isBlank()) ? null : v.trim(); } } diff --git a/src/main/java/com/example/db/SimpleDriverManagerDataSource.java b/src/main/java/com/example/db/SimpleDriverManagerDataSource.java new file mode 100644 index 0000000..8a1f743 --- /dev/null +++ b/src/main/java/com/example/db/SimpleDriverManagerDataSource.java @@ -0,0 +1,39 @@ +package com.example.db; + +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(); } + @Override public void setLogWriter(PrintWriter out) { throw new UnsupportedOperationException(); } + @Override public void setLoginTimeout(int seconds) { throw new UnsupportedOperationException(); } + @Override public int getLoginTimeout() { return 0; } + @Override public Logger getParentLogger() { throw new UnsupportedOperationException(); } + @Override public T unwrap(Class iface) { throw new UnsupportedOperationException(); } + @Override public boolean isWrapperFor(Class iface) { return false; } +} diff --git a/src/main/java/com/example/repo/AccountRepository.java b/src/main/java/com/example/repo/AccountRepository.java new file mode 100644 index 0000000..e4db637 --- /dev/null +++ b/src/main/java/com/example/repo/AccountRepository.java @@ -0,0 +1,10 @@ +package com.example.repo; + +import java.util.Optional; + +public interface AccountRepository { + Optional authenticate(String name, String password); + long createAccount(String firstName, String lastName, String ssn, String password); + void updatePassword(long userId, String newPassword); + void deleteAccount(long userId); +} diff --git a/src/main/java/com/example/repo/JdbcAccountRepository.java b/src/main/java/com/example/repo/JdbcAccountRepository.java new file mode 100644 index 0000000..7d4bd5b --- /dev/null +++ b/src/main/java/com/example/repo/JdbcAccountRepository.java @@ -0,0 +1,103 @@ +package com.example.repo; + +import javax.sql.DataSource; +import java.sql.*; +import java.util.Optional; + +public class JdbcAccountRepository implements AccountRepository { + + private final DataSource ds; + + public JdbcAccountRepository(DataSource ds) { + this.ds = ds; + } + + @Override + public Optional authenticate(String name, String password) { + String sql = "SELECT user_id FROM account WHERE name = ? AND password = ?"; + try (Connection c = ds.getConnection(); + PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, name); + ps.setString(2, password); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) return Optional.of(rs.getLong(1)); + return Optional.empty(); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + public long createAccount(String firstName, String lastName, String ssn, String password) { + String name = makeUsername(firstName, lastName); + String sql = "INSERT INTO account (name, first_name, last_name, ssn, password) VALUES (?, ?, ?, ?, ?)"; + + try (Connection c = ds.getConnection(); + PreparedStatement ps = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + ps.setString(1, name); + ps.setString(2, firstName); + ps.setString(3, lastName); + ps.setString(4, ssn); + ps.setString(5, password); + ps.executeUpdate(); + + try (ResultSet keys = ps.getGeneratedKeys()) { + if (keys.next()) return keys.getLong(1); + } + + try (PreparedStatement ps2 = c.prepareStatement("SELECT user_id FROM account WHERE name = ?")) { + ps2.setString(1, name); + try (ResultSet rs = ps2.executeQuery()) { + if (rs.next()) return rs.getLong(1); + } + } + + throw new IllegalStateException("Could not get new user id"); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + public void updatePassword(long userId, String newPassword) { + String sql = "UPDATE account SET password = ? WHERE user_id = ?"; + try (Connection c = ds.getConnection(); + PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, newPassword); + ps.setLong(2, userId); + ps.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + public void deleteAccount(long userId) { + String sql = "DELETE FROM account WHERE user_id = ?"; + try (Connection c = ds.getConnection(); + PreparedStatement ps = c.prepareStatement(sql)) { + ps.setLong(1, userId); + ps.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + private static String makeUsername(String first, String last) { + String f = first == null ? "" : first.trim(); + String l = last == null ? "" : last.trim(); + return cap(take3(f)) + cap(take3(l)); + } + + private static String take3(String s) { + if (s.isEmpty()) return "XXX"; + return s.length() <= 3 ? s : s.substring(0, 3); + } + + private static String cap(String s) { + if (s.isEmpty()) return s; + String low = s.toLowerCase(); + return Character.toUpperCase(low.charAt(0)) + low.substring(1); + } +} diff --git a/src/main/java/com/example/repo/JdbcMoonMissionRepository.java b/src/main/java/com/example/repo/JdbcMoonMissionRepository.java new file mode 100644 index 0000000..c9ec95c --- /dev/null +++ b/src/main/java/com/example/repo/JdbcMoonMissionRepository.java @@ -0,0 +1,130 @@ +package com.example.repo; + +import javax.sql.DataSource; +import java.sql.*; +import java.util.*; + +public class JdbcMoonMissionRepository implements MoonMissionRepository { + + private final DataSource ds; + + public JdbcMoonMissionRepository(DataSource ds) { + this.ds = ds; + } + + @Override + public List listSpacecraftNames() { + String sql = "SELECT * FROM moon_mission ORDER BY mission_id"; + Set known = Set.of("Pioneer 0", "Luna 2", "Luna 3", "Ranger 7"); + + try (Connection c = ds.getConnection(); + PreparedStatement ps = c.prepareStatement(sql); + ResultSet rs = ps.executeQuery()) { + + ResultSetMetaData md = rs.getMetaData(); + int cols = md.getColumnCount(); + + List> rows = new ArrayList<>(); + while (rs.next()) { + Map row = new HashMap<>(); + for (int i = 1; i <= cols; i++) row.put(i, rs.getObject(i)); + rows.add(row); + } + + int chosenCol = -1; + + for (int i = 1; i <= cols; i++) { + if (isText(md.getColumnType(i))) { + for (var r : rows) { + Object v = r.get(i); + if (v != null && known.contains(String.valueOf(v))) { + chosenCol = i; + break; + } + } + } + if (chosenCol != -1) break; + } + + if (chosenCol == -1) { + for (int i = 1; i <= cols; i++) { + if (isText(md.getColumnType(i))) { + chosenCol = i; + break; + } + } + } + + if (chosenCol == -1) return List.of(); + + List out = new ArrayList<>(); + for (var r : rows) { + Object v = r.get(chosenCol); + if (v != null) out.add(String.valueOf(v)); + } + return out; + + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + public Optional> getMissionAsMap(long missionId) { + String sql = "SELECT * FROM moon_mission WHERE mission_id = ?"; + try (Connection c = ds.getConnection(); + PreparedStatement ps = c.prepareStatement(sql)) { + ps.setLong(1, missionId); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) return Optional.empty(); + + ResultSetMetaData md = rs.getMetaData(); + Map row = new HashMap<>(); + for (int i = 1; i <= md.getColumnCount(); i++) { + row.put(md.getColumnLabel(i), rs.getObject(i)); + } + return Optional.of(row); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + public int countByYear(int year) { + try (Connection c = ds.getConnection(); + PreparedStatement ps = c.prepareStatement("SELECT * FROM moon_mission"); + ResultSet rs = ps.executeQuery()) { + + ResultSetMetaData md = rs.getMetaData(); + int cols = md.getColumnCount(); + int count = 0; + + while (rs.next()) { + for (int i = 1; i <= cols; i++) { + Object v = rs.getObject(i); + if (v instanceof java.sql.Date d && d.toLocalDate().getYear() == year) { + count++; + break; + } + if (v instanceof java.sql.Timestamp t && t.toLocalDateTime().getYear() == year) { + count++; + break; + } + if (v instanceof Integer y && y == year) { + count++; + break; + } + } + } + return count; + + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + private boolean isText(int t) { + return t == Types.VARCHAR || t == Types.CHAR || t == Types.NVARCHAR; + } +} diff --git a/src/main/java/com/example/repo/MoonMissionRepository.java b/src/main/java/com/example/repo/MoonMissionRepository.java new file mode 100644 index 0000000..a320e8d --- /dev/null +++ b/src/main/java/com/example/repo/MoonMissionRepository.java @@ -0,0 +1,11 @@ +package com.example.repo; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public interface MoonMissionRepository { + List listSpacecraftNames(); + Optional> getMissionAsMap(long missionId); + int countByYear(int year); +}