Resolve resource links to the controller exposing the domain class - #16272
Resolve resource links to the controller exposing the domain class#16272codeconsole wants to merge 5 commits into
Conversation
A resource link derived its controller from the domain class name via
PersistentEntity.getDecapitalizedName(), so a controller not named after
its domain class was never found. Adopting plural controller names, the
convention Rails uses and the one that makes wildcard REST mappings
practical, meant <g:link resource="${person}"> targeted a non-existent
"person" controller.
DefaultLinkGenerator now keeps a lazily built domain-class-to-controller
index alongside the existing namespace index, guarded by the same
artefact array identity check so it costs nothing on the common path and
rebuilds on a development-mode reload. A controller's domain class is
resolved by walking its superclass hierarchy for a generic type argument
the mapping context recognises as a persistent entity, so it works for
any generic REST base class at any depth without this class depending on
that hierarchy.
Resolution only applies when exactly one controller declares the domain
class; an ambiguous or absent declaration falls back to the previous
behaviour, keeping the change additive.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## 8.0.x #16272 +/- ##
==================================================
+ Coverage 54.7365% 54.7733% +0.0368%
- Complexity 20470 20523 +53
==================================================
Files 2103 2103
Lines 101077 101135 +58
Branches 17928 17944 +16
==================================================
+ Hits 55326 55395 +69
+ Misses 37876 37863 -13
- Partials 7875 7877 +2
🚀 New features to boost your workflow:
|
The previous commit consulted the domain-class index before the naming convention, so an application with both a BookController and a BookRestfulController extending RestfulController<Book> had every resource link retargeted at the latter. That is not additive, as its commit message claimed, and it is not confined to g:link: the HAL, Atom and vnd.error renderers all build hrefs through link(resource: instance), so REST response bodies moved too. BookFunctionalSpec in the hibernate7 test app caught it on every JDK and indy combination. The naming convention now wins whenever a controller with that name is registered, reusing the namespace index as the lookup, so resolution only applies where the previous behaviour pointed at a controller that does not exist. Two further corrections to the walk: Interfaces are now traversed as well as superclasses. Groovy traits compile to an interface, so a domain class declared by a trait was invisible and the claim that any generic base class works at any depth was not accurate. A supertype declaring more than one persistent entity is skipped rather than guessed at, and the walk continues upwards. A base parameterised on a parent and a child resource previously indexed the controller under whichever came first in declaration order. Three of the specs proved nothing and have been replaced. The reset test swapped in a new GrailsApplication, whose artefact array is a different object, so the identity guard rebuilt the index whether or not the reset ran. One assertion compared against a bare expression in a given: block, where Spock applies no implicit condition. A third asserted only that a link did not contain '/people/', which held for the toString() of the fixture. The replacements fail without this change, and cover the precedence rule, interface resolution and the ambiguous base class. Adds a spec exercising the real RestfulController hierarchy in grails-test-suite-uber, including the bounded type parameter shape RestfulServiceController uses, which the stand-in fixtures did not reach.
An association rendered while still a lazy proxy took a different path from one already loaded: AbstractLinkingRenderer passes the instance when initialised, but the domain class name when it is a proxy, to avoid loading it. Only the instance path resolved the controller, so the same association rendered /authors/2 or /author/2 depending on the session's fetch state. A domain class passed to the resource attribute now resolves through the same rule as an instance, and the renderer passes the associated entity's class rather than its decapitalised name. Both paths agree, and the class form keeps the naming convention precedence, so it is additive on the same terms. A string passed to the resource attribute is still treated as a literal controller name, since it cannot be distinguished from one.
✅ All tests passed ✅🏷️ Commit: 4555348 Learn more about TestLens at testlens.app/docs. |
| private volatile Map<String, Set<String>> controllerNamespacesByName | ||
| private volatile GrailsClass[] cachedControllers | ||
|
|
||
| private volatile Map<String, Set<String>> controllerNamesByDomainClass |
There was a problem hiding this comment.
We should discuss this further on the weekly or mailing list. This assumes all applications use the same design here and it's formalizing something that may not be true for all applications. This is also a major shift, not necessarily a bad one, but it will require another milestone. Thus this should go into 8.1 or later.
There was a problem hiding this comment.
Agreed — worth the weekly or the mailing list before this goes anywhere. No objection to holding it for a later milestone.
|
The concern regarding the potential impact of this change on applications that do not follow the assumed naming conventions is noted. Since this introduces a shift in how resource links are resolved, it is reasonable to consider whether this should be targeted for a future milestone like 8.1 to allow for further discussion and validation across different application architectures. |
A
resourcelink derives its controller from the domain class name, so a controller not named after its domain class is never found:DefaultLinkGeneratornow keeps a domain-class-to-controller index alongside the existing namespace index, built lazily and guarded by the same artefact-array identity check, so it costs nothing on the common path and rebuilds when the set of controllers changes.A controller named after the domain class always wins. The index is consulted only when no such controller is registered, so an application following the naming convention is unaffected — including one that has both a plain
BookControllerand aBookRestfulController extends RestfulController<Book>.A controller's domain class is found by walking its supertypes for a generic type argument the mapping context recognises as a persistent entity. Superclasses and interfaces are both walked, so a base class or a Groovy trait at any depth works, without this class depending on the REST controller hierarchy:
A supertype declaring more than one persistent entity is ambiguous and is skipped rather than guessed at, with the walk continuing upwards. If no controller declares the domain class, or more than one does, the link falls back to the domain class name.
Applies only when
resourceis given a domain instance and GORM is configured; passing a controller name directly (resource: "book") is unchanged.An association still held as a lazy proxy is rendered from the domain class rather than the instance, to avoid loading it. A domain class passed to the
resourceattribute resolves through the same rule, so a proxied and a loaded association produce the same href.