Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -111,3 +112,38 @@ VG (extra credit)
- Create a `DataSource` once at startup (using the connection settings above) and inject it into your repositories by constructor injection. For a minimal setup, you can implement a small `SimpleDriverManagerDataSource` that delegates to `DriverManager.getConnection(...)`. This keeps repositories independent of configuration and lets you upgrade to a connection pool (e.g., HikariCP) later without changing repository code.
- Define `AccountRepository` and `MoonMissionRepository` and provide JDBC implementations.
- In `Main`, resolve configuration, construct the `DataSource`, instantiate repositories.


# Databas-JDBC Laboration (3)

Detta projekt är en Java-applikation som använder JDBC för att interagera med en MySQL-databas. Denna applikation hanterar konton och rymduppdrag.

## Funktioner

- Logga in med användarnamn och lösenord.
- Lista alla rymduppdrag.
- Hämta ett specifikt uppdrag baserat på mission_id.
- Räkna antal uppdrag för ett specifikt år.
- Skapa nytt konto.
- Uppdatera lösenord för ett konto.
- Ta bort ett konto.

## Användning

1. Starta programmet via IntelliJ eller kommandoraden.
2. Logga in med ett existerande konto (eller skapa ett nytt via menyn).
3. Följ menyn för att utföra olika operationer på databasen.

## Olika teknologier

- Java 25
- JDBC
- MySQL
- Maven

## Testning

I detta projekt används Maven för att köra integrationstester. Kör tester med:

```bash
./mvnw clean verify
3 changes: 3 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<mockito.version>5.20.0</mockito.version>
</properties>
<dependencies>

<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
Expand Down Expand Up @@ -46,11 +47,13 @@
<version>1.21.3</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<version>1.21.3</version>
</dependency>
Comment on lines 51 to 55
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the pom.xml file to confirm the dependency scope
cat pom.xml | head -60

Repository: ithsjava25/ithsjava25-java-databas-jdbc-javafx-classrom

Length of output: 2220


🏁 Script executed:

# Search for DevDatabaseInitializer to understand its usage
find . -name "DevDatabaseInitializer.java" -type f

Repository: ithsjava25/ithsjava25-java-databas-jdbc-javafx-classrom

Length of output: 154


🏁 Script executed:

# Check all testcontainers dependencies in pom.xml
rg "testcontainers" pom.xml -A 2

Repository: ithsjava25/ithsjava25-java-databas-jdbc-javafx-classrom

Length of output: 371


🏁 Script executed:

# Examine DevDatabaseInitializer.java to understand the usage context
cat ./src/main/java/com/example/DevDatabaseInitializer.java

Repository: ithsjava25/ithsjava25-java-databas-jdbc-javafx-classrom

Length of output: 870


🏁 Script executed:

# Search for usages of DevDatabaseInitializer
rg "DevDatabaseInitializer" --type java

Repository: ithsjava25/ithsjava25-java-databas-jdbc-javafx-classrom

Length of output: 270


🏁 Script executed:

# Also search for calls to .start() on DevDatabaseInitializer
rg "DevDatabaseInitializer\.start\(\)" --type java

Repository: ithsjava25/ithsjava25-java-databas-jdbc-javafx-classrom

Length of output: 178


🏁 Script executed:

# Check if there's a main method or entry point that calls this
find ./src/main/java -name "*.java" -type f | xargs grep -l "public static void main"

Repository: ithsjava25/ithsjava25-java-databas-jdbc-javafx-classrom

Length of output: 136


🏁 Script executed:

# Examine Main.java to see the context of DevDatabaseInitializer.start()
cat ./src/main/java/com/example/Main.java

Repository: ithsjava25/ithsjava25-java-databas-jdbc-javafx-classrom

Length of output: 8825


Move DevDatabaseInitializer to test source or use a Maven profile for dev-mode dependencies.

The testcontainers-mysql dependency correctly remains in compile scope since DevDatabaseInitializer is in src/main/java and must reference MySQLContainer at compile time. However, since dev mode initialization is optional (controlled via isDevMode() check), the dependency is unnecessarily bundled in production artifacts.

Current state: Dev mode is already conditional and requires explicit activation (system property devMode, environment variable DEV_MODE, or --dev argument), so the class is never instantiated in standard production runs.

Recommended solution: Move src/main/java/com/example/DevDatabaseInitializer.java to src/test/java and add <scope>test</scope> to the testcontainers-mysql dependency. This aligns the code organization with its actual purpose as a development/testing utility and prevents the dependency from being packaged in production artifacts.

Alternatively, create a separate Maven profile for dev mode that includes the Testcontainers dependency only when explicitly activated.

🤖 Prompt for AI Agents
In pom.xml around lines 51-55, the testcontainers MySQL dependency is declared
without test scope causing it to be bundled into production artifacts because
DevDatabaseInitializer lives in src/main/java; either move
src/main/java/com/example/DevDatabaseInitializer.java to
src/test/java/com/example/ so it becomes a test-only utility and then change the
org.testcontainers:mysql dependency to include <scope>test</scope>, or instead
leave the class in main and wrap the dependency in a dedicated Maven profile
(e.g., <profile>dev</profile>) that adds the testcontainers dependency only when
the dev profile is explicitly activated; apply one of these two changes and
update any package/import paths if the class is moved.


</dependencies>
<build>
<plugins>
Expand Down
189 changes: 167 additions & 22 deletions src/main/java/com/example/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,61 +2,206 @@

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
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) {
// Om vi kör i DevMode: Starta testdatabasen, via Docker/Testcontainers.
if (isDevMode(args)) {
DevDatabaseInitializer.start();
}
// Startar programlogiken.
new Main().run();
}

public void run() {
// Resolve DB settings with precedence: System properties -> Environment variables
// Hämtar databasinställningar.
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");

// Om någon databasinställning skulle saknas, så avbryts programmet.
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.");
"Missing DB configuration. Provide APP_JDBC_URL, APP_DB_USER, APP_DB_PASS as system properties (-Dkey=value) or environment variables."
);
}

// Skapar anslutning till databasen.
try (Connection connection = DriverManager.getConnection(jdbcUrl, dbUser, dbPass)) {
// För att läsa input av användaren i terminalen, så används Scanner.
Scanner scanner = new Scanner(System.in);

// Loggar in med användarnamn och lösenord.
System.out.print("Username: ");
String username = scanner.nextLine();
System.out.print("Password: ");
String password = scanner.nextLine();

// En SQL-fråga för att kontrollera användarnamn och lösenord.
String loginSql = "SELECT * FROM account WHERE name = ? AND password = ?";
try (PreparedStatement stmt = connection.prepareStatement(loginSql)) {
stmt.setString(1, username);
stmt.setString(2, password);
try (ResultSet rs = stmt.executeQuery()) {
// Om ingen rad hittas, så misslyckas inloggningen.
if (!rs.next()) {
System.out.println("Invalid username or password");
return; // Avslutar run() om inloggningen misslyckas.
}
}
}
Comment on lines +47 to +58
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for password hashing implementation

echo "Checking if init.sql contains hashed passwords..."
if [ -f "src/main/resources/init.sql" ]; then
    grep -i "password" src/main/resources/init.sql | head -20
fi

echo ""
echo "Searching for password hashing libraries or utilities in the codebase..."
rg -n "hash|bcrypt|pbkdf2|scrypt|argon" --type java -g '!**/test/**'

Repository: ithsjava25/ithsjava25-java-databas-jdbc-javafx-classrom

Length of output: 487


🏁 Script executed:

#!/bin/bash
# Find the createAccount method
echo "Searching for createAccount method..."
rg -n "createAccount|CREATE.*ACCOUNT" --type java -A 15

echo ""
echo "Checking for any password manipulation in Java code..."
rg -n "password" --type java -B 2 -A 2 | head -60

Repository: ithsjava25/ithsjava25-java-databas-jdbc-javafx-classrom

Length of output: 9140


Passwords must be hashed using a secure algorithm.

Passwords are currently stored and validated as plain text in the database. Lines 47–58 compare the input password directly against the password column without any hashing. The createAccount method (lines 144–159) also stores passwords in plain text. Use a secure hashing algorithm such as bcrypt, PBKDF2, Argon2, or scrypt for both storage and validation. Apply hashing in createAccount when inserting and modify the login query to validate against the hashed value.

// Fortsätter programmet efter lyckad inloggning.
System.out.println("Login successful!");
Comment on lines 36 to +60
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Resource leak: Scanner is never closed.

The Scanner created at line 38 is never closed, which can lead to resource leaks. Since the scanner wraps System.in, closing it would close the underlying stream, but the scanner should still be managed properly.

Consider restructuring to ensure proper resource management:

     try (Connection connection = DriverManager.getConnection(jdbcUrl, dbUser, dbPass)) {
-        // För att läsa input av användaren i terminalen, så används Scanner.
-        Scanner scanner = new Scanner(System.in);
+        try (Scanner scanner = new Scanner(System.in)) {
+            // För att läsa input av användaren i terminalen, så används Scanner.

-    } catch (SQLException e) {
+        }
+    } catch (SQLException e) {

Note: Be aware that closing a Scanner that wraps System.in will also close System.in, making it unavailable for subsequent reads. If this is a concern, document this behavior or consider alternative input handling approaches.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/com/example/Main.java around lines 36 to 60, the Scanner
instantiated for System.in is never closed which risks a resource leak; either
wrap the Scanner in a try-with-resources to ensure it is closed (accepting that
this will close System.in) or, if you must keep System.in open for later use,
reuse a single shared Scanner (e.g., a static or method-scoped Scanner) and
document that it intentionally remains open; update the code to follow one of
these patterns and add a brief comment explaining the choice.


// En meny-loop.
boolean running = true;
while (running) {
System.out.println("\nMenu:");
System.out.println("1) List moon missions");
System.out.println("2) Get moon mission by ID");
System.out.println("3) Count missions for a year");
System.out.println("4) Create account");
System.out.println("5) Update account password");
System.out.println("6) Delete account");
System.out.println("0) Exit");
System.out.print("Choose an option: ");

// Läser in användarens menyval.
switch (scanner.nextLine()) {
case "1" -> listMissions(connection);
case "2" -> getMissionById(connection, scanner);
case "3" -> countMissionsByYear(connection, scanner);
case "4" -> createAccount(connection, scanner);
case "5" -> updatePassword(connection, scanner);
case "6" -> deleteAccount(connection, scanner);
case "0" -> {
running = false;
System.out.println("Exiting program...");
}
default -> System.out.println("Invalid option.");
}
}

} catch (SQLException e) {
// Om något skulle gå fel med databasen.
throw new RuntimeException(e);
}
//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
*/
// Listar alla rymduppdrag.
private void listMissions(Connection conn) throws SQLException {
try (PreparedStatement stmt = conn.prepareStatement("SELECT spacecraft FROM moon_mission");
ResultSet rs = stmt.executeQuery()) {
System.out.println("Moon missions:");
while (rs.next()) {
System.out.println(rs.getString("spacecraft"));
}
}
}

// Hämtar ett specifikt uppdrag baserat på mission_id.
private void getMissionById(Connection conn, Scanner scanner) throws SQLException {
System.out.print("Enter mission_id: ");
String id = scanner.nextLine();
try (PreparedStatement stmt = conn.prepareStatement(
"SELECT spacecraft, launch_date FROM moon_mission WHERE mission_id = ?")) {
stmt.setString(1, id);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {

// Hämtar årtal från launch_date.
int year = rs.getDate("launch_date").toLocalDate().getYear();
System.out.println("Mission: " + rs.getString("spacecraft") + ", Year: " + year);
} else {
System.out.println("Mission not found.");
}
}
}
}

// Räknar hur många uppdrag som skedde ett specifikt år.
private void countMissionsByYear(Connection conn, Scanner scanner) throws SQLException {
System.out.print("Enter year: ");
String year = scanner.nextLine();
try (PreparedStatement stmt = conn.prepareStatement(
"SELECT COUNT(*) AS count FROM moon_mission WHERE YEAR(launch_date) = ?")) {
stmt.setString(1, year);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
System.out.println("Year: " + year + ", Number of missions: " + rs.getInt("count"));
}
}
}
}

// Skapar ett nytt konto.
private void createAccount(Connection conn, Scanner scanner) throws SQLException {
System.out.print("First name: ");
String firstName = scanner.nextLine();
System.out.print("Last name: ");
String lastName = scanner.nextLine();
System.out.print("SSN: ");
String ssn = scanner.nextLine();
System.out.print("Password: ");
String password = scanner.nextLine();

try (PreparedStatement stmt = conn.prepareStatement(
"INSERT INTO account (first_name, last_name, ssn, password) VALUES (?, ?, ?, ?)")) {
stmt.setString(1, firstName);
stmt.setString(2, lastName);
stmt.setString(3, ssn);
stmt.setString(4, password);
stmt.executeUpdate();
System.out.println("Account created!");
}
}
Comment on lines +144 to +163
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add input validation and improve error handling.

The method has several concerns:

  1. No input validation: Empty strings or invalid SSN formats are not checked before database insertion
  2. Generic error handling: SQL constraint violations (e.g., duplicate SSN) are caught by the generic handler in run(), providing poor user feedback
  3. Password storage: Passwords appear to be stored without hashing (related to earlier comment)

Consider adding validation:

 private void createAccount(Connection conn, Scanner scanner) throws SQLException {
     System.out.print("First name: ");
     String firstName = scanner.nextLine();
+    if (firstName == null || firstName.trim().isEmpty()) {
+        System.out.println("First name cannot be empty.");
+        return;
+    }
+    
     System.out.print("Last name: ");
     String lastName = scanner.nextLine();
+    if (lastName == null || lastName.trim().isEmpty()) {
+        System.out.println("Last name cannot be empty.");
+        return;
+    }
+    
     System.out.print("SSN: ");
     String ssn = scanner.nextLine();
+    if (ssn == null || ssn.trim().isEmpty()) {
+        System.out.println("SSN cannot be empty.");
+        return;
+    }
+    
     System.out.print("Password: ");
     String password = scanner.nextLine();
+    if (password == null || password.trim().isEmpty()) {
+        System.out.println("Password cannot be empty.");
+        return;
+    }
+    // TODO: Hash password before storage

     try (PreparedStatement stmt = conn.prepareStatement(
             "INSERT INTO account (first_name, last_name, ssn, password) VALUES (?, ?, ?, ?)")) {
         stmt.setString(1, firstName);
         stmt.setString(2, lastName);
         stmt.setString(3, ssn);
         stmt.setString(4, password);
         stmt.executeUpdate();
         System.out.println("Account created!");
+    } catch (SQLException e) {
+        if (e.getSQLState() != null && e.getSQLState().startsWith("23")) {
+            System.out.println("Failed to create account: Duplicate SSN or constraint violation.");
+        } else {
+            throw e;
+        }
     }
 }


// Uppdaterar ett lösenord för ett konto.
private void updatePassword(Connection conn, Scanner scanner) throws SQLException {
System.out.print("User ID: ");
String userId = scanner.nextLine();
System.out.print("New password: ");
String newPassword = scanner.nextLine();

try (PreparedStatement stmt = conn.prepareStatement("UPDATE account SET password = ? WHERE user_id = ?")) {
stmt.setString(1, newPassword);
stmt.setString(2, userId);
int rows = stmt.executeUpdate();
System.out.println(rows > 0 ? "Password updated!" : "User not found.");
}
}
Comment on lines +166 to +178
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add validation for empty password.

The method doesn't validate that the new password is non-empty before updating the database.

Apply this diff to add validation:

 private void updatePassword(Connection conn, Scanner scanner) throws SQLException {
     System.out.print("User ID: ");
     String userId = scanner.nextLine();
     System.out.print("New password: ");
     String newPassword = scanner.nextLine();
+    
+    if (newPassword == null || newPassword.trim().isEmpty()) {
+        System.out.println("Password cannot be empty.");
+        return;
+    }
+    // TODO: Hash password before storage

     try (PreparedStatement stmt = conn.prepareStatement("UPDATE account SET password = ? WHERE user_id = ?")) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private void updatePassword(Connection conn, Scanner scanner) throws SQLException {
System.out.print("User ID: ");
String userId = scanner.nextLine();
System.out.print("New password: ");
String newPassword = scanner.nextLine();
try (PreparedStatement stmt = conn.prepareStatement("UPDATE account SET password = ? WHERE user_id = ?")) {
stmt.setString(1, newPassword);
stmt.setString(2, userId);
int rows = stmt.executeUpdate();
System.out.println(rows > 0 ? "Password updated!" : "User not found.");
}
}
private void updatePassword(Connection conn, Scanner scanner) throws SQLException {
System.out.print("User ID: ");
String userId = scanner.nextLine();
System.out.print("New password: ");
String newPassword = scanner.nextLine();
if (newPassword == null || newPassword.trim().isEmpty()) {
System.out.println("Password cannot be empty.");
return;
}
// TODO: Hash password before storage
try (PreparedStatement stmt = conn.prepareStatement("UPDATE account SET password = ? WHERE user_id = ?")) {
stmt.setString(1, newPassword);
stmt.setString(2, userId);
int rows = stmt.executeUpdate();
System.out.println(rows > 0 ? "Password updated!" : "User not found.");
}
}
🤖 Prompt for AI Agents
In src/main/java/com/example/Main.java around lines 166 to 178, the
updatePassword method lacks validation for an empty newPassword; before
preparing/executing the UPDATE, check that newPassword is not null and not blank
(e.g., trim and verify length > 0), print an error message and return early
without executing the SQL when validation fails, and only proceed to create the
PreparedStatement and run executeUpdate when the newPassword passes validation.


// Tar bort ett konto.
private void deleteAccount(Connection conn, Scanner scanner) throws SQLException {
System.out.print("User ID: ");
String userId = scanner.nextLine();

try (PreparedStatement stmt = conn.prepareStatement("DELETE FROM account WHERE user_id = ?")) {
stmt.setString(1, userId);
int rows = stmt.executeUpdate();
System.out.println(rows > 0 ? "Account deleted!" : "User not found.");
}
}

// Avgör om programmet körs i DevMode.
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 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.
*/
// Läser värden från system properties eller environment variabler.
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();
}
}
}