Skip to content

8.x Add Class-typed getAttribute reads for session, request and servletContext#15732

Open
codeconsole wants to merge 3 commits into
apache:8.0.xfrom
codeconsole:feat/typed-attribute-reads
Open

8.x Add Class-typed getAttribute reads for session, request and servletContext#15732
codeconsole wants to merge 3 commits into
apache:8.0.xfrom
codeconsole:feat/typed-attribute-reads

Conversation

@codeconsole

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #15715. The named converters (string, int, boolean, …) cover scalar attribute values, but attributes frequently hold richer objects — a security principal, a domain instance, a CSRF token — where today the only statically-compilable read is a cast at every call site:

CsrfToken token = (CsrfToken) session.getAttribute('_csrf')
UserDetails user = request.getAttribute(USER_ATTRIBUTE) as UserDetails

This adds a Class-typed overload of getAttribute (plus a default-value variant) to session, request and servletContext:

CsrfToken token   = session.getAttribute('_csrf', CsrfToken)
UserDetails user  = request.getAttribute('apiUser', UserDetails)
Theme theme       = session.getAttribute('theme', Theme, Theme.DEFAULT)

The generic signature (static <T> T getAttribute(holder, String name, Class<T> type)) lets the static compiler infer the return type from the class literal, so the read compiles under @CompileStatic / @GrailsCompileStatic with no cast.

Design

  • Typed read, not coercion. The attribute is returned only when it is an instance of the requested type; absent and wrong-typed attributes read as null (so the default-value overload also applies on a type mismatch). No TypeConverters delegation — the named converters remain the coercing API, the Class overload is the type-safe retrieval API. Two simple contracts instead of one mixed one.
  • Overloads getAttribute rather than introducing a new verb. Mirrors grails.config.Config.getProperty(String key, Class<T> targetType, T defaultValue) (and Spring's Environment.getProperty(key, type, default)), and shows up in IDE completion right next to the raw getAttribute(String).
  • Scope: the three attribute holders only. flash is skipped — it is a Map, and Groovy's DGM Map.get(key, default) (which inserts the default) makes a two-arg sibling on flash a semantic trap. params is skipped — its values are Strings, where a non-coercing typed read is not useful.

Tests & docs

  • HttpSessionExtensionSpec, HttpServletRequestExtensionSpec, ServletContextExtensionSpec each gain two features: instance-match / supertype-match / absent / wrong-type-reads-as-null semantics, default-value behavior, and a @CompileStatic static-resolution guard (the guard fails test compilation without the new extension methods).
  • New "Typed Reads of Attributes" subsection in the controllers guide, following the "Type Conversion of Attributes" section from Add typed attribute converters for session, request, flash and servletContext #15715.

:grails-web-core:test passes in full.

@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 48.4425%. Comparing base (1c2d8a6) to head (b1fc911).
⚠️ Report is 24 commits behind head on 8.0.x.

Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##                8.0.x     #15732        +/-   ##
==================================================
+ Coverage     48.3255%   48.4425%   +0.1169%     
- Complexity      15216      15284        +68     
==================================================
  Files            1875       1875                
  Lines           85818      85873        +55     
  Branches        14969      14978         +9     
==================================================
+ Hits            41472      41599       +127     
+ Misses          37975      37863       -112     
- Partials         6371       6411        +40     
Files with missing lines Coverage Δ
...ils/web/servlet/HttpServletRequestExtension.groovy 21.5054% <100.0000%> (+6.3891%) ⬆️
...org/grails/web/servlet/HttpSessionExtension.groovy 55.5556% <100.0000%> (+10.7280%) ⬆️
.../grails/web/servlet/ServletContextExtension.groovy 33.3333% <100.0000%> (+17.9487%) ⬆️

... and 12 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codeconsole codeconsole changed the title Add Class-typed getAttribute reads for session, request and servletContext 8.x Add Class-typed getAttribute reads for session, request and servletContext Jun 12, 2026
@codeconsole codeconsole force-pushed the feat/typed-attribute-reads branch from a5f6083 to b1fc911 Compare June 12, 2026 16:21
@testlens-app

testlens-app Bot commented Jun 12, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: b1fc911
▶️ Tests: 37606 executed
⚪️ Checks: 43/43 completed


Learn more about TestLens at testlens.app.

}
Object value = request.getAttribute(name)
Class<T> resolvedType = (Class<T>) ClassUtils.resolvePrimitiveIfNecessary(type)
resolvedType.isInstance(value) ? resolvedType.cast(value) : null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be value?.class?.isAssignableFrom ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type.isInstance(value) is true when value is-a type, including when type is a supertype/interface of the value's concrete class. That's the primary use case for these reads:

// stored value is a concrete DefaultCsrfToken; caller requests the interface
CsrfToken token = request.getAttribute('_csrf', CsrfToken)

value.class.isAssignableFrom(type) inverts the relationship — it asks "is type the same as or a subclass of the value's class", i.e. DefaultCsrfToken.isAssignableFrom(CsrfToken), which is false — so it would return null for every request-by-interface. The supertype-match case is covered by the spec (getAttribute('principal', CharSequence) for a StringBuilder value).

The isAssignableFrom equivalent of the current check would be type.isAssignableFrom(value.class) (with a null guard); type.isInstance(value) just expresses that directly and is null-safe.

@bito-code-review

Copy link
Copy Markdown

Using value?.class?.isAssignableFrom is not necessary here because the code already resolves the type and uses isInstance to check compatibility. The current implementation resolvedType.isInstance(value) is the standard, idiomatic way to check if an object is an instance of a specific class in Java/Groovy, and it correctly handles null values (returning false).

grails-web-core/src/main/groovy/org/grails/web/servlet/HttpServletRequestExtension.groovy

Object value = request.getAttribute(name)
        Class<T> resolvedType = (Class<T>) ClassUtils.resolvePrimitiveIfNecessary(type)
        resolvedType.isInstance(value) ? resolvedType.cast(value) : null

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants