Skip to content

Make SiteMesh 3 the default GSP layout#15713

Open
codeconsole wants to merge 10 commits into
apache:8.0.xfrom
codeconsole:feat/sitemesh3-default-layout
Open

Make SiteMesh 3 the default GSP layout#15713
codeconsole wants to merge 10 commits into
apache:8.0.xfrom
codeconsole:feat/sitemesh3-default-layout

Conversation

@codeconsole
Copy link
Copy Markdown
Contributor

Makes SiteMesh 3 the default GSP layout in generated applications, mutually exclusive with the legacy SiteMesh 2 grails-layout feature.

Introduces a GspLayout one-of feature group (enforced by OneOfFeatureValidator):

  • GspLayout — abstract one-of parent (Category.VIEW, WEB/WEB_PLUGIN)
  • Sitemesh3 — default member; auto-applied unless another GspLayout is selected; adds grails-sitemesh3
  • GrailsLayout — opt-in member; adds grails-layout (SiteMesh 2)

GrailsGsp is intentionally not modified: its existing if (!isFeaturePresent(Sitemesh3)) guard already skips grails-layout when SiteMesh 3 applies, so SiteMesh 2 remains available via GrailsLayout. Selecting both sitemesh3 and grails-layout now fails fast.

Depends on #15710

Split out from #15710 per review feedback ("making this the default & updating to sitemesh 3 should be separate PRs"). This branch is stacked on #15710, so until #15710 merges this PR's diff also shows the enable-SiteMesh-3 commits; it will reduce to just the make-default change once #15710 lands. Please merge #15710 first.

…s-layout

Introduce a GspLayout one-of feature group so SiteMesh 3 (grails-sitemesh3)
and the legacy SiteMesh 2 grails-layout are both selectable but never
applied together (enforced by OneOfFeatureValidator):

- GspLayout: abstract OneOfFeature parent (Category.VIEW), WEB/WEB_PLUGIN
- Sitemesh3: default member; auto-applied unless another GspLayout is
  selected; adds grails-sitemesh3
- GrailsLayout: opt-in member; adds grails-layout (SiteMesh 2)

The GspLayout features now own the layout dependency, so GrailsGsp's
'if (!isFeaturePresent(Sitemesh3)) add grails-layout' block is removed.
Selecting both sitemesh3 and grails-layout now fails fast.
@jamesfredley jamesfredley self-requested a review June 3, 2026 15:06
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 3, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 0.0000%. Comparing base (4836003) to head (8a1fbcb).
⚠️ Report is 93 commits behind head on 8.0.x.

Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                @@
##                8.0.x   #15713         +/-   ##
=================================================
- Coverage     49.0042%        0   -49.0042%     
=================================================
  Files            2014        0       -2014     
  Lines           94747        0      -94747     
  Branches        16547        0      -16547     
=================================================
- Hits            46430        0      -46430     
+ Misses          41019        0      -41019     
+ Partials         7298        0       -7298     

see 2014 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.

@jamesfredley
Copy link
Copy Markdown
Contributor

Gaps to review and potentially address before SM3 becomes default

Gap / Open item SM2 / Current state SM3 / Needed
g:applyLayout attributes (template,url,action,controller,params,model,contentType,encoding,parse) Full support - RenderGrailsLayoutTagLib.groovy:100-173 name + body only; other attrs silently ignored - RenderSitemeshTagLib.groovy:77-106
Default-layout config + implicit application fallback Reads grails.views.layout.default (LayoutGrailsPlugin:37); falls back to layout application (GroovyPageLayoutFinder:180) Reads only grails.sitemesh.default.layout (Sitemesh3GrailsPlugin:83); no application fallback
Partial/template render suppression (GrailsRenderViewMutator + GrailsLayoutSelector) Registers both (LayoutGrailsPlugin:74-75); consumed by core ResponseRenderer:340-341,563 Registers neither → beans null → behavior lost
grails.views.layout.enable.nongsp (non-GSP/JSP layouts) LayoutGrailsPlugin:38 + GroovyPageLayoutFinder enableNonGspViews/viewMustExist Sitemesh3LayoutFinder:192-194 GSP locator only; flag unsupported
Captured-page isolation across nested/sibling applyLayout N/A (different mechanism) GrailsSiteMeshViewContext.dispatch:81 sets fresh page, no restore; taglib saves only LAYOUT_ATTRIBUTE (RenderSitemeshTagLib:78,100-104)
No-body captured page + layoutBody Handled CaptureAwareContentProcessor:82 returns used page without data; layoutBody reads only body (RenderSitemeshTagLib:218-225)
<title> strip robustness Regex-based, well-formed extractHead() matches "<title" as prefix - mis-slices <titlebar>/<title-x> (Sitemesh3CapturedPage.java:232)
Aggregate web starter still defaults to SM2 starter-web/build.gradle:53:grails-layout unless SITEMESH3_TESTING_ENABLED=true Default branch must be :grails-sitemesh3
~35 test examples default to SM2 else → grails-layout in every example (e.g. app1/build.gradle:42-47) Default = SM3; SM2 via forced lane
Main unit suite defaults to SM2 grails-test-suite-uber/build.gradle:53-58:grails-layout Default = SM3
SM2-only examples with no SM3 coverage Hardcoded grails-layout: test-phases:42, scaffolding:49, scaffolding-fields:38, jetty:41, database-cleanup:37, gsp-layout:50 Keep as SM2 anchors; add SM3 variants where parity matters
Default path ≠ tested path SITEMESH3_TESTING_ENABLED gates SM3; default build never runs SM3 Retire/invert flag so default == tested
No CI lane exercising SM3 as default Only opt-in env runs SM3 Add SM3-default lane + SM2 lane
Fragile upstream auto-config suppression Disabled NoopSitemeshFilter named sitemesh suppresses upstream SiteMeshAutoConfiguration (Sitemesh3GrailsPlugin:106-124) Robust suppression + guard test
Servlet 6.1 / Tomcat 11 forward→include dispatch SM3 bypasses RequestDispatcher.forward() for absolute layout paths (GrailsSiteMeshViewContext.java:88-93) Functional coverage on Tomcat 11
grails-gsp-spring-boot unpublished In settings.gradle:174, depends on SM3 (spring-boot/build.gradle:37), but absent from publish-root-config.gradle (only grails-sitemesh3 at :75) Publish or don't rely on it

Not gaps (verified parity)

Resolution order (6 steps), NONE_LAYOUT, layoutTitle/layoutHead, pageProperty/ifPageProperty, meta extraction, body.* attrs, grailsLayout:parameter, comma/chained decoration (happy path), unbounded layout cache (SM2 identical).

Minor / cosmetic

Cache-interval key differs: SM3 grails.sitemesh.layout.cache.interval vs SM2 grails.gsp.reload.interval.

@codeconsole codeconsole requested review from matrei and sbglasius June 3, 2026 21:44
Copy link
Copy Markdown
Contributor

@matrei matrei left a comment

Choose a reason for hiding this comment

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

There seems to be a glitch in the UI when a default feature is replaced:

Image

Copy link
Copy Markdown
Contributor

@jdaugherty jdaugherty left a comment

Choose a reason for hiding this comment

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

I forgot about the test apps pointing at grails-layout by default. We'll need to update that as part of this switch and ensure no regressions.

@jamesfredley
Copy link
Copy Markdown
Contributor

I wish github didn't scroll on tables like that in comments, but there are a number of key features that will need to be added to the sitemesh 3 plugin to match sitemesh 2's historical feature set. When the tests are all run against sitemesh 3 it will surface most of these, but the comment above should be close to comprehensive.

…itch

Address review feedback on the SiteMesh 3 default change:

- Align the SiteMesh 3 feature title with SiteMesh 2 ("GSP SiteMesh 3
  Layouts") and clean up GspLayoutSpec (single quotes, drop the
  short-lived SNAPSHOT-repo assertions).
- Fix the UI glitch where replacing the default layout left both
  sitemesh3 and grails-layout selected. The two decorators are now
  driven by a single GspLayoutImpl option (SITEMESH3 default,
  GRAILS_LAYOUT) instead of a visible feature card, mirroring the
  servlet and reloading one-of groups. Both members are invisible
  DefaultFeatures that apply based on the selected option, so the
  default-features endpoint always resolves exactly one member.

Threads the option through Options, FeatureFilter, ApplicationController
and ContextFactory, exposes it via select-options (GspLayoutImplDTO /
GspLayoutImplSelectOptions) for the UI dropdown, and adds a --gsp-layout
CLI option. Updates BuildBuilder and GspLayoutSpec and adds
SelectOptionsControllerSpec.
@codeconsole codeconsole marked this pull request as draft June 5, 2026 20:06
The test apps already gate the layout dependency on the
SITEMESH3_TESTING_ENABLED environment variable, but no CI lane set it,
so the suite only ever exercised the legacy grails-layout. Set the flag
at the workflow level in the CI and Groovy joint builds so the example
apps and grails-test-suite-uber resolve grails-sitemesh3 by default.

No build-script changes needed: the flag stays as-is, the SiteMesh 2
anchors keep their grails-layout coverage, and developers can still drop
the flag to run against SiteMesh 2 locally.
@codeconsole codeconsole force-pushed the feat/sitemesh3-default-layout branch from f816246 to 021059d Compare June 5, 2026 23:47
@codeconsole
Copy link
Copy Markdown
Contributor Author

#15585

In the filterless SiteMesh 3 pipeline the GSP capture taglib writes the
full <head>/<body> markup to the response buffer and also captures the
head/body into a Sitemesh3CapturedPage. When no decorator is selected
(e.g. a view with no <meta name="layout"> and no matching convention or
default layout), SiteMeshView writes content.getData() back. The captured
page had neither renderedContent nor pageBuffer set, so getData()
reconstructed only from (empty) properties and emitted an empty
<html><head></head><body></body></html>.

Attach the original response buffer as the captured page's rendered
content in the no-merge branch so the original page is written back when
nothing decorates it. Decorated (meta-layout) pages are unaffected: they
take the decorate branch, which reads the head/body child properties.

Verified against grails-test-examples/app1 integration tests under
SITEMESH3_TESTING_ENABLED=true: ConfigTestControllerSpec,
ControllerIncludesSpec, ControllerFromPluginSpec and
ConditionalOnPropertyFromPluginYmlSpec now pass, with no regression to
meta-layout pages (BookFunctionalSpec).
The grails-fields plugin renders embedded objects by wrapping the
sub-fields in <g:applyLayout name="_fields/embedded" params="[...]">.
SiteMesh 3's applyLayout built the body Content from the request-scoped
Sitemesh3CapturedPage (the outer page being decorated) and ignored the
tag's params, so the _fields/embedded layout's <g:layoutBody/> rendered
empty and <g:pageProperty name="legend"/> / pageProperty(name:'type')
had no values. The result was an empty <fieldset class="embedded "> with
none of the address.* inputs. SiteMesh 2's GrailsLayoutTagLib pushed a
fresh layout page and copied params to page properties, which is why the
same grails-fields plugin worked under SiteMesh 2 but not SiteMesh 3.

applyLayout now pushes a fresh Sitemesh3CapturedPage for the body render
and restores the outer page in a finally block, wires the rendered body
in via setBodyBuffer so <g:layoutBody/> works, applies each params entry
as a page property for <g:pageProperty>, and emits the raw body verbatim
when no decorator is resolved (matching SiteMesh 2's no-decorator
fallback).

Verified under SITEMESH3_TESTING_ENABLED=true: scaffolding-fields
RelationshipsFunctionalSpec (embedded address fields) passes and the full
scaffolding-fields integration suite is green, with no regression to
meta-layout pages or the earlier undecorated-page fix.
database-cleanup, scaffolding-fields and test-phases declared
grails-layout directly in addition to grails-dependencies-starter-web.
starter-web already provides the layout plugin and flips it with
SITEMESH3_TESTING_ENABLED, so with the flag on these apps ended up with
both grails-sitemesh3 (via starter-web) and grails-layout (direct) on the
classpath, and failed to boot with a jspViewResolver bean-type clash.

Remove the redundant direct grails-layout dependency so each app gets
exactly one layout plugin from starter-web: SiteMesh 2 when the flag is
off, SiteMesh 3 when it is on. The dedicated SiteMesh 2 layout coverage
remains on gsp-layout, which does not use starter-web.
A controller's "render template: 'x'" renders the GSP directly with
renderView=false and must not be decorated with a layout. Under the
filterless SiteMesh 3 integration the template view was still wrapped by
GrailsSiteMeshView and Sitemesh3LayoutFinder picked the default layout,
so partials came back wrapped in the application layout (e.g. the main
layout's title instead of the partial's own). SiteMesh 2 suppressed this
via GrailsLayoutSelector keyed on the renderView flag.

Sitemesh3LayoutFinder.selectDecoratorPaths now returns no decorator when
the current render has renderView=false and no explicit layout was
requested. An explicit "render template: ..., layout: 'x'" still sets the
LAYOUT_ATTRIBUTE and is honoured, and normal view renders (renderView=true)
are unaffected, so decorated pages still decorate.

Verified under SITEMESH3_TESTING_ENABLED=true: LayoutWithTemplateSpec
passes; BookFunctionalSpec (meta-layout pages), ControllerIncludesSpec
(incl. include-from-template), ConfigTestControllerSpec and
scaffolding-fields RelationshipsFunctionalSpec remain green.
@testlens-app
Copy link
Copy Markdown

testlens-app Bot commented Jun 6, 2026

✅ All tests passed ✅

🏷️ Commit: 8a1fbcb
▶️ Tests: 14076 executed
⚪️ Checks: 43/43 completed


Learn more about TestLens at testlens.app.

@codeconsole codeconsole marked this pull request as ready for review June 7, 2026 03:51
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.

5 participants