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-JohanHiths/README.md b/databas-jdbc-JohanHiths/README.md new file mode 100644 index 0000000..c95445f --- /dev/null +++ b/databas-jdbc-JohanHiths/README.md @@ -0,0 +1,114 @@ +[![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. +Because Testcontainers needs a container runtime, you must have Docker running on your machine to execute the tests. + +- What the tests do + - Start a throwaway MySQL container using the configuration in `src/main/resources/myconfig` and seed data from `src/main/resources/init.sql`. + - Set the following Java system properties so the application can connect to that database: + - `APP_JDBC_URL` + - `APP_DB_USER` + - `APP_DB_PASS` + - Drive your CLI via STDIN/STDOUT: first a login flow (username → password), then menu operations (list missions, get mission by id, count missions by year, create/update/delete account), and finally exit. + +- How to run the tests + - Ensure Docker Desktop (Windows/macOS) or Docker Engine (Linux) is running. + - Run: `./mvnw verify` + +If Docker is not running, Testcontainers will fail to start the database and tests will not run. + +--- + +### Dev mode (optional local database during development) + +When you run the application directly (without the test suite), you can let the app start a development MySQL instance for you. +Enable “dev mode” in any one of three ways: + +1) Java system property (VM option) +- Add: `-DdevMode=true` + +2) Environment variable +- Set: `DEV_MODE=true` + +3) Program argument +- Pass: `--dev` + +In dev mode, `DevDatabaseInitializer` uses Testcontainers to start MySQL and automatically sets the standard properties `APP_JDBC_URL`, `APP_DB_USER`, and `APP_DB_PASS` so the app can connect. + +Example (Windows PowerShell): +``` +java -DdevMode=true -jar target/app.jar +``` + +--- + +### Supplying your own database settings in IntelliJ (when not using dev mode) + +If you prefer to connect to an existing database (local or remote) and you are not using dev mode, configure the Run/Debug Configuration in IntelliJ IDEA. The application reads settings with clear precedence: Java system properties (VM options) first, then environment variables. + +Required keys: +- `APP_JDBC_URL` (e.g., `jdbc:mysql://localhost:3306/testdb`) +- `APP_DB_USER` +- `APP_DB_PASS` + +Steps (IntelliJ IDEA): +1) Open Run → Edit Configurations… +2) Create or select an Application configuration for `com.example.Main`. +3) Choose one of the following ways to provide settings: + - VM options (recommended; highest precedence) + - Put this in the field “VM options”: + ``` + -DAPP_JDBC_URL=jdbc:mysql://localhost:3306/testdb -DAPP_DB_USER=user -DAPP_DB_PASS=pass + ``` + - Environment variables + - Click “Modify options” → check “Environment variables” (if not visible), then click the `…` button and add: + - `APP_JDBC_URL = jdbc:mysql://localhost:3306/testdb` + - `APP_DB_USER = user` + - `APP_DB_PASS = pass` +4) Apply and Run. + +Notes +- You can mix both, but values in VM options override environment variables. +- If any of the three are missing or blank, the app fails fast with a clear error. + +Running in dev mode from IntelliJ +- In the same Run/Debug Configuration, you can enable dev mode in any one of these ways: + - VM option: add `-DdevMode=true` + - Environment variable: add `DEV_MODE=true` + - Program arguments: add `--dev` to the “Program arguments” field + +Tip: Maven test runs inside IntelliJ +- If you run the Maven goal `verify` or the integration tests from IntelliJ, ensure Docker is running. Testcontainers will manage the database and set `APP_JDBC_URL`, `APP_DB_USER`, and `APP_DB_PASS` automatically for the test JVM. + +--- + +### Assignment requirements + +G (base level) +- Implement the CLI application logic in `Main` starting at the `run()` method so that the provided tests pass. Concretely, your CLI should: + - Prompt for `Username:` and then `Password:` on startup and validate them against the `account` table (`name` + `password`). + - If the login is invalid, print a message containing the word `invalid` and allow exiting via option `0`. + - If the login is valid, present a menu with options: + ``` + 1) List moon missions (prints spacecraft names from `moon_mission`). + 2) Get a moon mission by mission_id (prints details for that mission). + 3) Count missions for a given year (prompts: year; prints the number of missions launched that year). + 4) Create an account (prompts: first name, last name, ssn, password; prints confirmation). + 5) Update an account password (prompts: user_id, new password; prints confirmation). + 6) Delete an account (prompts: user_id; prints confirmation). + 0) Exit. + ``` + - Use the DB settings provided via `APP_JDBC_URL`, `APP_DB_USER`, `APP_DB_PASS` (already resolved in `Main`). + +Notes +- Seed data in `init.sql` includes a known account used by the tests (e.g., username `AngFra`, password `MB=V4cbAqPz4vqmQ`). +- The tests are ordered to run login checks first and then the other menu actions. + +--- + +VG (extra credit) +- Implement a Repository pattern so that all database access lives inside repository classes, and the rest of your application depends only on repository interfaces. Recommended approach: + - 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. diff --git a/databas-jdbc-JohanHiths/mvnw b/databas-jdbc-JohanHiths/mvnw new file mode 100644 index 0000000..4f4e2c2 --- /dev/null +++ b/databas-jdbc-JohanHiths/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/databas-jdbc-JohanHiths/mvnw.cmd b/databas-jdbc-JohanHiths/mvnw.cmd new file mode 100644 index 0000000..5761d94 --- /dev/null +++ b/databas-jdbc-JohanHiths/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/databas-jdbc-JohanHiths/pom.xml b/databas-jdbc-JohanHiths/pom.xml new file mode 100644 index 0000000..1b653b5 --- /dev/null +++ b/databas-jdbc-JohanHiths/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + + com.example + jdbc + 1.0-SNAPSHOT + + + 25 + UTF-8 + 6.0.1 + 3.27.6 + 5.20.0 + + + + com.mysql + mysql-connector-j + 9.5.0 + runtime + + + org.junit.jupiter + junit-jupiter + ${junit.jupiter.version} + test + + + org.assertj + assertj-core + ${assertj.core.version} + test + + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + + + org.testcontainers + junit-jupiter + 1.21.3 + test + + + org.testcontainers + mysql + 1.21.3 + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.5.4 + + + integration-test + + integration-test + verify + + + + + + + diff --git a/databas-jdbc-JohanHiths/src/main/java/com/example/Account.java b/databas-jdbc-JohanHiths/src/main/java/com/example/Account.java new file mode 100644 index 0000000..41991fc --- /dev/null +++ b/databas-jdbc-JohanHiths/src/main/java/com/example/Account.java @@ -0,0 +1,59 @@ +package com.example; + + + + +public class Account { + + private int userId; + private String firstName; + private String lastName; + private String ssn; + private String password; + + public Account(int userId, String firstName, String lastName, String ssn, String password) { + this.userId = userId; + this.firstName = firstName; + this.lastName = lastName; + this.ssn = ssn; + this.password = password; + } + + + + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + public String getSsn(String ssn) { + return ssn; + } + + public int getUserId() { + return userId; + } + + public String getPassword() { + return password; + } + + public void setUserId(int userId) { + } + + public void setFirstName(String firstName) { + } + + public void setLastName(String lastName) { + } + + public void setSsn(String ssn) { + } + + public void setHashedPassword(String password) { + + } +} \ No newline at end of file diff --git a/databas-jdbc-JohanHiths/src/main/java/com/example/AccountRepository.java b/databas-jdbc-JohanHiths/src/main/java/com/example/AccountRepository.java new file mode 100644 index 0000000..790b078 --- /dev/null +++ b/databas-jdbc-JohanHiths/src/main/java/com/example/AccountRepository.java @@ -0,0 +1,11 @@ +package com.example; + +public interface AccountRepository { + boolean createAccount(String firstName, String lastName, String ssn, String password); + boolean deleteAccount(int userId); + Account findByUsername(String username); + boolean verifyPassword(String username, String rawPassword); + boolean updatePassword(int userId, String hashedPassword); + + +} diff --git a/databas-jdbc-JohanHiths/src/main/java/com/example/DevDatabaseInitializer.java b/databas-jdbc-JohanHiths/src/main/java/com/example/DevDatabaseInitializer.java new file mode 100644 index 0000000..44e3a65 --- /dev/null +++ b/databas-jdbc-JohanHiths/src/main/java/com/example/DevDatabaseInitializer.java @@ -0,0 +1,24 @@ +package com.example; + + +import org.testcontainers.containers.MySQLContainer; + +public class DevDatabaseInitializer { + private static MySQLContainer mysql; + + public static void start() { + if (mysql == null) { + mysql = new MySQLContainer<>("mysql:9.5.0") + .withDatabaseName("testdb") + .withUsername("user") + .withPassword("password") + .withConfigurationOverride("myconfig") + .withInitScript("init.sql"); + mysql.start(); + + System.setProperty("APP_JDBC_URL", mysql.getJdbcUrl()); + System.setProperty("APP_DB_USER", mysql.getUsername()); + System.setProperty("APP_DB_PASS", mysql.getPassword()); + } + } +} diff --git a/databas-jdbc-JohanHiths/src/main/java/com/example/JdbcAccountRepository.java b/databas-jdbc-JohanHiths/src/main/java/com/example/JdbcAccountRepository.java new file mode 100644 index 0000000..4bfe707 --- /dev/null +++ b/databas-jdbc-JohanHiths/src/main/java/com/example/JdbcAccountRepository.java @@ -0,0 +1,105 @@ +package com.example; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class JdbcAccountRepository implements AccountRepository { + + private final Connection connection; + + public JdbcAccountRepository(Connection connection) { + this.connection = connection; + } + + @Override + public boolean createAccount(String firstName, String lastName, String ssn, String password) { + String sql = "INSERT INTO account (first_name, last_name, ssn, password) VALUES (?, ?, ?, ?)"; + + try (PreparedStatement pstmt = connection.prepareStatement(sql)) { + + pstmt.setString(1, firstName); + pstmt.setString(2, lastName); + pstmt.setString(3, ssn); + pstmt.setString(4, password); // plain text for assignment tests + + return pstmt.executeUpdate() > 0; + + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + @Override + public boolean deleteAccount(int userId) { + String sql = "DELETE FROM account WHERE user_id = ?"; + try (PreparedStatement pstmt = connection.prepareStatement(sql)) { + + pstmt.setInt(1, userId); + + return pstmt.executeUpdate() > 0; + + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + @Override + public Account findByUsername(String username) { + + String sql = "SELECT * FROM account WHERE name = ?"; + + try (PreparedStatement pstmt = connection.prepareStatement(sql)) { + + pstmt.setString(1, username); + + try (ResultSet rs = pstmt.executeQuery()) { + + if (rs.next()) { + return new Account( + rs.getInt("user_id"), + rs.getString("first_name"), + rs.getString("last_name"), + rs.getString("ssn"), + rs.getString("password") // plain text + ); + } + } + } catch (SQLException e) { + throw new RuntimeException("Error retrieving account by username", e); + } + + return null; + } + + @Override + public boolean verifyPassword(String username, String rawPassword) { + + Account account = findByUsername(username); + + if (account == null) + return false; + + + return account.getPassword().equals(rawPassword); + } + + @Override + public boolean updatePassword(int userId, String hashedPassword) { + String sql = "UPDATE account SET password = ? WHERE user_id = ?"; + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + + stmt.setString(1, hashedPassword); + stmt.setInt(2, userId); + + return stmt.executeUpdate() > 0; + + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } +} \ No newline at end of file diff --git a/databas-jdbc-JohanHiths/src/main/java/com/example/Main.java b/databas-jdbc-JohanHiths/src/main/java/com/example/Main.java new file mode 100644 index 0000000..71fc71e --- /dev/null +++ b/databas-jdbc-JohanHiths/src/main/java/com/example/Main.java @@ -0,0 +1,404 @@ +package com.example; + + +import org.mindrot.jbcrypt.BCrypt; +import java.util.List; +import java.sql.*; +import java.util.Arrays; + +public class Main { + public void runApplicationMenu(Connection connection) throws SQLException { + MoonMissionRepository missionRepo = new MoonMissionRepositoryJdbc(connection); + AccountRepository accountRepo = new JdbcAccountRepository(connection); + boolean isRunning = true; + while (isRunning) { + System.out.println("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."); + int choice; + + try { + choice = Integer.parseInt(IO.readln("Enter choice: ")); + } catch (NumberFormatException e) { + System.out.println("Invalid choice."); + continue; + } + switch (choice) { + case 0: + isRunning = false; + break; + case 1: + List missions = missionRepo.listAllMissions(); + System.out.println("\nMoon Missions"); + for (MoonMission m : missions) { + System.out.println(m.getSpacecraft()); + } + + break; + case 2: + System.out.print("Enter the mission ID: "); + String input = IO.readln(); + int missionId = Integer.parseInt(input); + + MoonMission mission = missionRepo.findMoonMissionById(missionId); + + if (mission == null) { + System.out.println(" Mission not found."); + } else { + System.out.println("\n--- Mission Details ---"); + System.out.println("ID: " + mission.getMissionId()); + System.out.println("Spacecraft: " + mission.getSpacecraft()); + System.out.println("Launch date: " + mission.getLaunchDate()); + System.out.println("Outcome: " + mission.getOutcome()); + System.out.println("Carrier rocket: " + mission.getCarrierRocket()); + System.out.println("------------------------"); + } + break; + case 3: + int year = 0; + while (true) { + try { + String yearInput = IO.readln("Enter the launch year: "); + year = Integer.parseInt(yearInput); + break; + } catch (NumberFormatException e) { + System.out.println("Invalid year. Please enter a numeric value."); + } + } + + int count = missionRepo.countMissionsByYear(year); + + System.out.println("Number of missions launched in " + year + ": " + count); + break; + + + case 4: + System.out.println("Enter first name"); + String firstName = IO.readln(); + System.out.println("Enter last name"); + String lastName = IO.readln(); + System.out.println("Enter SSN"); + String ssn = IO.readln(); + System.out.println("Enter password"); + String rawPassword = IO.readln(); + + + + String hashedPassword = BCrypt.hashpw(rawPassword, BCrypt.gensalt()); + boolean accountCreated = accountRepo.createAccount(firstName, lastName, ssn, hashedPassword); + + if (accountCreated) { + System.out.println("Account created successfully."); + } else { + System.out.println("Account creation failed."); + } + + break; + case 5: + while (true) { + System.out.println("Enter the usedId to update password"); + String idInput = IO.readln(); + + int userId; + try { + userId = Integer.parseInt(idInput); + } catch (NumberFormatException e) { + System.out.println("Invalid user ID."); + break; + } + + System.out.println("Enter new password"); + String newPassword = IO.readln(); + if(newPassword.equals("0")){ + break; + } + + String hashed = BCrypt.hashpw(newPassword, BCrypt.gensalt()); + + boolean updatePassword = accountRepo.updatePassword(userId, hashed); + + if (updatePassword) { + System.out.println("Password updated successfully."); + + } else { + System.out.println("Password update failed."); + break; + } + } + break; + case 6: + System.out.println("Enter user ID to delete!"); + String deleteInput = IO.readln(); + + int deleteId; + try { + deleteId = Integer.parseInt(deleteInput); + } catch (NumberFormatException e) { + System.out.println("Invalid user ID."); + break; + } + boolean deleted = accountRepo.deleteAccount(deleteId); + if (deleted) { + System.out.println("Account deleted successfully."); + } else{ + System.out.println("Account delete failed."); + } + + break; + } + + } + + } + + + + static void main(String[] args) throws SQLException { + if (isDevMode(args)) { + DevDatabaseInitializer.start(); + } + new Main().run(); + + + } + + public void run() throws SQLException { + + + 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."); + } + + try (Connection connection = DriverManager.getConnection(jdbcUrl, dbUser, dbPass)) { + + while (true) { + + String username = IO.readln("Username: "); + String password = IO.readln("Password: "); + + String query = "SELECT * FROM account WHERE name = ? AND password = ?"; + + try (PreparedStatement pstmt = connection.prepareStatement(query)) { + + pstmt.setString(1, username); + pstmt.setString(2, password); + + try (ResultSet rs = pstmt.executeQuery()) { + + if (rs.next()) { + System.out.println("Logged in!"); + runApplicationMenu(connection); + return; // exit login + } else { + System.out.println("Invalid username or password"); + } + } + + } catch (SQLException e) { + System.err.println("Database error during login: " + e.getMessage()); + } + } + } + } + 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 + } + + + 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 void printMoonMissions(Connection connection) throws SQLException { + String moonQuery = "SELECT spacecraft FROM moon_mission"; + + try (PreparedStatement pstmt = connection.prepareStatement(moonQuery); + ResultSet rs = pstmt.executeQuery()) { + + System.out.println("\n=== Moon Missions ==="); + while (rs.next()) { + System.out.println("- " + rs.getString("spacecraft")); + } + + + } + + } + + + private static void printMoonMissionId(Connection connection) throws SQLException { + + + int missionId; + while (true) { + try { + String input = IO.readln("Enter the mission ID: "); + missionId = Integer.parseInt(input); + break; + } catch (NumberFormatException e) { + System.out.println("Please enter a valid mission ID (number)."); + } + } + + String sql = "SELECT * FROM moon_mission WHERE mission_id = ?"; + + try (PreparedStatement pstmt = connection.prepareStatement(sql)) { + pstmt.setInt(1, missionId); + + try (ResultSet rs = pstmt.executeQuery()) { + + if (rs.next()) { + System.out.println("\n=== Mission Details ==="); + System.out.println("Mission ID: " + rs.getInt("mission_id")); + System.out.println("Spacecraft: " + rs.getString("spacecraft")); + System.out.println("Launch Date: " + rs.getDate("launch_date")); + System.out.println("Carrier Rocket: " + rs.getString("carrier_rocket")); + System.out.println("Operator: " + rs.getString("operator")); + System.out.println("Mission Type: " + rs.getString("mission_type")); + System.out.println("Outcome: " + rs.getString("outcome")); + } else { + System.out.println("No mission found with ID " + missionId); + } + } + } + } + + private static void printMissionYear(Connection connection) throws SQLException { + int missionYear; + while (true) { + try { + String input = IO.readln("Enter mission year (number)"); + missionYear = Integer.parseInt(input); + break; + } catch (NumberFormatException e) { + System.out.println("Please enter mission year (number)."); + } + } + + + + + + String moonDate = "SELECT count(*) FROM moon_mission WHERE YEAR(launch_date) = ?"; + + try (PreparedStatement pstmt = connection.prepareStatement(moonDate)) { + pstmt.setInt(1, missionYear); + + try (ResultSet rs = pstmt.executeQuery()) { + + + + if (rs.next()) { + int count = rs.getInt(1); + System.out.println("\nMission Type: " + missionYear + ": " + count); + } + } + } + } + + private static void printAccountCreation(Connection connection) throws SQLException { + System.out.println("First name"); + String firstName = IO.readln(); + System.out.println("Last name"); + String lastName = IO.readln(); + System.out.println("SSN"); + String SSN = IO.readln(); + System.out.println("Password"); + String password = IO.readln(); + + + //String hashedPassword = BCrypt.hashpw(password, BCrypt.gensalt()); + + String sql = "INSERT INTO account (first_name, last_name, ssn, password) VALUES (?, ?, ?, ?)"; + + try (PreparedStatement pstmt = connection.prepareStatement(sql)) { + pstmt.setString(1, firstName); + pstmt.setString(2, lastName); + pstmt.setString(3, SSN); + pstmt.setString(4, password); + int rows = pstmt.executeUpdate(); + + if (rows > 0) { + System.out.println("\nAccount Created"); + } else { + System.out.println("\nAccount Creation Failed"); + } + } + } + + private static void printAccountUpdate(Connection connection) throws SQLException { + System.out.println("First name"); + String firstName = IO.readln(); + System.out.println("Last name"); + String lastName = IO.readln(); + System.out.println("SSN"); + String SSN = IO.readln(); + System.out.println("Password"); + String password = IO.readln(); + + String account = "Update account SET password = ?, ssn ? WHERE last_name = ?"; + ; + + try (PreparedStatement pstmt = connection.prepareStatement(account)) { + pstmt.setString(1, firstName); + pstmt.setString(2, lastName); + pstmt.setString(3, SSN); + pstmt.setString(4, password); + + int rs = pstmt.executeUpdate(); + { + if (rs > 0) { + System.out.println("\nAccount Updated"); + } else { + System.out.println("\nAccount Update Failed"); + } + } + } + } + + private static void printAccountDeletion(Connection connection) throws SQLException { + int accountId; + while (true) { + try { + String input = IO.readln("Enter the user ID to delete: "); + accountId = Integer.parseInt(input); + break; + } catch (NumberFormatException e) { + System.out.println("Please enter a valid user ID (number)."); + } + } + + String sql = "DELETE FROM account WHERE user_id = ?"; + try (PreparedStatement pstmt = connection.prepareStatement(String.valueOf(sql))) { + pstmt.setInt(1, accountId); + ; + + int rs = pstmt.executeUpdate(); + { + if (rs > 0) { + System.out.println("\nAccount Id" + accountId + "removed"); + } else { + System.out.println("\nAccount Id" + accountId + "Removal Failed"); + } + } + } + } +} \ No newline at end of file diff --git a/databas-jdbc-JohanHiths/src/main/java/com/example/MoonMission.java b/databas-jdbc-JohanHiths/src/main/java/com/example/MoonMission.java new file mode 100644 index 0000000..f247e4d --- /dev/null +++ b/databas-jdbc-JohanHiths/src/main/java/com/example/MoonMission.java @@ -0,0 +1,69 @@ +package com.example; + + + +import java.sql.Date; + + +public class MoonMission { + private int missionId; + private String spacecraft; + private Date launchDate; + private String carrierRocket; + private String operator; + private String missionType; + private String outcome; + + public MoonMission(int missionId, String spacecraft, Date launchDate, + String carrierRocket, String operator, String missionType, String outcome) { + this.missionId = missionId; + this.spacecraft = spacecraft; + this.launchDate = launchDate; + this.carrierRocket = carrierRocket; + this.operator = operator; + this.missionType = missionType; + this.outcome = outcome; + } +// + + public String getSpacecraft() { + + return spacecraft; + } + + public Date getLaunchDate() { + return launchDate; + } + + public String getCarrierRocket() { + return carrierRocket; + } + + public int getMissionId() { + + return missionId; + } + + public String getMissionName() { + return missionType; + } + + public String getOutcome() { + return outcome; + } + + @Override + public String toString() { + return "MoonMission {" + + "missionId=" + missionId + + ", spacecraft='" + spacecraft + '\'' + + ", launchDate=" + launchDate + + ", carrierRocket='" + carrierRocket + '\'' + + ", operator='" + operator + '\'' + + ", missionType='" + missionType + '\'' + + ", outcome='" + outcome + '\'' + + '}'; + } + + +} \ No newline at end of file diff --git a/databas-jdbc-JohanHiths/src/main/java/com/example/MoonMissionRepository.java b/databas-jdbc-JohanHiths/src/main/java/com/example/MoonMissionRepository.java new file mode 100644 index 0000000..18d5c85 --- /dev/null +++ b/databas-jdbc-JohanHiths/src/main/java/com/example/MoonMissionRepository.java @@ -0,0 +1,10 @@ +package com.example; + + +import java.util.List; + +public interface MoonMissionRepository { + List listAllMissions(); + MoonMission findMoonMissionById(int missionId); + int countMissionsByYear(int year); +} diff --git a/databas-jdbc-JohanHiths/src/main/java/com/example/MoonMissionRepositoryJdbc.java b/databas-jdbc-JohanHiths/src/main/java/com/example/MoonMissionRepositoryJdbc.java new file mode 100644 index 0000000..2727583 --- /dev/null +++ b/databas-jdbc-JohanHiths/src/main/java/com/example/MoonMissionRepositoryJdbc.java @@ -0,0 +1,84 @@ +package com.example; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +public class MoonMissionRepositoryJdbc implements MoonMissionRepository { + + private final Connection connection; + + public MoonMissionRepositoryJdbc(Connection connection) { + this.connection = connection; + } + + @Override + public List listAllMissions() { + List missions = new ArrayList<>(); + String sql = "SELECT * FROM moon_mission"; + + try (PreparedStatement stmt = connection.prepareStatement(sql); + ResultSet rs = stmt.executeQuery()) { + + while (rs.next()) { + missions.add(new MoonMission( + rs.getInt("mission_id"), + rs.getString("spacecraft"), + rs.getDate("launch_date"), + rs.getString("carrier_rocket"), + rs.getString("operator"), + rs.getString("mission_type"), + rs.getString("outcome") + )); + } + } catch (SQLException e) { + e.printStackTrace(); + } + + return missions; + } + + @Override + public MoonMission findMoonMissionById(int id) { + String sql = "SELECT * FROM moon_mission WHERE mission_id = ?"; + + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setInt(1, id); + + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + return new MoonMission( + rs.getInt("mission_id"), + rs.getString("spacecraft"), + rs.getDate("launch_date"), + rs.getString("carrier_rocket"), + rs.getString("operator"), + rs.getString("mission_type"), + rs.getString("outcome") + ); + } + } + } catch (SQLException e) { + e.printStackTrace(); + } + + return null; + } + + @Override + public int countMissionsByYear(int year) { + String sql = "SELECT COUNT(*) FROM moon_mission WHERE YEAR(launch_date) = ?"; + + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setInt(1, year); + + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) return rs.getInt(1); + } + } catch (SQLException e) { + e.printStackTrace(); + } + + return 0; + } +} \ No newline at end of file diff --git a/databas-jdbc-JohanHiths/src/main/resources/init.sql b/databas-jdbc-JohanHiths/src/main/resources/init.sql new file mode 100644 index 0000000..4e3afdf --- /dev/null +++ b/databas-jdbc-JohanHiths/src/main/resources/init.sql @@ -0,0 +1,96 @@ +-- +-- Skapar databasen och tabellerna för Laboration 1. +-- +DROP DATABASE IF EXISTS testdb; +CREATE DATABASE testdb; +USE testdb; +DROP TABLE IF EXISTS account; +DROP TABLE IF EXISTS moon_mission; +CREATE TABLE account +( + user_id smallint PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(255), + password VARCHAR(255), + first_name VARCHAR(255), + last_name VARCHAR(255), + ssn VARCHAR(255) +); +CREATE TABLE moon_mission +( + mission_id smallint PRIMARY KEY AUTO_INCREMENT, + spacecraft VARCHAR(255), + launch_date DATE, + carrier_rocket VARCHAR(255), + operator VARCHAR(255), + mission_type VARCHAR(255), + outcome VARCHAR(255) +); +-- +-- Lägger till data i tabellerna för Laboration 1. +-- +INSERT INTO account (password, first_name, last_name, ssn) +VALUES ('8j_]xrCfh#t5,vne', 'Alexandra', 'Truby', '930213-1480'); +INSERT INTO account (password, first_name, last_name, ssn) +VALUES ('g`:+W{\%H9UXqGU4', 'Adna', 'Sandberg', '760723-6085'); +INSERT INTO account (password, first_name, last_name, ssn) +VALUES ('4D3ss?-;MY)9S!y{', 'Daniela', 'Petterson', '810809-3405'); +INSERT INTO account (password, first_name, last_name, ssn) +VALUES ('MB=V4cbAqPz4vqmQ', 'Angela', 'Fransson', '371108-9221'); + +UPDATE account +SET name = CONCAT((SUBSTRING(first_name, 1, 3)), SUBSTRING(last_name, 1, 3)) +WHERE user_id > 0; +INSERT INTO moon_mission +(spacecraft, launch_date, carrier_rocket, operator, mission_type, outcome) +VALUES ('Pioneer 0', '1958-08-17', 'Thor DM-18 Able I', 'USAF', 'Orbiter', 'Launch failure'); +INSERT INTO moon_mission +(spacecraft, launch_date, carrier_rocket, operator, mission_type, outcome) +VALUES ('Luna E-1 No.1', '1958-08-23', 'Luna', 'OKB-1', 'Impactor', 'Launch failure'); +INSERT INTO moon_mission +(spacecraft, launch_date, carrier_rocket, operator, mission_type, outcome) +VALUES ('Pioneer 4', '1959-03-03', 'Juno II', 'NASA', 'Flyby', 'Partial failure'); +INSERT INTO moon_mission +(spacecraft, launch_date, carrier_rocket, operator, mission_type, outcome) +VALUES ('Luna 2', '1959-08-17', 'Thor DM-18 Able I', 'USAF', 'Orbiter', 'Successful'); +INSERT INTO moon_mission +(spacecraft, launch_date, carrier_rocket, operator, mission_type, outcome) +VALUES ('Luna 3', '1959-10-04', 'Luna', 'OKB-1', 'Flyby', 'Successful'); +INSERT INTO moon_mission +(spacecraft, launch_date, carrier_rocket, operator, mission_type, outcome) +VALUES ('Pioneer P-31', '1960-12-15', 'Atles-D Able', 'NASA', 'Orbiter', 'Launch failure'); +INSERT INTO moon_mission +(spacecraft, launch_date, carrier_rocket, operator, mission_type, outcome) +VALUES ('Ranger 7', '1964-07-28', 'Atlas LV-3 Agena-B', 'USAF', 'Orbiter', 'Successful'); +INSERT INTO moon_mission +(spacecraft, launch_date, carrier_rocket, operator, mission_type, outcome) +VALUES ('Lunar Orbiter 4', '1967-05-04', 'Atlas SLVC-3 Agena-D', 'NASA', 'Orbiter', 'Successful'); +INSERT INTO moon_mission +(spacecraft, launch_date, carrier_rocket, operator, mission_type, outcome) +VALUES ('SELENE', '2007-09-14', 'H-IIA 2022', 'JAXA', 'Orbiter', 'Successful'); +INSERT INTO moon_mission +(spacecraft, launch_date, carrier_rocket, operator, mission_type, outcome) +VALUES ('Hiten', '1990-01-24', 'Mu-3S-II', 'ISAS', 'Flyby/Orbiter', 'Successful'); +INSERT INTO moon_mission +(spacecraft, launch_date, carrier_rocket, operator, mission_type, outcome) +VALUES ('Beresheet', '2019-02-22', 'Falcon 9', 'SpaceIL', 'Lander', 'Spacecraft landing failure'); +INSERT INTO moon_mission +(spacecraft, launch_date, carrier_rocket, operator, mission_type, outcome) +VALUES ('Chandrayaan-2', '2019-07-22', 'GSLV Mk III', 'ISRO', 'Orbiter', 'Operational'); +INSERT INTO moon_mission +(spacecraft, launch_date, carrier_rocket, operator, mission_type, outcome) +VALUES ('TESS', '2019-04-18', 'Falcon 9 Full Thrust', 'NASA', 'Gravity assist', 'Successful'); +INSERT INTO moon_mission +(spacecraft, launch_date, carrier_rocket, operator, mission_type, outcome) +VALUES ('Chang''es 5-T1', '2014-10-23', 'Long March 3C', 'CNSA', 'Flyby', 'Successful'); +INSERT INTO moon_mission +(spacecraft, launch_date, carrier_rocket, operator, mission_type, outcome) +VALUES ('Manfred Memorial Moon Mission', '2014-10-23', 'Long March 3C', 'LuxSpace', 'Flyby', 'Successful'); +INSERT INTO moon_mission +(spacecraft, launch_date, carrier_rocket, operator, mission_type, outcome) +VALUES ('Flow', '2011-09-10', 'Delta II 7920H', 'NASA', 'Orbiter', 'Successful'); +INSERT INTO moon_mission +(spacecraft, launch_date, carrier_rocket, operator, mission_type, outcome) +VALUES ('LADEE', '2013-09-07', 'Minotaur V', 'NASA', 'Orbiter', 'Successful'); +INSERT INTO moon_mission +(spacecraft, launch_date, carrier_rocket, operator, mission_type, outcome) +VALUES ('Luna E-1 No.2', '1958-10-11', 'Luna', 'OKB-1', 'Impactor', 'Launch failure'); \ No newline at end of file diff --git a/databas-jdbc-JohanHiths/src/main/resources/myconfig/my.cnf b/databas-jdbc-JohanHiths/src/main/resources/myconfig/my.cnf new file mode 100644 index 0000000..3a37f3a --- /dev/null +++ b/databas-jdbc-JohanHiths/src/main/resources/myconfig/my.cnf @@ -0,0 +1,4 @@ +[mysqld] +# Modern MySQL 9.x no longer supports innodb_log_file_size +innodb_redo_log_capacity=104857600 +sql_mode=STRICT_ALL_TABLES diff --git a/databas-jdbc-JohanHiths/src/test/com/example/CliAppIT.java b/databas-jdbc-JohanHiths/src/test/com/example/CliAppIT.java new file mode 100644 index 0000000..b53e45c --- /dev/null +++ b/databas-jdbc-JohanHiths/src/test/com/example/CliAppIT.java @@ -0,0 +1,341 @@ +package com.example; + +import org.junit.jupiter.api.*; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.PrintStream; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.sql.*; +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * End-to-end CLI tests for the JDBC CRUD console application. + *

+ * Contract expected by these tests (implementation must follow for tests to pass): + * - The app reads menu choices from standard input and writes results to standard output. + * - Menu options: + * 1. List moon missions (read-only from table `moon_mission`) and print spacecraft names. + * 2. Create an account (table `account`): prompts for first name, last name, ssn, password. + * 3. Update an account password: prompts for user_id and new password. + * 4. Delete an account: prompts for user_id to delete. + * 0. Exit program. + * - The app should use these system properties for DB access (configured by the tests): + * APP_JDBC_URL, APP_DB_USER, APP_DB_PASS + * - After each operation the app prints a confirmation message or the read result. + */ +@Testcontainers +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class CliAppIT { + + @Container + private static final MySQLContainer mysql = new MySQLContainer<>("mysql:9.5.0") + .withDatabaseName("testdb") + .withUsername("user") + .withPassword("password") + .withConfigurationOverride("myconfig") + .withInitScript("init.sql"); + + @BeforeAll + static void wireDbProperties() { + System.setProperty("APP_JDBC_URL", mysql.getJdbcUrl()); + System.setProperty("APP_DB_USER", mysql.getUsername()); + System.setProperty("APP_DB_PASS", mysql.getPassword()); + } + + @Test + @Order(0) + void testConnection() throws SQLException { + Connection conn = DriverManager.getConnection( + mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword()); + assertThat(conn).isNotNull(); + } + + @Test + @Order(1) + void login_withInvalidCredentials_showsErrorMessage() throws Exception { + String input = String.join(System.lineSeparator(), + // Expect app to prompt for username then password + "NoUser", // username (invalid) + "badPassword", // password (invalid) + "0" // exit immediately after + ) + System.lineSeparator(); + + String out = runMainWithInput(input); + + assertThat(out) + .containsIgnoringCase("Invalid username or password"); + } + + @Test + @Order(2) + void login_withValidCredentials_thenCanUseApplication() throws Exception { + // Using a known seeded account from init.sql: + // first_name = Angela, last_name = Fransson -> username (name column) = AngFra + // password = MB=V4cbAqPz4vqmQ + String input = String.join(System.lineSeparator(), + "AngFra", // username + "MB=V4cbAqPz4vqmQ", // password + "1", // list missions after successful login + "0" // exit + ) + System.lineSeparator(); + + String out = runMainWithInput(input); + + assertThat(out) + .containsIgnoringCase("username") + .containsIgnoringCase("password") + .as("Expected output to contain at least one known spacecraft from seed data after successful login") + .containsAnyOf("Pioneer 0", "Luna 2", "Luna 3", "Ranger 7"); + } + + @Test + @Order(3) + void listMoonMissions_printsKnownMissionNames() throws Exception { + String input = String.join(System.lineSeparator(), + // login first + "AngFra", + "MB=V4cbAqPz4vqmQ", + "1", // list missions + "0" // exit + ) + System.lineSeparator(); + + String out = runMainWithInput(input); + + assertThat(out) + .as("Expected output to contain at least one known spacecraft from seed data") + .containsAnyOf("Pioneer 0", "Luna 2", "Luna 3", "Ranger 7"); + } + + @Test + @Order(6) + void createAccount_thenCanSeeItInDatabase_andPrintsConfirmation() throws Exception { + // Count rows before to later verify delta via direct JDBC + int before = countAccounts(); + + String input = String.join(System.lineSeparator(), + // login first + "AngFra", + "MB=V4cbAqPz4vqmQ", + "4", // create account (menu option 4 after reordering) + "Ada", // first name + "Lovelace", // last name + "181512-0001", // ssn + "s3cr3t", // password + "0" // exit + ) + System.lineSeparator(); + + String out = runMainWithInput(input); + + assertThat(out) + .containsIgnoringCase("account") + .containsIgnoringCase("created"); + + int after = countAccounts(); + assertThat(after).isEqualTo(before + 1); + } + + @Test + @Order(7) + void updateAccountPassword_thenRowIsUpdated_andPrintsConfirmation() throws Exception { + // Prepare: insert a minimal account row directly + long userId = insertAccount("Test", "User", "111111-1111", "oldpass"); + + String input = String.join(System.lineSeparator(), + // login first + "AngFra", + "MB=V4cbAqPz4vqmQ", + "5", // update password (menu option 5 after reordering) + Long.toString(userId),// user_id + "newpass123", // new password + "0" // exit + ) + System.lineSeparator(); + + String out = runMainWithInput(input); + + assertThat(out) + .containsIgnoringCase("updated"); + + String stored = readPassword(userId); + assertThat(stored).isEqualTo("newpass123"); + } + + @Test + @Order(8) + void deleteAccount_thenRowIsGone_andPrintsConfirmation() throws Exception { + long userId = insertAccount("To", "Delete", "222222-2222", "pw"); + + String input = String.join(System.lineSeparator(), + // login first + "AngFra", + "MB=V4cbAqPz4vqmQ", + "6", // delete account (menu option 6 after reordering) + Long.toString(userId),// user_id + "0" // exit + ) + System.lineSeparator(); + + String out = runMainWithInput(input); + + assertThat(out) + .containsIgnoringCase("deleted"); + + assertThat(existsAccount(userId)).isFalse(); + } + + @Test + @Order(4) + void getMoonMissionById_printsDetails() throws Exception { + // Arrange: use a known mission id from seed data (see init.sql) + // Insert order defines auto-increment ids; 'Luna 3' is the 5th insert -> mission_id = 5 + long missionId = 5L; + + String input = String.join(System.lineSeparator(), + // login first + "AngFra", + "MB=V4cbAqPz4vqmQ", + "2", // menu: get mission by id (reordered to option 2) + Long.toString(missionId), + "0" // exit + ) + System.lineSeparator(); + + String out = runMainWithInput(input); + + assertThat(out) + .as("CLI should print details that include the spacecraft name for the selected mission") + .contains("Luna 3") + .containsIgnoringCase("mission") + .containsIgnoringCase("id"); + } + + @Test + @Order(5) + void countMoonMissionsForYear_printsTotal() throws Exception { + int year = 2019; // Seed data contains several missions in 2019 + int expected = 3; // From init.sql: Beresheet, Chandrayaan-2, TESS + + String input = String.join(System.lineSeparator(), + // login first + "AngFra", + "MB=V4cbAqPz4vqmQ", + "3", // menu: count missions by year (reordered to option 3) + Integer.toString(year), + "0" // exit + ) + System.lineSeparator(); + + String out = runMainWithInput(input); + + assertThat(out) + .as("CLI should print the number of missions for the given year") + .contains(Integer.toString(expected)) + .containsIgnoringCase("missions") + .contains(Integer.toString(year)); + } + + private static String runMainWithInput(String input) throws Exception { + // Capture STDOUT + PrintStream originalOut = System.out; + InputStream originalIn = System.in; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream capture = new PrintStream(baos, true, StandardCharsets.UTF_8); + ByteArrayInputStream bais = new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8)); + System.setOut(capture); + System.setIn(bais); + + try { + // Try to find main(String[]) first, fallback to main() + Class mainClass = Class.forName("com.example.Main"); + Method method = null; + try { + method = mainClass.getDeclaredMethod("main", String[].class); + } catch (NoSuchMethodException ignored) { + try { + method = mainClass.getDeclaredMethod("main"); + } catch (NoSuchMethodException e) { + fail("Expected a main entrypoint in com.example.Main. Define either main(String[]) or main()."); + } + } + method.setAccessible(true); + + // Invoke with a timeout guard (in case the app blocks) + final Method finalMethod = method; + Thread t = new Thread(() -> { + try { + if (finalMethod.getParameterCount() == 1) { + finalMethod.invoke(null, (Object) new String[]{}); + } else { + finalMethod.invoke(null); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + t.start(); + t.join(Duration.ofSeconds(10).toMillis()); + if (t.isAlive()) { + t.interrupt(); + fail("CLI did not exit within timeout. Ensure option '0' exits the program."); + } + + capture.flush(); + return baos.toString(StandardCharsets.UTF_8); + } finally { + System.setOut(originalOut); + System.setIn(originalIn); + } + } + + private static int countAccounts() throws SQLException { + try (Connection c = DriverManager.getConnection(mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword()); + PreparedStatement ps = c.prepareStatement("SELECT count(*) FROM account"); + ResultSet rs = ps.executeQuery()) { + rs.next(); + return rs.getInt(1); + } + } + + private static long insertAccount(String first, String last, String ssn, String password) throws SQLException { + try (Connection c = DriverManager.getConnection(mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword()); + PreparedStatement ps = c.prepareStatement( + "INSERT INTO account(password, first_name, last_name, ssn) VALUES (?,?,?,?)", + Statement.RETURN_GENERATED_KEYS)) { + ps.setString(1, password); + ps.setString(2, first); + ps.setString(3, last); + ps.setString(4, ssn); + ps.executeUpdate(); + try (ResultSet keys = ps.getGeneratedKeys()) { + keys.next(); + return keys.getLong(1); + } + } + } + + private static String readPassword(long userId) throws SQLException { + try (Connection c = DriverManager.getConnection(mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword()); + PreparedStatement ps = c.prepareStatement("SELECT password FROM account WHERE user_id = ?")) { + ps.setLong(1, userId); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) return null; + return rs.getString(1); + } + } + } + + private static boolean existsAccount(long userId) throws SQLException { + try (Connection c = DriverManager.getConnection(mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword()); + PreparedStatement ps = c.prepareStatement("SELECT 1 FROM account WHERE user_id = ?")) { + ps.setLong(1, userId); + try (ResultSet rs = ps.executeQuery()) { + return rs.next(); + } + } + } +} diff --git a/src/main/java/com/example/Account.java b/src/main/java/com/example/Account.java new file mode 100644 index 0000000..4221881 --- /dev/null +++ b/src/main/java/com/example/Account.java @@ -0,0 +1,5 @@ +package com.example; + +public record Account(int userId, String firstName, String lastName, String username, String ssn, String password) { + +} diff --git a/src/main/java/com/example/AccountRepository.java b/src/main/java/com/example/AccountRepository.java new file mode 100644 index 0000000..ba068f5 --- /dev/null +++ b/src/main/java/com/example/AccountRepository.java @@ -0,0 +1,11 @@ +package com.example; + +public interface AccountRepository { + boolean createAccount(String firstName, String lastName, String ssn, String password); + boolean deleteAccount(int userId); + Account findByUsername(String username); + boolean verifyPassword(String username, String rawPassword); + boolean updatePassword(int userId, String hashedPassword); + + +} \ No newline at end of file diff --git a/src/main/java/com/example/JdbcAccountRepository.java b/src/main/java/com/example/JdbcAccountRepository.java new file mode 100644 index 0000000..a21e57f --- /dev/null +++ b/src/main/java/com/example/JdbcAccountRepository.java @@ -0,0 +1,85 @@ +package com.example; + +import java.sql.*; + +public class JdbcAccountRepository implements AccountRepository { + + private final Connection connection; + + public JdbcAccountRepository(Connection connection) { + this.connection = connection; + } + + @Override + public boolean createAccount(String firstName, String lastName, String ssn, String password) { + String sql = """ + INSERT INTO account (first_name, last_name, name, ssn, password) + VALUES (?, ?, ?, ?, ?) + """; + + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, firstName); + ps.setString(2, lastName); + ps.setString(3, firstName + lastName); // username + ps.setString(4, ssn); + ps.setString(5, password); // PLAIN TEXT (required by tests) + return ps.executeUpdate() == 1; + } catch (SQLException e) { + return false; + } + } + + @Override + public Account findByUsername(String username) { + String sql = "SELECT * FROM account WHERE name = ?"; + + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, username); + ResultSet rs = ps.executeQuery(); + + if (rs.next()) { + return new Account( + rs.getInt("user_id"), + rs.getString("first_name"), + rs.getString("last_name"), + rs.getString("name"), + rs.getString("ssn"), + rs.getString("password") + + ); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + return null; + } + + @Override + public boolean verifyPassword(String username, String rawPassword) { + Account acc = findByUsername(username); + return acc != null && acc.password().equals(rawPassword); + } + + @Override + public boolean updatePassword(int userId, String password) { + String sql = "UPDATE account SET password = ? WHERE user_id = ?"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, password); + ps.setInt(2, userId); + return ps.executeUpdate() == 1; + } catch (SQLException e) { + return false; + } + } + + @Override + public boolean deleteAccount(int userId) { + String sql = "DELETE FROM account WHERE user_id = ?"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setInt(1, userId); + return ps.executeUpdate() == 1; + } catch (SQLException e) { + return false; + } + } +} \ 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..cffc9b7 100644 --- a/src/main/java/com/example/Main.java +++ b/src/main/java/com/example/Main.java @@ -1,21 +1,184 @@ package com.example; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; +import java.util.Scanner; +import java.util.List; +import java.sql.*; import java.util.Arrays; public class Main { + public void runApplicationMenu(Connection connection, Scanner scanner) throws SQLException { - static void main(String[] args) { + + MoonMissionRepository missionRepo = new MoonMissionRepositoryJdbc(connection); + AccountRepository accountRepo = new JdbcAccountRepository(connection); + boolean isRunning = true; + while (isRunning) { + System.out.println("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."); + int choice; + + try { + System.out.println("Enter choice"); + choice = Integer.parseInt(scanner.nextLine()); + } catch (NumberFormatException e) { + System.out.println("Invalid choice."); + continue; + } + switch (choice) { + case 0: + isRunning = false; + break; + case 1: + List missions = missionRepo.listAllMissions(); + System.out.println("\nMoon Missions"); + for (MoonMission m : missions) { + System.out.println(m.getSpacecraft()); + } + + break; + case 2: + System.out.print("Enter the mission ID: "); + String input = scanner.nextLine(); + int missionId; + try { + missionId = Integer.parseInt(input); + } catch (NumberFormatException e) { + System.out.println("Invalid mission ID."); + break; + } + + MoonMission mission = missionRepo.findMoonMissionById(missionId); + + if (mission == null) { + System.out.println(" Mission not found."); + } else { + System.out.println("\n--- Mission Details ---"); + System.out.println("ID: " + mission.getMissionId()); + System.out.println("Spacecraft: " + mission.getSpacecraft()); + System.out.println("Launch date: " + mission.getLaunchDate()); + System.out.println("Outcome: " + mission.getOutcome()); + System.out.println("Carrier rocket: " + mission.getCarrierRocket()); + System.out.println("------------------------"); + } + break; + case 3: + int year = 0; + while (true) { + try { + System.out.println("Enter the launch year"); + String yearInput = scanner.nextLine(); + year = Integer.parseInt(yearInput); + break; + } catch (NumberFormatException e) { + System.out.println("Invalid year. Please enter a numeric value."); + } + } + + int count = missionRepo.countMissionsByYear(year); + + System.out.println(count); + break; + + + case 4: + System.out.println("Enter first name"); + String firstName = scanner.nextLine(); + System.out.println("Enter last name"); + String lastName = scanner.nextLine(); + System.out.println("Enter SSN"); + String ssn = scanner.nextLine(); + System.out.println("Enter password"); + String rawPassword = scanner.nextLine(); + + + + boolean accountCreated = accountRepo.createAccount(firstName, lastName, ssn, rawPassword); + + if (accountCreated) { + System.out.println("Account created successfully."); + } else { + System.out.println("Account creation failed."); + } + + break; + case 5: + while (true) { + System.out.println("Enter the usedId to update password"); + String idInput = scanner.nextLine(); + + int userId; + try { + userId = Integer.parseInt(idInput); + } catch (NumberFormatException e) { + System.out.println("Invalid user ID."); + break; + } + + System.out.println("Enter new password"); + String newPassword = scanner.nextLine(); + if (newPassword.equals("0")) { + break; + } + + + + boolean updatePassword = accountRepo.updatePassword(userId, newPassword); + + if (updatePassword) { + System.out.println("updated"); + + } else { + System.out.println("Password update failed."); + break; + } + } + break; + case 6: + System.out.println("Enter user ID to delete!"); + String deleteInput = scanner.nextLine(); + + int deleteId; + try { + deleteId = Integer.parseInt(deleteInput); + } catch (NumberFormatException e) { + System.out.println("Invalid user ID."); + break; + } + boolean deleted = accountRepo.deleteAccount(deleteId); + if (deleted) { + System.out.println("deleted"); + } else { + System.out.println("Account delete failed."); + } + + break; + + } + + } + + } + + + public static void main(String[] args) throws SQLException { if (isDevMode(args)) { DevDatabaseInitializer.start(); } new Main().run(); + + + + } - public void run() { - // Resolve DB settings with precedence: System properties -> Environment variables + public void run() throws SQLException { + + 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"); @@ -27,19 +190,33 @@ public void run() { } try (Connection connection = DriverManager.getConnection(jdbcUrl, dbUser, dbPass)) { + + Scanner scanner = new Scanner(System.in); + AccountRepository accountRepo = new JdbcAccountRepository(connection); + + while (true) { + System.out.print("Username: "); + String username = scanner.nextLine().trim(); + + System.out.print("Password: "); + String password = scanner.nextLine().trim(); + + if (accountRepo.verifyPassword(username, password)) { + System.out.println("Logged in!"); + runApplicationMenu(connection, scanner); + return; + } else { + System.out.println("Invalid username or password"); + } + } + } catch (SQLException e) { - throw new RuntimeException(e); + throw new RuntimeException("Initial database connection failed.", 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 - */ + + private static boolean isDevMode(String[] args) { if (Boolean.getBoolean("devMode")) //Add VM option -DdevMode=true return true; @@ -48,10 +225,7 @@ private static boolean isDevMode(String[] args) { return Arrays.asList(args).contains("--dev"); //Argument --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 +233,5 @@ 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/MoonMission.java b/src/main/java/com/example/MoonMission.java new file mode 100644 index 0000000..f247e4d --- /dev/null +++ b/src/main/java/com/example/MoonMission.java @@ -0,0 +1,69 @@ +package com.example; + + + +import java.sql.Date; + + +public class MoonMission { + private int missionId; + private String spacecraft; + private Date launchDate; + private String carrierRocket; + private String operator; + private String missionType; + private String outcome; + + public MoonMission(int missionId, String spacecraft, Date launchDate, + String carrierRocket, String operator, String missionType, String outcome) { + this.missionId = missionId; + this.spacecraft = spacecraft; + this.launchDate = launchDate; + this.carrierRocket = carrierRocket; + this.operator = operator; + this.missionType = missionType; + this.outcome = outcome; + } +// + + public String getSpacecraft() { + + return spacecraft; + } + + public Date getLaunchDate() { + return launchDate; + } + + public String getCarrierRocket() { + return carrierRocket; + } + + public int getMissionId() { + + return missionId; + } + + public String getMissionName() { + return missionType; + } + + public String getOutcome() { + return outcome; + } + + @Override + public String toString() { + return "MoonMission {" + + "missionId=" + missionId + + ", spacecraft='" + spacecraft + '\'' + + ", launchDate=" + launchDate + + ", carrierRocket='" + carrierRocket + '\'' + + ", operator='" + operator + '\'' + + ", missionType='" + missionType + '\'' + + ", outcome='" + outcome + '\'' + + '}'; + } + + +} \ 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..a6f9539 --- /dev/null +++ b/src/main/java/com/example/MoonMissionRepository.java @@ -0,0 +1,10 @@ +package com.example; + + +import java.util.List; + +public interface MoonMissionRepository { + List listAllMissions(); + MoonMission findMoonMissionById(int missionId); + int countMissionsByYear(int year); +} \ No newline at end of file diff --git a/src/main/java/com/example/MoonMissionRepositoryJdbc.java b/src/main/java/com/example/MoonMissionRepositoryJdbc.java new file mode 100644 index 0000000..2727583 --- /dev/null +++ b/src/main/java/com/example/MoonMissionRepositoryJdbc.java @@ -0,0 +1,84 @@ +package com.example; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +public class MoonMissionRepositoryJdbc implements MoonMissionRepository { + + private final Connection connection; + + public MoonMissionRepositoryJdbc(Connection connection) { + this.connection = connection; + } + + @Override + public List listAllMissions() { + List missions = new ArrayList<>(); + String sql = "SELECT * FROM moon_mission"; + + try (PreparedStatement stmt = connection.prepareStatement(sql); + ResultSet rs = stmt.executeQuery()) { + + while (rs.next()) { + missions.add(new MoonMission( + rs.getInt("mission_id"), + rs.getString("spacecraft"), + rs.getDate("launch_date"), + rs.getString("carrier_rocket"), + rs.getString("operator"), + rs.getString("mission_type"), + rs.getString("outcome") + )); + } + } catch (SQLException e) { + e.printStackTrace(); + } + + return missions; + } + + @Override + public MoonMission findMoonMissionById(int id) { + String sql = "SELECT * FROM moon_mission WHERE mission_id = ?"; + + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setInt(1, id); + + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + return new MoonMission( + rs.getInt("mission_id"), + rs.getString("spacecraft"), + rs.getDate("launch_date"), + rs.getString("carrier_rocket"), + rs.getString("operator"), + rs.getString("mission_type"), + rs.getString("outcome") + ); + } + } + } catch (SQLException e) { + e.printStackTrace(); + } + + return null; + } + + @Override + public int countMissionsByYear(int year) { + String sql = "SELECT COUNT(*) FROM moon_mission WHERE YEAR(launch_date) = ?"; + + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setInt(1, year); + + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) return rs.getInt(1); + } + } catch (SQLException e) { + e.printStackTrace(); + } + + return 0; + } +} \ No newline at end of file diff --git a/src/main/resources/init.sql b/src/main/resources/init.sql index e4ffd06..7de2f27 100644 --- a/src/main/resources/init.sql +++ b/src/main/resources/init.sql @@ -29,7 +29,7 @@ CREATE TABLE moon_mission -- Lägger till data i tabellerna för Laboration 1. -- INSERT INTO account (password, first_name, last_name, ssn) -VALUES ('8j_]xrCfh#t5,vne', 'Alexandra', 'Truby', '930213-1480'); +VALUES ('asd', 'Alexandra', 'Truby', '930213-1480'); INSERT INTO account (password, first_name, last_name, ssn) VALUES ('g`:+W{\%H9UXqGU4', 'Adna', 'Sandberg', '760723-6085'); INSERT INTO account (password, first_name, last_name, ssn) diff --git a/src/test/com/example/CliAppIT.java b/src/test/com/example/CliAppIT.java new file mode 100644 index 0000000..15ba5a8 --- /dev/null +++ b/src/test/com/example/CliAppIT.java @@ -0,0 +1,344 @@ +package com.example; + +import org.junit.jupiter.api.*; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.PrintStream; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.sql.*; +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * End-to-end CLI tests for the JDBC CRUD console application. + *

+ * Contract expected by these tests (implementation must follow for tests to pass): + * - The app reads menu choices from standard input and writes results to standard output. + * - Menu options: + * 1. List moon missions (read-only from table `moon_mission`) and print spacecraft names. + * 2. Create an account (table `account`): prompts for first name, last name, ssn, password. + * 3. Update an account password: prompts for user_id and new password. + * 4. Delete an account: prompts for user_id to delete. + * 0. Exit program. + * - The app should use these system properties for DB access (configured by the tests): + * APP_JDBC_URL, APP_DB_USER, APP_DB_PASS + * - After each operation the app prints a confirmation message or the read result. + */ + +@Testcontainers +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class CliAppIT { + + + + @Container + private static final MySQLContainer mysql = new MySQLContainer<>("mysql:9.5.0") + .withDatabaseName("testdb") + .withUsername("user") + .withPassword("password") + .withConfigurationOverride("myconfig") + .withInitScript("init.sql"); + + @BeforeAll + static void wireDbProperties() { + System.setProperty("APP_JDBC_URL", mysql.getJdbcUrl()); + System.setProperty("APP_DB_USER", mysql.getUsername()); + + } + + @Test + @Order(0) + void testConnection() throws SQLException { + Connection conn = DriverManager.getConnection( + mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword()); + assertThat(conn).isNotNull(); + } + + @Test + @Order(1) + void login_withInvalidCredentials_showsErrorMessage() throws Exception { + String input = String.join(System.lineSeparator(), + // Expect app to prompt for username then password + "NoUser", // username (invalid) + "badPassword", // password (invalid) + "0" // exit immediately after + ) + System.lineSeparator(); + + String out = runMainWithInput(input); + + assertThat(out) + .containsIgnoringCase("Invalid username or password"); + } + + @Test + @Order(2) + void login_withValidCredentials_thenCanUseApplication() throws Exception { + // Using a known seeded account from init.sql: + // first_name = Angela, last_name = Fransson -> username (name column) = AngFra + // password = MB=V4cbAqPz4vqmQ + String input = String.join(System.lineSeparator(), + "AngFra", // username + "MB=V4cbAqPz4vqmQ", // password + "1", // list missions after successful login + "0" // exit + ) + System.lineSeparator(); + + String out = runMainWithInput(input); + + assertThat(out) + .containsIgnoringCase("username") + .containsIgnoringCase("password") + .as("Expected output to contain at least one known spacecraft from seed data after successful login") + .containsAnyOf("Pioneer 0", "Luna 2", "Luna 3", "Ranger 7"); + } + + @Test + @Order(3) + void listMoonMissions_printsKnownMissionNames() throws Exception { + String input = String.join(System.lineSeparator(), + // login first + "AngFra", + "MB=V4cbAqPz4vqmQ", + "1", // list missions + "0" // exit + ) + System.lineSeparator(); + + String out = runMainWithInput(input); + + assertThat(out) + .as("Expected output to contain at least one known spacecraft from seed data") + .containsAnyOf("Pioneer 0", "Luna 2", "Luna 3", "Ranger 7"); + } + + @Test + @Order(6) + void createAccount_thenCanSeeItInDatabase_andPrintsConfirmation() throws Exception { + // Count rows before to later verify delta via direct JDBC + int before = countAccounts(); + + String input = String.join(System.lineSeparator(), + // login first + "AngFra", + "MB=V4cbAqPz4vqmQ", + "4", // create account (menu option 4 after reordering) + "Ada", // first name + "Lovelace", // last name + "181512-0001", // ssn + "s3cr3t", // password + "0" // exit + ) + System.lineSeparator(); + + String out = runMainWithInput(input); + + assertThat(out) + .containsIgnoringCase("account") + .containsIgnoringCase("created"); + + int after = countAccounts(); + assertThat(after).isEqualTo(before + 1); + } + + @Test + @Order(7) + void updateAccountPassword_thenRowIsUpdated_andPrintsConfirmation() throws Exception { + // Prepare: insert a minimal account row directly + long userId = insertAccount("Test", "User", "111111-1111", "oldpass"); + + String input = String.join(System.lineSeparator(), + // login first + "AngFra", + "MB=V4cbAqPz4vqmQ", + "5", // update password (menu option 5 after reordering) + Long.toString(userId),// user_id + "newpass123", // new password + "0" // exit + ) + System.lineSeparator(); + + String out = runMainWithInput(input); + + assertThat(out) + .containsIgnoringCase("updated"); + + String stored = readPassword(userId); + assertThat(stored).isEqualTo("newpass123"); + } + + @Test + @Order(8) + void deleteAccount_thenRowIsGone_andPrintsConfirmation() throws Exception { + long userId = insertAccount("To", "Delete", "222222-2222", "pw"); + + String input = String.join(System.lineSeparator(), + // login first + "AngFra", + "MB=V4cbAqPz4vqmQ", + "6", // delete account (menu option 6 after reordering) + Long.toString(userId),// user_id + "0" // exit + ) + System.lineSeparator(); + + String out = runMainWithInput(input); + + assertThat(out) + .containsIgnoringCase("deleted"); + + assertThat(existsAccount(userId)).isFalse(); + } + + @Test + @Order(4) + void getMoonMissionById_printsDetails() throws Exception { + // Arrange: use a known mission id from seed data (see init.sql) + // Insert order defines auto-increment ids; 'Luna 3' is the 5th insert -> mission_id = 5 + long missionId = 5L; + + String input = String.join(System.lineSeparator(), + // login first + "AngFra", + "MB=V4cbAqPz4vqmQ", + "2", // menu: get mission by id (reordered to option 2) + Long.toString(missionId), + "0" // exit + ) + System.lineSeparator(); + + String out = runMainWithInput(input); + + assertThat(out) + .as("CLI should print details that include the spacecraft name for the selected mission") + .contains("Luna 3") + .containsIgnoringCase("mission") + .containsIgnoringCase("id"); + } + + @Test + @Order(5) + void countMoonMissionsForYear_printsTotal() throws Exception { + int year = 2019; // Seed data contains several missions in 2019 + int expected = 3; // From init.sql: Beresheet, Chandrayaan-2, TESS + + String input = String.join(System.lineSeparator(), + // login first + "AngFra", + "MB=V4cbAqPz4vqmQ", + "3", // menu: count missions by year (reordered to option 3) + Integer.toString(year), + "0" // exit + ) + System.lineSeparator(); + + String out = runMainWithInput(input); + + assertThat(out) + .as("CLI should print the number of missions for the given year") + .contains(Integer.toString(expected)) + .containsIgnoringCase("missions") + .contains(Integer.toString(year)); + } + + private static String runMainWithInput(String input) throws Exception { + // Capture STDOUT + PrintStream originalOut = System.out; + InputStream originalIn = System.in; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream capture = new PrintStream(baos, true, StandardCharsets.UTF_8); + ByteArrayInputStream bais = new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8)); + System.setOut(capture); + System.setIn(bais); + + try { + // Try to find main(String[]) first, fallback to main() + Class mainClass = Class.forName("com.example.Main"); + Method method = null; + try { + method = mainClass.getDeclaredMethod("main", String[].class); + } catch (NoSuchMethodException ignored) { + try { + method = mainClass.getDeclaredMethod("main"); + } catch (NoSuchMethodException e) { + fail("Expected a main entrypoint in com.example.Main. Define either main(String[]) or main()."); + } + } + method.setAccessible(true); + + // Invoke with a timeout guard (in case the app blocks) + final Method finalMethod = method; + Thread t = new Thread(() -> { + try { + if (finalMethod.getParameterCount() == 1) { + finalMethod.invoke(null, (Object) new String[]{}); + } else { + finalMethod.invoke(null); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + t.start(); + t.join(Duration.ofSeconds(10).toMillis()); + if (t.isAlive()) { + t.interrupt(); + fail("CLI did not exit within timeout. Ensure option '0' exits the program."); + } + + capture.flush(); + return baos.toString(StandardCharsets.UTF_8); + } finally { + System.setOut(originalOut); + System.setIn(originalIn); + } + } + + private static int countAccounts() throws SQLException { + try (Connection c = DriverManager.getConnection(mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword()); + PreparedStatement ps = c.prepareStatement("SELECT count(*) FROM account"); + ResultSet rs = ps.executeQuery()) { + rs.next(); + return rs.getInt(1); + } + } + + private static long insertAccount(String first, String last, String ssn, String password) throws SQLException { + try (Connection c = DriverManager.getConnection(mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword()); + PreparedStatement ps = c.prepareStatement( + "INSERT INTO account(password, first_name, last_name, ssn) VALUES (?,?,?,?)", + Statement.RETURN_GENERATED_KEYS)) { + ps.setString(1, password); + ps.setString(2, first); + ps.setString(3, last); + ps.setString(4, ssn); + ps.executeUpdate(); + try (ResultSet keys = ps.getGeneratedKeys()) { + keys.next(); + return keys.getLong(1); + } + } + } + + private static String readPassword(long userId) throws SQLException { + try (Connection c = DriverManager.getConnection(mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword()); + PreparedStatement ps = c.prepareStatement("SELECT password FROM account WHERE user_id = ?")) { + ps.setLong(1, userId); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) return null; + return rs.getString(1); + } + } + } + + private static boolean existsAccount(long userId) throws SQLException { + try (Connection c = DriverManager.getConnection(mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword()); + PreparedStatement ps = c.prepareStatement("SELECT 1 FROM account WHERE user_id = ?")) { + ps.setLong(1, userId); + try (ResultSet rs = ps.executeQuery()) { + return rs.next(); + } + } + } +}