DO NOT MERGE: #34154: refactor(java25): scoped values for reindex + config guard, flexible constructor for ISODateParam - #37106
Conversation
…lexible ctor for ISODateParam Applies three Java 25 features where the codebase already had the problem they solve. Sourced from the Java 21->25 tech talk; each change is small, local and covered by new unit tests. ISODateParam — flexible constructor bodies (JEP 513) DateUtil.parseISO signals unusable input by returning null, and UtilMethods.isSet rejects null, blank text *and the literal four characters "null"* — which is what a client sends when it interpolates an unset variable into the URL. The constructor called .getTime() on that result inside the argument list of super(...), so those inputs produced a NullPointerException naming no parameter. It could not be fixed before: nothing was allowed to precede an explicit constructor invocation. A prologue now validates first and throws a ParseException naming the offending value. The class is a JAX-RS @PathParam binder (BundleResource.deleteBundlesOlderThan), so the input is untrusted. ThreadContextUtil — scoped value for the reindex flag (JEP 506) wrapVoidNoReindex/wrapReturnNoReindex were a hand-written dynamically scoped binding: read the old value, rebind, run the delegate, restore in a finally — duplicated across both methods. That is exactly ScopedValue.where(...).call(...), so the two copies collapse into one and the restore can no longer be skipped. includeDependencies is deliberately NOT migrated. It travels callee to caller — ESContentletAPIImpl records it and WorkflowAPIImpl.fireWorkflowPostCheckin consumes it afterwards — and a scoped binding is immutable by design, so it stays in the mutable ThreadContext. Both classes now document why the two pieces of state use different mechanisms. Verified that no wrap* call site crosses an executor before the flag is read; ScopedValue does not propagate through DotSubmitter. Config — re-entrancy guard via ScopedValue.isBound() Reading a property can consult the system table, which reads properties to reach the database, so the lookup must detect re-entry. The guard was a mutable "tag" string on the per-thread ThreadContext whose finally block assigned null rather than restoring the previous value. Not a live defect — the nested call returns before the try, so two levels never coexist — but a single shared slot used as a one-deep stack with an exit path that assumes it was empty on entry. isBound() makes that class of mistake unwritable, and drops a per-thread allocation: getOrCreateContext() installed a ThreadContext in a thread local for every thread that ever read a property, cleared only by a reflective shutdown task. This was the sole user of ThreadContext.tag, and the reindex flag was the sole user of ThreadContext.reindex, so both fields are removed. ThreadContext goes from three fields to one. Tests: 22 new unit tests (9 ISODateParam, 9 ThreadContextUtil, 4 Config guard). The four ISODateParam null-input tests were confirmed failing with NullPointerException before the fix. The ThreadContextUtil tests pass before and after by design — they are the regression net proving the rewrite preserves behaviour, including that a throwing delegate still restores the flag and that a nested wrap restores the enclosing value rather than the default. Full dotcms-core unit suite green: 3463 tests, 0 failures, 0 errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @fabrizzio-dotCMS's task in 2m 1s —— View job Code Review
Reviewed the diff end-to-end. This is a careful, well-documented refactor and I found no blocking issues. Notes below. Verification performed
Notes (non-blocking)
No issues found that block merge. |
Design note: why
|
What
Applies three Java 25 features to places where the codebase already had the problem they solve. Material came out of the Java 21→25 tech talk (#34154): rather than illustrate the features with invented examples, these are the real call sites the features were designed for.
Each change is small, local, and covered by new unit tests. No behaviour change is intended except the
ISODateParamfix.1.
ISODateParam— flexible constructor bodies (JEP 513)DateUtil.parseISOsignals unusable input by returningnull, andUtilMethods.isSetrejectsnull, blank text and the literal four characters"null"— which is what a client sends when it interpolates an unset variable into a URL (/olderthan/null).The constructor was:
.getTime()is called on thatnullinside the argument list ofsuper(...), so those inputs produced aNullPointerExceptionthat named no parameter. The author could not have fixed it: before Java 25 nothing was allowed to precede an explicit constructor invocation, so there was no place in the constructor to put the check.A prologue now validates first and throws a
ParseExceptionnaming the offending value. This class is a JAX-RS@PathParambinder used byBundleResource.deleteBundlesOlderThan, so the string is untrusted request input.The prologue is visible in the bytecode — the null check completes before
invokespecial Date.<init>:2.
ThreadContextUtil— scoped value for the reindex flag (JEP 506)wrapVoidNoReindex/wrapReturnNoReindexwere a hand-written dynamically scoped binding — read the old value, rebind, run the delegate, restore in afinally— duplicated across both methods. That is precisely whatScopedValue.where(...).call(...)does, so the two copies collapse into one and the restore can no longer be skipped.includeDependenciesis deliberately not migrated. It travels callee to caller:ESContentletAPIImplrecords it deep in the stack andWorkflowAPIImpl.fireWorkflowPostCheckinconsumes it afterwards. A scoped binding is immutable by design, so it cannot express an out-channel — it stays in the mutableThreadContext. Both classes now document why the two pieces of state use different mechanisms.Verified that no
wrap*call site crosses an executor before the flag is read;ScopedValuedoes not propagate throughDotSubmitter.3.
Config— re-entrancy guard viaScopedValue.isBound()Reading a property can consult the system table, and consulting the system table reads properties in order to reach the database — so the lookup must detect re-entry. The guard was a mutable
tagstring on the per-threadThreadContextwhosefinallyassignednullrather than restoring the previous value.To be clear, this is not a live defect — the nested call returns before the
try, so two levels never coexist, and a test in this PR confirms the existing guard blocks recursion correctly. What the change buys:isBound()makes that class of mistake unwritable: a single shared slot used as a one-deep stack, with an exit path that assumes it was empty on entry.getOrCreateContext()installs aThreadContextin a thread local for every thread that ever reads a config property, cleared only byThreadLocalCleanupShutdownTask— which reaches into private static fields by reflection to do it.This was the sole user of
ThreadContext.tag, and the reindex flag was the sole user ofThreadContext.reindex, so both fields are removed.ThreadContextgoes from three fields to one.Testing
22 new unit tests: 9 for
ISODateParam, 9 forThreadContextUtil, 4 for theConfigguard.ISODateParamnull-input tests were confirmed failing withNullPointerExceptionbefore the fix (Red), then green.ThreadContextUtiltests pass before and after by design — they are the regression net proving the rewrite preserves behaviour, including that a throwing delegate still restores the flag and that a nested wrap restores the enclosing value rather than the default.Configguard tests pin that a nested lookup never reaches the source, that the guard is released after the outer lookup returns, and that it is released when the source throws.Full
dotcms-coreunit suite green: 3463 tests, 0 failures, 0 errors, 17 skipped.Integration tests were not run. These are pure unit tests and register in no suite, but
ESContentletAPIImplandWorkflowAPIImplexercise the reindex flag in ITs — worth a workflow/contentlet battery before merge.Notes for the reviewer
dotcms.core.compiler.release). NeitherScopedValuenor flexible constructor bodies is a preview feature in 25, so no--enable-previewis needed for these.includeDependenciesis never cleared between workflow fires, so on a pooled thread it staystrueafter the first deferred reindex. That over-indexes rather than under-indexes, and changing it could reduce indexing where someone relies on it — flagging rather than folding it in.🤖 Generated with Claude Code
This PR fixes: #34154