Add custom-validation, etag-caching, restclient-basic-auth: Boot 4.1 API pass

Three companion modules verifying and rewriting the Boot 4.1.1 / Framework
7.0.9 story for three older articles: the javax->jakarta.validation namespace
fix plus Jakarta Validation 3.1 record-validation clarification, ETag/
conditional-request APIs re-verified unchanged plus the starter rename, and
RestTemplate Basic Auth rebuilt on RestClient with the exchange() trap called
out. 19 real passing tests generate every transcript quoted from the three
companion articles.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EQNA6DJ9VgCtW6zhCE8Xud
This commit is contained in:
Claude
2026-09-19 10:17:09 +00:00
parent 03bdf7ee87
commit e4b5636f7c
75 changed files with 2374 additions and 0 deletions
@@ -0,0 +1,5 @@
Class: jakarta.validation.Validation
Jar file: jakarta.validation-api-3.1.1.jar
Bundle-SymbolicName: jakarta.validation.jakarta.validation-api
Bundle-Version: 3.1.1
Implementation-Version: null
@@ -0,0 +1,4 @@
Class: org.hibernate.validator.internal.engine.ValidatorFactoryImpl
Jar file: hibernate-validator-9.1.3.Final.jar
Implementation-Title: hibernate-validator
Implementation-Version: 9.1.3.Final
@@ -0,0 +1,6 @@
$ curl -s -X POST localhost:8080/contact \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","message":"This is spam."}'
HTTP status: 400
Body: {"message":"Message contains 'spam' and must be at least 50 characters long."}
@@ -0,0 +1,6 @@
$ curl -s -X POST localhost:8080/contact \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","message":"This is a legitimate message about an issue I'm facing."}'
HTTP status: 200
Body: Contact form submitted successfully!
@@ -0,0 +1,12 @@
$ curl -s -i -X POST localhost:8080/contact-record \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","message":"This is spam."}'
HTTP status: 400
Body: '' (empty!)
# The record parameter has no BindingResult to collect field errors into, unlike
# ContactController#submitContactForm's class-based, BindingResult-carrying signature.
# A failing constraint throws MethodArgumentNotValidException instead, and Boot 4.1's
# default handling for it returns an EMPTY body when the request has no Accept header
# asking for a structured error. See the next transcript for what changes with one.
@@ -0,0 +1,12 @@
$ curl -s -X POST localhost:8080/contact-record \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
-d '{"email":"[email protected]","message":"This is spam."}'
HTTP status: 400
Content-Type: null
Body: '' (still empty!)
# An Accept header alone changes nothing -- the body is still empty. What actually turns
# on a structured error body is the spring.mvc.problemdetails.enabled property, which is
# off by default and unrelated to content negotiation. See
# docs/output/07-problemdetails-enabled-record-rejected.txt for the same request with it on.
@@ -0,0 +1,6 @@
$ curl -s -X POST localhost:8080/booking \
-H 'Content-Type: application/json' \
-d '{"startDate":"2026-05-10","endDate":"2026-05-01"}'
HTTP status: 400
Body:
@@ -0,0 +1,10 @@
$ curl -s -X POST localhost:8080/booking-record \
-H 'Content-Type: application/json' \
-d '{"startDate":"2026-05-10","endDate":"2026-05-01"}'
HTTP status: 400
Body:
# Class-level @DateRangeValid, placed on the record's type declaration exactly as it would
# be on a class, is honoured the same way. The validator reads booking.startDate() /
# booking.endDate() (accessor methods) instead of getStartDate()/getEndDate().
@@ -0,0 +1,6 @@
$ curl -s -X POST localhost:8080/booking-record \
-H 'Content-Type: application/json' \
-d '{"startDate":"2026-05-01","endDate":"2026-05-10"}'
HTTP status: 200
Body: Booking accepted: 2026-05-01 -> 2026-05-10
@@ -0,0 +1,18 @@
# application.yaml: spring.mvc.problemdetails.enabled: true (opt-in; unrelated to Bean
# Validation 3.1 itself -- this property already existed in Boot 3)
$ curl -s -X POST localhost:8080/contact-record \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","message":"This is spam."}'
HTTP status: 400
Content-Type: application/problem+json
Body: {"detail":"Invalid request content.","instance":"/contact-record","status":400,"title":"Bad Request"}
# Progress over the empty body, but notice what is MISSING: no mention of "spam", no field
# name. Boot's default MethodArgumentNotValidException -> ProblemDetail mapping fills in
# only the generic RFC 9457 fields (title, status, detail="Invalid request content.").
# Per-field messages -- what the class-based /contact endpoint hand-rolls from
# bindingResult.getFieldErrors() -- need a custom @ExceptionHandler that does the same
# thing into the ProblemDetail's own "properties" map. Turning the property on is not,
# by itself, a drop-in replacement for BindingResult-based error reporting.