Add library-management module: console app evolved into a Spring Boot 4 REST API

Stage 2 console classes and the shared LoanPolicy, then a Spring Boot 4.1.2 + JPA API
(H2, @Version optimistic locking, problem+json errors). Tests write output/ transcripts
while asserting the same numbers; scripts regenerate everything.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017ic4b4DPL6dtpFg4U4LHc2
This commit is contained in:
Claude
2026-11-14 21:21:16 +05:30
parent b9aeb78b24
commit 26ece9cc9b
38 changed files with 1444 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Drives a running API (scripts/run.sh --detach) with curl: the same story as the console demo,
# against a real servlet container. Dates in the output are "today", so they differ per run.
set -uo pipefail
BASE=${BASE:-http://localhost:8080/api}
call() { # method path [json]
echo "> $1 $2${3:+ $3}"
if [ -n "${3:-}" ]; then
curl -s -X "$1" "$BASE$2" -H 'Content-Type: application/json' -d "$3" -w '\n< HTTP %{http_code}\n\n'
else
curl -s -X "$1" "$BASE$2" -w '\n< HTTP %{http_code}\n\n'
fi
}
call POST /books '{"isbn":"978-0134685991","title":"Effective Java","author":"Joshua Bloch","copies":2}'
call POST /books '{"isbn":"978-0596009205","title":"Head First Java","author":"Kathy Sierra","copies":1}'
call POST /books '{"isbn":"","title":"","author":"Nobody","copies":0}'
call POST /members '{"name":"Asha","email":"[email protected]"}'
call POST /members '{"name":"Ravi","email":"not-an-email"}'
call POST /members '{"name":"Ravi","email":"[email protected]"}'
call POST /loans '{"memberId":1,"bookId":2}'
call POST /loans '{"memberId":2,"bookId":2}'
call POST /loans/1/return
call GET /books
+14
View File
@@ -0,0 +1,14 @@
add-book 978-0134685991|Effective Java|Joshua Bloch|2
add-book 978-0596009205|Head First Java|Kathy Sierra|1
add-book 978-0134685991|Effective Java|Joshua Bloch|2
add-member Asha
add-member Ravi
borrow 978-0596009205 1 2026-11-01
borrow 978-0596009205 2 2026-11-01
borrow 978-0134685991 1 2026-11-01
report
return 1 2026-11-20
return 1 2026-11-21
borrow 978-0596009205 2 2026-11-21
report
quit
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# Starts the API with NO application.properties, to capture the warning Spring Boot prints when
# spring.jpa.open-in-view is left at its default. Our real config sets it to false explicitly.
set -euo pipefail
cd "$(dirname "$0")/.."
mvn -B -q -DskipTests package
mkdir -p /tmp/empty-config
timeout 25 java -jar target/library-management-1.0.0.jar --spring.config.location=file:/tmp/empty-config/ --server.port=0 2>&1 \
| grep -E 'open-in-view|Started LibraryApplication' | sed -E 's/^[0-9T:.+-]+ +//' || true
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Regenerates every file in output/. Needs JDK 25 and Maven; the first run must be online.
set -euo pipefail
cd "$(dirname "$0")/.."
rm -f output/*.txt
# 01: the single-file console version (the copy attached to the article lives in the WordPress media library)
mkdir -p /tmp/lm && cp scripts/demo-input.txt /tmp/lm/
echo "01 console (structured) ..."
mvn -B -q -DskipTests compile
java -cp target/classes com.ankurm.library.console.ConsoleApp < scripts/demo-input.txt 2>/dev/null > output/01-console-structured.txt
# 02-06: written by the tests themselves, so each file is also an assertion
echo "02-06 tests ..."
mvn -B -q test
# 07: the real server, driven with curl
echo "07 curl demo ..."
scripts/run.sh --detach
scripts/api-demo.sh > output/07-curl-demo.txt
scripts/stop.sh
# 08: the Boot default we override
echo "08 open-in-view warning ..."
scripts/open-in-view-warning.sh > output/08-open-in-view-warning.txt
# 09: the SQL one borrow produces
echo "09 borrow SQL ..."
scripts/sql-trace.sh > output/09-borrow-sql.txt
ls output
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# Starts the REST API on :8080 (in-memory H2, so every start is a clean library).
# Usage: scripts/run.sh foreground
# scripts/run.sh --detach background, waits until /api/books answers
set -euo pipefail
cd "$(dirname "$0")/.."
mvn -B -q -DskipTests package
if [ "${1:-}" = "--detach" ]; then
setsid nohup java -jar target/library-management-1.0.0.jar > /tmp/library-api.log 2>&1 < /dev/null &
for _ in $(seq 1 60); do curl -s -o /dev/null localhost:8080/api/books && exit 0; sleep 1; done
echo "API did not start; see /tmp/library-api.log" >&2; exit 1
fi
exec java -jar target/library-management-1.0.0.jar
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# Starts the API with Hibernate SQL logging, performs one borrow, and prints only the SQL that
# the borrow produced. Look for "version" in the UPDATE: that is optimistic locking at work.
set -euo pipefail
cd "$(dirname "$0")/.."
mvn -B -q -DskipTests package
LOG=/tmp/library-sql.log
setsid nohup java -jar target/library-management-1.0.0.jar --server.port=8081 --logging.level.org.hibernate.SQL=DEBUG > "$LOG" 2>&1 < /dev/null &
for _ in $(seq 1 60); do curl -s -o /dev/null localhost:8081/api/books && break; sleep 1; done
J='Content-Type: application/json'
curl -s -o /dev/null -H "$J" -d '{"isbn":"978-0134685991","title":"Effective Java","author":"Joshua Bloch","copies":2}' localhost:8081/api/books
curl -s -o /dev/null -H "$J" -d '{"name":"Asha","email":"[email protected]"}' localhost:8081/api/members
START=$(wc -l < "$LOG")
curl -s -o /dev/null -H "$J" -d '{"memberId":1,"bookId":1}' localhost:8081/api/loans
sleep 1
tail -n +"$((START + 1))" "$LOG" | grep 'org.hibernate.SQL' | sed -E 's/^.*org.hibernate.SQL +: //'
scripts/stop.sh
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
# Stops the API started by run.sh --detach. Only processes whose command is "java" match, so
# this script can never match (and kill) the shell that is running it.
for p in $(ps -eo pid,comm,args | awk '$2 == "java" && /library-management-1.0.0.jar/ {print $1}'); do kill "$p"; done