chore(deps): [SPIKE/DRAFT] upgrade to Spring Framework 7 (+ Security 7 / Data Redis 4 / Session 4 / JUnit 6)#24087
chore(deps): [SPIKE/DRAFT] upgrade to Spring Framework 7 (+ Security 7 / Data Redis 4 / Session 4 / JUnit 6)#24087stian-sandvold wants to merge 28 commits into
Conversation
…n through dhis-service-core) spring.version 6.2.17 -> 7.0.7. Fixes the mechanical Spring 7 removals so dhis-api + dhis-support-* + dhis-service-core compile (main + tests): - HttpHeaders no longer implements Map/MultiValueMap: retype AuthScheme.apply (+ RouteService.applyAuthScheme) headers param Map<String,List<String>> -> HttpHeaders; impl bodies use add()/put(); RouteService entrySet()->headerSet(), keySet()->headerNames(); tests use containsHeader/getFirst/getOrEmpty. - AsyncListenableTaskExecutor removed: DefaultAsyncTaskExecutor -> AsyncTaskExecutor + submitCompletable (no production callers). - ListenableFutureCallback removed: MessageSendingCallback -> BiConsumer (dead code). - UriComponentsBuilder.fromHttpUrl removed -> fromUriString (SimplisticHttpGetGateWay). NOT done (blocks full build/validation): dhis-web-api MVC behavioral removals (setUseSuffixPatternMatch/setUseRegisteredSuffixPatternMatch/setUseTrailingSlashMatch on CustomRequestMappingHandlerMapping -> affects .json/.xml + trailing-slash routing); runtime ecosystem coupling (spring-security 6.5 + Spring 7, spring-data-redis 2.7 + Spring 7). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed MVC flags Spring 7.0 removed all path-extension content-negotiation support (setUseSuffixPatternMatch/RegisteredSuffix, favorPathExtension, PathExtensionContentNegotiationStrategy base, PatternsRequestCondition suffix booleans) and the default PathPattern matcher never supported suffixes. Reinstate DHIS2's /api/x.json behaviour at the edge instead: - MediaTypeSuffixFilter (new): for a registered extension (MEDIA_TYPE_MAP), stashes the resolved MediaType in a request attribute and wraps the request to strip the suffix so PathPattern handler mappings match. Registered after Spring Security (so security still sees the original suffix), mapped /api/*. - SuffixMediaTypeContentNegotiationStrategy (new): reads that attribute; replaces CustomPathExtensionContentNegotiationStrategy (deleted - base class removed). - WebMvcConfig + MvcTestConfig: use the new strategy; drop removed setters/flags. - CustomRequestMappingHandlerMapping: drop removed PatternsRequestCondition suffix booleans; keep force-GET via mutate(). (Version-range routing is unaffected - handled by ApiVersionFilter.) TODO: trailing-slash matching for /api/** (setUseTrailingSlashMatch removed); wire MediaTypeSuffixFilter into MockMvc test setup for suffix-based controller tests. Not yet compiled past web-api (Security 7 / SDR 4 still pending). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- MockMvc request-builder hierarchy redesign (Spring 6.2/7): MockMultipart no longer extends MockHttpServletRequestBuilder. Widen perform()/webRequestWithMvcResult()/ webRequestWithAsyncMvcResult() params to AbstractMockHttpServletRequestBuilder<?>; retype the local in RouteControllerTest to MockMultipartHttpServletRequestBuilder. - StatusResultMatchers.isMovedTemporarily() removed -> isFound() (StaticContentControllerTest). - HttpHeaders.containsKey -> containsHeader (MetadataExportControllerUtilsTest). All MAIN source across the whole reactor (incl. dhis-web-server WAR, dhis-test-integration) already compiles on Spring 7; these are the remaining dhis-test-web-api test-compile breaks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ride to AbstractMockHttpServletRequestBuilder Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… / Session 4.0.3; drop dead spring-mobile-device - spring-security 6.5.10 -> 7.0.5; spring-authorization-server -> 7.0.5 (now versioned with Spring Security in the Boot 4 generation) - spring-data-redis 2.7.18 -> 4.0.1 (Spring 7 / Boot 4 line; keeps lettuce 6.8.x. Lettuce 7 still needs SDR 4.1 -> remains task 03) - spring-session-data-redis 2.7.4 -> 4.0.3 (aligns with spring-session-core 4.0.3) - removed spring-mobile-device 1.1.5.RELEASE: dead project (Spring 4 era), declared only in dependencyManagement, no module depends on it, zero org.springframework.mobile usage in code. Expected to introduce a Security-7 DSL + SDR-4 API compile-break wave (next). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Security 7 added a Builder ctor) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…access.vote) Spring Security 7.0 removed the AccessDecisionVoter/AccessDecisionManager model (org.springframework.security.access.vote). AllRequiredRoleVoter extended the removed RoleVoter and is referenced nowhere (no Java, no XML, no AccessDecisionManager wiring) - dead code. Removed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…estClientAuthorizationCodeTokenResponseClient Spring Security 7.0 removed the RequestEntity-converter / RestOperations OAuth2 token-client infrastructure (OAuth2AuthorizationCodeGrantRequestEntityConverter, the RestTemplate-based DefaultAuthorizationCodeTokenResponseClient pattern). Delegate to RestClientAuthorizationCodeTokenResponseClient and register the NimbusJwtClientAuthenticationParametersConverter via addParametersConverter — it self-gates on PRIVATE_KEY_JWT, so per-request converter selection is no longer needed. private_key_jwt JWK + jwkSetUrl customization preserved. Default RestClient provides the message converters + OAuth2 error handling the old code configured explicitly. NEEDS OIDC runtime validation + security review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…vice via super() ctor Security 7.0 removed DaoAuthenticationProvider's no-arg ctor + setUserDetailsService(); supply UserDetailsService via super(detailsService). setPasswordEncoder retained. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Matcher - Spring Authorization Server merged into spring-security-config (Security 7): move OAuth2AuthorizationServerConfiguration/Configurer imports to the new org.springframework.security.config.annotation.web.* packages. - ObjectPostProcessor moved org.springframework.security.config.annotation -> org.springframework.security.config. - AntPathRequestMatcher removed in Security 7 (PathPattern can't express mid-pattern /**/). Added a faithful drop-in org.hisp.dhis.webapi.security.AntPathRequestMatcher backed by Spring AntPathMatcher (servletPath+pathInfo, case-sensitive) to preserve exact request-authorization behaviour; swapped imports in DhisWebApiWebSecurityConfig + AuthenticationApiTestBase (35 call sites unchanged). NEEDS security review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rverConfig to lambda DSL; SDR4/Session4 - Security 7 removed the non-lambda HttpSecurity DSL (.and() chaining): rewrote requestCache/headers/httpBasic/oauth2Login/tokenEndpoint/authorizationEndpoint/ exceptionHandling/logout/sessionManagement to Customizer lambdas. - OAuth2AuthorizationServerConfigurer.authorizationServer() factory removed -> new ctor. - AbstractAuthenticationToken(null) ambiguity (new Builder ctor) -> cast null to Collection<? extends GrantedAuthority> at the 2 remaining sites. - Spring Session 4 / SDR 4: RedisIndexedSessionRepository now needs RedisOperations<String,Object> -> RedisTemplate<String,Object>. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ckMvc JSON-escapes cause quotes) On master the standalone MockMvc rendered the type-mismatch WebMessage cause tail with literal quotes; under Spring 7 the standalone setup JSON-escapes the embedded quotes (For input string: "10.5"), so the literal-quote containsString failed. Production behaviour is unchanged (always a JSON WebMessage, 400 + descriptive msg). Assert the stable, quote-free prefix instead of the framework-dependent cause tail. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…JUnit Jupiter 6.0+) Spring Framework 7.0's SpringExtension calls JUnit Jupiter 6 API (ExtensionContext.Store.computeIfAbsent(Object,Function,Class)); JUnit 5.12 lacks it, causing NoSuchMethodError on every SpringExtension-based (context-loading) test. Bump junit-bom to 6.0.3. Whole reactor still test-compiles clean (mockito 5.18 + surefire 3.5.3 are JUnit-6 compatible). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…struct) REAL Spring 7 regression found via context-startup test (RequestIdFilterTest passes on master, failed on Spring 7 with 'Password cannot be set empty'). Spring Framework 7.0 may invoke @bean factory methods on a @configuration class BEFORE that class's @PostConstruct runs, so HibernateEncryptionConfig's password field (set in @PostConstruct, used by the tripleDes/aes128 encryptor @bean methods) was still null. Switch to constructor injection so the password is resolved before any @bean method. NOTE for review: other @configuration classes using the @PostConstruct-sets-field / @Bean-uses-field pattern may have the same latent issue under Spring 7. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…roller mappings Spring 6.0 made PathPatternParser the default request-matching engine. DHIS2's controllers relied on AntPathMatcher (via the old PatternsRequestCondition that the suffix-flag config used), notably mid-pattern '**'. Under the default PathPattern many mappings stopped matching -> widespread 404 in the controller suite. Keep AntPathMatcher via setPatternParser(null) in WebMvcConfig + MvcTestConfig until the controllers are migrated to PathPattern (deferred). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s setUseTrailingSlashMatch) Spring 7.0 removed setUseTrailingSlashMatch; reinstate it for /api/** by stripping a single trailing slash in the filter so e.g. POST /api/categoryOptions/ matches the /api/categoryOptions mapping (was 404). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…st injection) Spring 7 is stricter about @Autowired on static members: the spurious @Autowired on TestBase.categoryService (a static field actually populated by initServices() from the injected instance field) broke autowiring/@PostConstruct for the whole test instance, leaving categoryService (and friends) null -> NPE in setupCategoryMetadata across many tests. Remove the static @Autowired; the @PostConstruct copy already sets it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MockMvc controller tests build their own filter chain and didn't include the new MediaTypeSuffixFilter, so .json/.xml content negotiation + trailing-slash matching (which Spring 7 removed from the handler mapping and we reinstate via the filter in production) were absent in tests -> e.g. POST /api/categoryOptions/ -> 404. Add the filter to ControllerIntegrationTestBase MockMvc setup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ring.test.extension.context.scope=test_class) Spring 7.0 switched SpringExtension to a test-METHOD-scoped ExtensionContext by default, which breaks @PostConstruct-populated fixtures (e.g. TestBase's static categoryService/configurationService/periodService -> null -> NPE across many integration tests). Restore the pre-7 test-class scope globally via the documented system property in surefireArgLine. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…> jakarta (40 files) ROOT CAUSE of the @PostConstruct regressions: Spring 6.2 honoured BOTH javax.annotation and jakarta.annotation @PostConstruct/@PreDestroy; Spring 7 only honours jakarta.annotation.*. So all javax-annotated lifecycle callbacks silently stopped firing under Spring 7 (e.g. HibernateEncryptionConfig password init, and TestBase.initServices() populating the static categoryService/configurationService/ periodService used across the integration test suite -> NPEs). Migrate all 40 javax.annotation.PostConstruct/PreDestroy imports to jakarta.annotation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ation-api (3.0.0) Companion to the @PostConstruct/@PreDestroy import migration: the javax annotations came from an explicitly-declared javax.annotation:javax.annotation-api (1.3.2) in 16 module poms (not transitive from Spring). Swap to jakarta.annotation:jakarta.annotation-api 3.0.0 (managed) so the jakarta imports resolve; update the stale dependency:analyze ignore entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #24087 +/- ##
=============================================
- Coverage 69.04% 46.63% -22.41%
- Complexity 709 1002 +293
=============================================
Files 3684 3663 -21
Lines 141646 140513 -1133
Branches 16453 16347 -106
=============================================
- Hits 97797 65530 -32267
- Misses 36243 68602 +32359
+ Partials 7606 6381 -1225
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 1610 files with indirect coverage changes Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
…ntext, dependency:analyze Fixes found via the draft PR's CI run: - @EnableGlobalMethodSecurity -> @EnableMethodSecurity (WebMvcConfig): the old annotation's MethodSecurityMetadataSourceAdvisor was removed in Security 7 -> NoClassDefFoundError -> app context failed to start (api-test) and many integration-h2 controller tests errored. - hibernate.current_session_context_class: org.springframework.orm.hibernate5. SpringSessionContext -> org.springframework.orm.jpa.hibernate.SpringSessionContext (Spring 7 moved the class; it's a runtime class-name string -> ClassNotFoundException). - dependency:analyze: declare spring-web in dhis-api (AuthScheme now uses HttpHeaders); ignore javaagent-only java-allocation-instrumenter in dhis-support-hibernate; drop unused-declared spring-core from dhis-service-data-exchange. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er mapping (literal-first) Replace the blind MediaTypeSuffixFilter with literal-first matching in CustomRequestMappingHandlerMapping.getHandlerInternal: try the path as-is (so controllers that map an extension literally, e.g. OpenApiController's /openapi/openapi.json, keep working), then fall back to matching the path with a trailing slash and/or a registered media-type extension removed (recording the media type for SuffixMediaTypeContentNegotiationStrategy). Fixes OpenApiControllerTest (10/10) and trailing-slash routing without a separate filter. Removed MediaTypeSuffixFilter + its registrations. Known residual: 2 DataSetControllerTest metadata-download tests (.json/.json.zip Content-Disposition filename) still fail - the export converter's format/filename derivation needs the path extension; tracked as follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oofKey) Spring Authorization Server 7 changed ClientSettings.builder()'s default for requireProofKey from false to true (PKCE-by-default). DHIS2's existing OAuth2 clients and the authorization-code flow do not send a PKCE code_challenge, so the new default made /oauth2/authorize fail with invalid_request and bounce the browser to the redirect_uri (e2e OAuth2Test: net::ERR_CONNECTION_REFUSED, 11 errors). Explicitly set requireProofKey(false) in applyCreateDefaults to preserve prior (SAS 1.x) behaviour. SECURITY REVIEW: decide whether to adopt PKCE-by-default for DHIS2 OAuth2 clients (recommended) and migrate clients/flows accordingly. Verified: new client stores require-proof-key=false and /oauth2/authorize now redirects to /login/ instead of erroring. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|




Spring Framework 7 upgrade — spike / draft for review
What this branch does
Upgrades
spring.version6.2.17 → 7.0.7 and everything that is version-locked to it, then fixesevery resulting compile + the runtime breakage found so far.
✅ Validated locally
mvn clean test-compile), main and tests — 0 errors.unit-testprofile — 678/678 pass (1 brittle assertion adjusted, see below).RequestIdFilterTest) boots the Security‑7 filterchain + MVC + content‑negotiation + Spring Data Redis 4 + Spring Session 4 → passes. This is
what surfaced the one real runtime regression (encryption config, below).
integration-test, the fullintegration-h2, andapi-teste2e suites. CI on this PR is thefirst real run of those.
1. What must be upgraded together with Spring Framework 7
Spring 7 is version-locked to a whole generation. These all had to move in lockstip (a lone
spring.versionbump does not compile or run):spring.version(Framework)spring-security.versionspring-data-redis.versionspring-session-data-redis.versionspring-session-core4.0.3 (already pinned)spring-authorization-server.versionjunit.version(junit-bom)SpringExtensionrequires JUnit Jupiter 6.0+javax.annotation:javax.annotation-apijakarta.annotation:jakarta.annotation-api3.0.0jakarta.annotationJSR‑250 (see §3a)spring-mobile-deviceAlready in place (no change needed): Spring Boot 4.0.x (tomcat starter + boot plugin), Java 17
baseline.
spring-retry/spring-ldap/spring-restdocscompiled unchanged (note for review).Still NOT upgradable here: lettuce 7 — needs Spring Data Redis 4.1+; this branch is on
SDR 4.0.x (→ lettuce 6.8.x). Lettuce 7 remains a follow-up (see plan Phase 3).
2. Code that no longer works and was replaced (Spring 7 removals)
HttpHeadersno longer implementsMap/MultiValueMapcontainsKey,keySet,entrySet,addIfAbsent, passingHttpHeadersasMapcontainsHeader,headerNames(),headerSet(),add()+containsHeader;AuthScheme.apply(... HttpHeaders ...)/api/x.json/.xml)setUseSuffixPatternMatch/RegisteredSuffix/TrailingSlash,favorPathExtension,PathExtensionContentNegotiationStrategy,PatternsRequestCondition(..,bool,bool,..)MediaTypeSuffixFilter(strips a registered suffix + stashes theMediaType) + newSuffixMediaTypeContentNegotiationStrategy;CustomRequestMappingHandlerMappingsimplifiedAntPathRequestMatcher(security)PathPatterncan't do mid‑pattern/**/org.hisp.dhis.webapi.security.AntPathRequestMatcherbacked by Spring'sAntPathMatcher(preserves exact rules incl./api/**/loginConfig)HttpSecurityDSL.and()chainingCustomizerlambda rewrite (DhisWebApiWebSecurityConfig)OAuth2AuthorizationCodeGrantRequestEntityConverter/RestOperations‑basedDefaultAuthorizationCodeTokenResponseClientRestClientAuthorizationCodeTokenResponseClient+addParametersConverter(private_key_jwt preserved)AccessDecisionVoter/AccessDecisionManagermodelorg.springframework.security.access.vote.*AllRequiredRoleVoterdeleted (no usages)DaoAuthenticationProvidersetUserDetailsServicesuper(UserDetailsService)ctor (TwoFactorAuthenticationProvider)o.s.s.oauth2.server.authorization.config.annotation.web.*o.s.s.config.annotation.web.*;authorizationServer()factory →newctorObjectPostProcessoro.s.s.config.annotation.ObjectPostProcessoro.s.s.config.ObjectPostProcessorAbstractAuthenticationToken(null)Builderctor)nulltoCollection<? extends GrantedAuthority>UriComponentsBuilder.fromHttpUrlfromUriStringAsyncListenableTaskExecutor/ListenableFuture(Callback)AsyncTaskExecutor.submitCompletable/BiConsumerRedisIndexedSessionRepository(RedisOperations<Object,Object>)RedisOperations<String,Object>(RedisTemplate<String,Object>)3.⚠️ Real runtime regressions found (not compile errors) — these are the important ones
Found by running the suites; each passes on master and failed only under Spring 7.
3a. 🔴
@PostConstruct/@PreDestroysilently stop firing —javax.annotationis no longer honoured.Spring 6.2 honoured both
javax.annotation.*andjakarta.annotation.*JSR‑250 annotations;Spring 7 honours only
jakarta.annotation.*. dhis2 explicitly declaredjavax.annotation:javax.annotation-api(1.3.2) in 16 module poms and used the import in 40 files,so all those
@PostConstruct/@PreDestroycallbacks silently stopped running under Spring 7 — e.g.HibernateEncryptionConfig(encryption password never initialised) andTestBase.initServices()(the static
categoryService/configurationService/periodServiceleft null → NPEs across the wholeintegration test suite). Fix: migrate the 40 imports to
jakarta.annotation.*and swap thedependency to
jakarta.annotation:jakarta.annotation-api3.0.0. This is the single highest‑impactruntime fix — it cleared the bulk of the initial controller‑suite failures.
3b. 🔴 Request routing: AntPathMatcher → PathPattern. dhis2 controllers rely on AntPathMatcher
semantics (mid‑pattern
**, e.g. versioned/api/40/...). Spring 6 madePathPatternParserthe defaultmatcher; under it many mappings no longer match → systemic 404s. Fix (interim):
setPatternParser(null)on the handler mapping (keep AntPathMatcher). Proper fix deferred: migratecontroller
@RequestMappingpatterns to PathPattern (AntPathMatcher is deprecated for removal).3c. 🟠 Trailing‑slash matching (
setUseTrailingSlashMatchremoved) → normalised inMediaTypeSuffixFilter; the filter is now also wired into the MockMvc test setup (tests build theirown filter chain).
3d. 🟠
SpringExtensionis now test‑method‑scoped (was test‑class‑scoped) → restored viaspring.test.extension.context.scope=test_class.3e. 🔴
@EnableGlobalMethodSecurityremoved‑infrastructure.WebMvcConfigused the deprecated@EnableGlobalMethodSecurity; Security 7 removed its supporting class(
MethodSecurityMetadataSourceAdvisor) →NoClassDefFoundErrorat context startup → the app wouldnot boot (
api-test) and method‑security controller tests errored. Fix:@EnableMethodSecurity(the existing
MethodSecurityExpressionHandlerbean is retained).3f. 🔴 Hibernate
SpringSessionContextrelocated.HibernateConfigsethibernate.current_session_context_class = org.springframework.orm.hibernate5.SpringSessionContext—Spring 7 moved it to
org.springframework.orm.jpa.hibernate.SpringSessionContext. It's a runtimeclass‑name string (compiles fine) →
ClassNotFoundExceptionat runtime. Fix: update the string.3g. (originally found)
HibernateEncryptionConfigalso fixed via constructor init (a robustimprovement; the underlying cause was 3a).
4. Needs CRITICAL / SECURITY review (highest priority)
These are correct‑by‑construction migrations but touch auth/routing and were not runtime‑validated
against real infrastructure here:
DhisWebApiWebSecurityConfig— the entireSecurityFilterChainrewritten to the lambda DSL.Security review: confirm every
permitAll/authenticatedrule, session/CSRF/headers behaviour issemantically identical.
AntPathRequestMatcher(new custom class) — decides which endpoints are public vs authenticated(~35 rules, incl. mid‑pattern
/**/). Security review + behavioural tests: matching must bebyte‑for‑byte equivalent to the removed Spring matcher.
MediaTypeSuffixFilter+SuffixMediaTypeContentNegotiationStrategy— reinstates.json/.xmlrouting & content negotiation. Review: preserves the public API URL contract; verify against the
api-test suite. TODO: trailing‑slash matching for
/api/**(wassetUseTrailingSlashMatch(true))is not yet reinstated; and the filter must be wired into MockMvc test setups.
DhisAuthorizationCodeTokenResponseClient— OIDC authorization‑code token exchange incl.private_key_jwt. Needs a real OIDC provider test + security review of the newRestClientAuthorizationCodeTokenResponseClientpath.AuthorizationServerConfig— dhis2‑as‑IdP on the new (merged) Authorization Server APIs.SpringBindingTest— 2 assertions relaxed to a quote‑free prefix because the standalone MockMvcnow JSON‑escapes the cause message (production behaviour unchanged). Confirm acceptable.
5. Checks / CI status (being driven to green on this PR)
First CI run (before the §3e/§3f fixes) already showed the core is sound:
in dhis‑api; ignore javaagent‑only
java-allocation-instrumenter; drop unusedspring-coreindata‑exchange).
starts.
CustomRequestMappingHandlerMapping, literal‑first + suffix/trailing‑slashfallback) cleared the earlier systematic
OpenApiControllerTest×9 + the ~192ClientProtocolExceptionmass‑failure in api‑test (those were the AntPathMatcher/PathPattern +suffix‑routing regression, not an auth/response‑format problem as first suspected).
Current state — driven down to a small, well‑understood residue (reproduced locally on the Jib
image + the full
dhis-test-e2eDocker stack):OAuth2Test×11 = Spring Authorization Server 7 PKCE‑by‑default. SAS 7 flippedClientSettings.builder()'s defaultrequireProofKeyfalse→true (verified at the bytecodelevel). DHIS2's authorization‑code flow sends no PKCE
code_challenge, so/oauth2/authorizereturned
error=invalid_request (code_challenge, RFC 7636)and bounced the Selenium browser to theredirect_uri →
net::ERR_CONNECTION_REFUSED. Fix:Dhis2OAuth2ClientServiceImpl .applyCreateDefaultsnow setsrequireProofKey(false)to preserve prior behaviour. Verified on thelive image (new clients store
require‑proof‑key=false;/oauth2/authorize302s to/login/).DataSetControllerTest.json/.json.zipContent‑Disposition ×2. The endpoint isbyte‑identical to
master, so this is a path‑extension content‑negotiation regression. The newCustomRequestMappingHandlerMappingmatches literal‑first (needed soOpenApiController'sliteral
/openapi/openapi.jsonkeeps working); when a generic CRUD/{uid}/{property}handlermatches the dotted path as‑is,
normalize()never runs, the suffix isn't resolved, andMetadataExportParamsMessageConverterisn't selected (→ plainapplication/json+inline;filename=f.txt). The old always‑stripMediaTypeSuffixFilter(servlet filter, pre‑mapping)avoided this. Confirmed via curl: with
Accept: application/jsonthe converter works — only suffixrouting is broken. Needs a design fix reconciling literal‑mapped vs suffix‑negotiated paths.
DcrControllerTest×2 → 400invalid_scope. Captured body:{"error":"invalid_scope","error_description":"Invalid Client Registration: scope"}. SAS 7 changeddynamic‑client‑registration scope validation; the requested scopes (
openid profile username) arerejected. The
AuthorizationServerConfigdiff is purely mechanical, so this is a genuine SAS 7behaviour change. Security‑sensitive — needs a DCR scope‑policy decision.
compile/runtime error).
container are environmental, not regressions (they pass in CI).
6. 📋 Proposed phased rollout (land in small, non‑breaking chunks)
The all‑in‑one branch is too big to merge safely. Most of the diff is forward‑compatible and can
land on
master(still Spring 6) before the major bump, shrinking the risky step to just theversion‑locked core.
Phase 0 — land on master now (Spring 6.2 / Security 6.5; each independently shippable & non‑breaking).
Spring 6.2 honours both
javax/jakartaannotations and supports the lambda DSL, so most of the diff isforward‑compatible and shrinks the risky bump dramatically:
javax.annotation→jakarta.annotation@PostConstruct/@PreDestroy(40 files + the dependency).Forward‑compatible (6.2 honours jakarta) and the single biggest de‑risking step (§3a). Do this first.
.and()is deprecated there).UriComponentsBuilder.fromHttpUrl→fromUriString.DefaultAsyncTaskExecutor→submitCompletable;MessageSendingCallback→BiConsumer.AllRequiredRoleVoter;AbstractAuthenticationToken(null)cast.AntPathRequestMatcherand switch call sites to it (works on Spring 6 too).HibernateEncryptionConfigconstructor init (defensive; pairs with the §3a fix).MediaTypeSuffixFiltercontent‑negotiation + trailing‑slashrefactor and
setPatternParser(null)— these reproduce the current AntPathMatcher behaviour and can beintroduced on Spring 6 first so the behavioural change is isolated from the framework bump.
Phase 1 — the coupled major bump (one PR, must move atomically):
Spring Framework 7 + Security 7 + Data Redis 4 + Session 4 + Authorization Server 2 + JUnit 6, plus
the Spring‑7‑only API changes that can't exist on 6 (
HttpHeaders.headerSet/headerNames,addIfAbsentremoval,
ListenableFutureremoval, AS package moves,DaoAuthenticationProviderctor,PatternsRequestConditionboolean removal, OAuth2RestClienttoken client, removed MVC suffix flags).After Phase 0 this PR is dramatically smaller and mostly mechanical.
Phase 2 — JUnit 6 timing: if Spring‑6
spring-testtolerates JUnit 6 it can precede Phase 1;otherwise it ships inside Phase 1 (validated here as coupled).
Phase 3 — lettuce 7: only after Spring Data Redis 4.0 → 4.1+ (the first SDR line that ships
lettuce 7). Tracked separately.
Phase 4 — hardening / follow‑ups (the behavioural tail surfaced by CI):
extension literally (
OpenApiController, dataset.json.zip) keep working —lookupHandlerMethodtries the literal path first, then the stripped path + content‑type attribute. (Fixes the 9
OpenApiControllerTest+DataSetControllerTestintegration‑h2 failures.)ClientProtocolExceptionin tracker‑test setup against theDocker e2e stack (likely auth/response‑format).
DcrControllerTest) behaviour.CI snapshot (commit
5999d165)✅ unit‑test · ✅ integration‑test (Postgres, all shards) · ✅ dependency‑analysis · ✅ sonarqube (job)
⚠️ SonarCloud gate. The deterministic + core‑JVM checks and the whole Postgres integration suite are
· 🔧 check‑formatting (fixed) · 🟠 integration‑h2 (12, see above) · 🟠 api‑test (e2e setup) ·
green and the app boots on Spring 7; the remainder is the documented behavioural/review tail.
🤖 Generated with Claude Code