Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
09b8b7f
feat(sitemesh3): add capture page and taglib
codeconsole Apr 18, 2026
d25d531
feat(sitemesh3): add capture-aware content processor
codeconsole Apr 18, 2026
62b3f43
feat(sitemesh3): add view-resolver dispatch context
codeconsole Apr 18, 2026
b6d3e2b
feat(sitemesh3): add layout view, resolver, and post-processor
codeconsole Apr 18, 2026
a89f62a
feat(sitemesh3): add convention-based decorator selector
codeconsole Apr 18, 2026
512755c
refactor(sitemesh3): wire plugin to view-resolver path, drop filter
codeconsole Apr 18, 2026
858b46d
test(sitemesh3): add specs for new components
codeconsole Apr 18, 2026
c2b44b9
fix(sitemesh3): correct decoration second-pass, layout capture isolat…
codeconsole Apr 18, 2026
06523ae
perf(sitemesh3): eliminate the decoration-phase HTML parse
codeconsole Apr 18, 2026
71921cd
fix(sitemesh3): use view-resolver dispatch for <g:applyLayout>
codeconsole Apr 18, 2026
01e158c
perf(sitemesh3): pass captured buffers as CharSequence, not String
codeconsole Apr 18, 2026
d3dd277
fix(sitemesh3): review fixes — null-guard, prefix match, unused impor…
codeconsole Apr 18, 2026
517baf3
build(sitemesh3): bump to 3.2.3-SNAPSHOT for view-resolver integration
codeconsole Apr 19, 2026
99a7e8d
refactor(sitemesh3): consume upstream spring-webmvc-sitemesh module
codeconsole Apr 19, 2026
ee00982
test(sitemesh3): specs for Grails BPP + view/context subclasses
codeconsole Apr 19, 2026
41ba93c
perf/fix(sitemesh3): review follow-ups — volatile flag, extractHead l…
codeconsole Apr 19, 2026
cb3d5ab
refactor(sitemesh3): drop field duplication in GrailsSiteMeshView
codeconsole Apr 19, 2026
eb2b3c1
build: allow org.sitemesh.* snapshots from central.sonatype.com
codeconsole Apr 19, 2026
792e2e7
fix spec
codeconsole Apr 16, 2026
497e6b6
code style changes
codeconsole Apr 19, 2026
64173fa
Merge branch '7.1.x' into feature/sitemesh3-viewresolver
codeconsole Apr 22, 2026
5a6d36d
async support for sitemesh 3
codeconsole Apr 23, 2026
fe603f6
Merge branch '7.2.x' into feature/sitemesh3-viewresolver
codeconsole Apr 23, 2026
2b48c76
various fixes
codeconsole Apr 24, 2026
665c42d
Fix layout dispatch to route absolute paths through ViewResolver
codeconsole Apr 24, 2026
692f169
sitemesh 2 doc update
codeconsole Apr 29, 2026
35d1d4d
Merge remote-tracking branch 'upstream/7.2.x' into feature/sitemesh3-…
codeconsole May 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class GrailsRepoSettingsPlugin implements Plugin<Settings> {
url = 'https://central.sonatype.com/repository/maven-snapshots'
content {
it.includeVersionByRegex('cloud[.]wondrify.*', '.*', '.*-SNAPSHOT')
it.includeVersionByRegex('org[.]sitemesh.*', '.*', '.*-SNAPSHOT')
}
mavenContent {
it.snapshotsOnly()
Expand Down Expand Up @@ -91,6 +92,7 @@ class GrailsRepoSettingsPlugin implements Plugin<Settings> {
url = 'https://central.sonatype.com/repository/maven-snapshots'
content {
it.includeVersionByRegex('cloud[.]wondrify.*', '.*', '.*-SNAPSHOT')
it.includeVersionByRegex('org[.]sitemesh.*', '.*', '.*-SNAPSHOT')
}
mavenContent {
it.snapshotsOnly()
Expand Down
4 changes: 3 additions & 1 deletion dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ ext {
'selenium.version' : '4.38.0',
'spock.version' : '2.3-groovy-4.0',
'sitemesh.version' : '2.6.0',
'starter-sitemesh.version' : '3.2.2',
'starter-sitemesh.version' : '3.2.3-SNAPSHOT',
'spring-webmvc-sitemesh.version': '3.2.3-SNAPSHOT',
]

// Note: the name of the dependency must be the prefix of the property name so properties in the pom are resolved correctly
Expand Down Expand Up @@ -182,6 +183,7 @@ ext {
'rxjava3' : "io.reactivex.rxjava3:rxjava:${bomDependencyVersions['rxjava3.version']}",
'sitemesh' : "opensymphony:sitemesh:${bomDependencyVersions['sitemesh.version']}",
'starter-sitemesh' : "org.sitemesh:spring-boot-starter-sitemesh:${bomDependencyVersions['starter-sitemesh.version']}",
'spring-webmvc-sitemesh' : "org.sitemesh:spring-webmvc-sitemesh:${bomDependencyVersions['spring-webmvc-sitemesh.version']}",
]

// Because pom exclusions aren't properly supported by gradle, we can't inherit the grails-gradle-bom
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,4 @@ class GrailsVersionSpec extends Specification {
thrown(IllegalArgumentException)
}

}
}
25 changes: 22 additions & 3 deletions grails-doc/src/en/guide/theWebLayer/gsp/layouts.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ under the License.

==== Creating Layouts

This product includes software developed by the OpenSymphony Group (https://github.com/sitemesh). The Sitemesh framework is used to provide a decorator engine, to support view layouts. Layouts are located in the `grails-app/views/layouts` directory. A typical layout can be seen below:
Grails uses the https://github.com/sitemesh/sitemesh3[SiteMesh 3] framework as its decorator engine to support view layouts. In Grails 7.2 and later, decoration is performed by a Spring MVC `ViewResolver` rather than a Servlet filter, which improves performance and enables correct support for async controller return types.
Comment thread
codeconsole marked this conversation as resolved.

Layouts are located in the `grails-app/views/layouts` directory. A typical layout can be seen below:

[source,xml]
----
Expand Down Expand Up @@ -128,17 +130,22 @@ Alternatively, you can create a layout called `grails-app/views/layouts/book/lis
If you have both the above-mentioned layouts in place the layout specific to the action will take precedence when the list action is executed.

If a layout is not located using any of those conventions, the convention of last resort is to look for the application default layout which
is `grails-app/views/layouts/application.gsp`. The name of the application default layout may be changed by defining the property `grails.views.layout.default`
is `grails-app/views/layouts/application.gsp`. The name of the application default layout may be changed by defining the property `grails.sitemesh.default.layout`
in the application configuration as follows:

[source,yaml]
.grails-app/conf/application.yml
----
grails.views.layout.default: myLayoutName
grails:
sitemesh:
default:
layout: myLayoutName
----

With that property in place, the application default layout will be `grails-app/views/layouts/myLayoutName.gsp`.

NOTE: In Grails 7.2+ the canonical property for the default layout is `grails.sitemesh.default.layout`. The older `grails.views.layout.default` key is no longer consulted by the SiteMesh 3 integration.


==== Inline Layouts

Expand Down Expand Up @@ -186,3 +193,15 @@ def content = include(controller:"book", action:"list")
----

The resulting content will be provided via the return value of the link:{gspTagsRef}include.html[include] tag.


==== SiteMesh 2 Attribution (grails-layout)

The legacy `grails-layout` plugin remains available for applications that have not yet migrated to the SiteMesh 3 integration. When `grails-layout` is on the classpath, decoration is provided by SiteMesh 2 and the following attribution applies:

[NOTE]
====
This product includes software developed by the OpenSymphony Group (https://github.com/sitemesh). The SiteMesh 2 framework is licensed under the OpenSymphony Software License, Version 1.1.
====

This acknowledgment is required by the OpenSymphony Software License when redistributing SiteMesh 2 and applies only to applications that include the `grails-layout` plugin. Applications using only the SiteMesh 3 integration are not subject to this requirement.
1 change: 1 addition & 0 deletions grails-doc/src/en/guide/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ gettingStarted:
developmentReloading: Development Reloading
upgrading:
title: Upgrading from the previous versions
upgrading72x: Upgrading from Grails 7.1 to Grails 7.2
upgrading71x: Upgrading from Grails 7.0 to Grails 7.1
upgrading70x: Upgrading from Grails 6 to Grails 7.0
upgrading60x: Upgrading from Grails 5 to Grails 6
Expand Down
102 changes: 102 additions & 0 deletions grails-doc/src/en/guide/upgrading/upgrading72x.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
////
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
////

=== Upgrade Instructions for Grails 7.1 to Grails 7.2

This guide outlines the changes introduced in Grails 7.2 and the steps required to upgrade your application.

==== 1. SiteMesh 3 Filter-less Integration

Grails 7.2 upgrades the SiteMesh 3 integration to a *filter-less* architecture. Decoration is now performed by a Spring MVC `ViewResolver` rather than a Servlet filter. This change improves performance, eliminates ordering issues with other filters, and correctly supports async controller return types (`Callable`, `@Async`).

===== What Changed

* The `SiteMeshFilter` servlet filter is no longer active. A `NoopSitemeshFilter` placeholder bean is registered under the `sitemesh` name to prevent SiteMesh's auto-configuration from activating the upstream filter-based integration.
* Layout resolution and decoration are now handled by `Sitemesh3LayoutView` (a Spring `View`) and `CaptureAwareContentProcessor`.
* GSP pages populate a `Sitemesh3CapturedPage` at render time via the `grailsLayout` capture tag library. SiteMesh no longer parses the raw HTML response to extract head/body/title.

===== Required Actions

For most applications **no changes are required**. The filter-less integration is a drop-in replacement: layouts, `<g:layoutHead>`, `<g:layoutTitle>`, `<g:layoutBody>`, `<g:pageProperty>`, `<g:applyLayout>`, and layout-by-convention all behave identically to the previous release.

[IMPORTANT]
====
The guidance below applies *only* when the SiteMesh 3 integration is in use. The legacy `grails-layout` plugin (SiteMesh 2) remains supported in Grails 7.2 and continues to honor its existing configuration. If your application still depends on `grails-layout`, leave the SiteMesh 2 properties in place — they remain meaningful for that codepath.
====

When using the SiteMesh 3 integration, the following SiteMesh 2-era configuration properties are no longer consulted and can be removed from your `application.yml` if present:

[source,yaml]
----
# Remove these — no longer used by the SiteMesh 3 integration in Grails 7.2.
# (Retain them if your application still uses the grails-layout / SiteMesh 2 plugin.)
grails.gsp.view.layoutViewResolver: false
sitemesh.filter.order: ...
sitemesh.decorator.tagRuleBundles: ...
----

===== Configuring the Default Layout

The default layout is still configured via:

[source,yaml]
.grails-app/conf/application.yml
----
grails:
sitemesh:
default:
layout: application
----

===== Layout Cache Tuning (Optional)

In non-development environments, layout paths are cached by controller/action pair. The cache TTL can be tuned independently of the GSP reload interval using a dedicated key:

[source,yaml]
.grails-app/conf/application.yml
----
grails:
sitemesh:
layout:
cache:
interval: 5000 # milliseconds, default 5000
----

This replaces any reliance on `grails.gsp.reload.interval` for layout cache expiration in previous snapshots.

===== Async Controller Support

The filter-less architecture correctly handles Grails controllers that return `Callable` or use `@Async`. Previously, async dispatch could cause decoration to run on a different thread from the one that populated the SiteMesh request scope, leading to empty layouts or stale content. This is resolved in 7.2.

===== Content Blocks

Content blocks (the `<content tag="...">` pattern) continue to work as before. The GSP preprocessor maps them to `<grailsLayout:captureContent tag="...">` at compile time, and the captured buffers are passed directly to the layout without an additional HTML parse.

[source,xml]
----
<%-- Page --%>
<content tag="sidebar">
... sidebar content ...
</content>

<%-- Layout --%>
<div id="sidebar">
<g:pageProperty name="page.sidebar" />
</div>
----
54 changes: 17 additions & 37 deletions grails-gsp/grails-sitemesh3/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,48 +38,28 @@ ext {

dependencies {

astImplementation platform(project(':grails-bom'))

implementation platform(project(':grails-bom'))
api "org.sitemesh:spring-boot-starter-sitemesh"
api "org.sitemesh:spring-webmvc-sitemesh"
api project(':grails-web-gsp-taglib')
api project(':grails-web-gsp')
api project(':grails-web-common')
api project(':grails-gsp')
api project(':grails-core')

implementation 'org.apache.groovy:groovy'

// api project(':grails-web-gsp'), { // GrailsConventionGroovyPageLocator
// // API dependencies in grails-web-gsp
// //exclude group: 'org.apache.grails.views', module: 'grails-gsp-core' // DefaultGroovyPageLocator
// exclude group: 'org.apache.grails.web', module: 'grails-web-common'
// exclude group: 'org.apache.grails.web', module: 'grails-web-taglib'
// }
// api project(':grails-web-gsp-taglib'), { // GrailsConventionGroovyPageLocator
// // API dependencies in grails-web-gsp-taglib
// exclude group: 'org.apache.grails.web', module: 'grails-taglib'
// //exclude group: 'org.apache.grails.web', module: 'grails-web-gsp' // DefaultGroovyPageLocator
// }
// api "org.sitemesh:sitemesh:$sitemeshVersion" // SiteMeshFilter
// api 'org.springframework:spring-webmvc' // AbstractHandlerAdapter, ParameterizableViewController, AbstractHandlerMapping
// api 'org.springframework.boot:spring-boot' // FilterRegistrationBean
//
// implementation project(':grails-web-taglib'), { // TagLib, TagLibrary
// // API dependencies in grails-web-taglib
// exclude group: 'org.apache.grails.web', module: 'grails-taglib'
// exclude group: 'org.apache.grails.web', module: 'grails-web-common'
// }
// implementation project(':grails-web-common'), { // WebUtils
// // API dependencies in grails-web-common
// exclude group: 'org.apache.groovy', module: 'groovy-templates'
// exclude group: 'org.apache.grails', module: 'grails-core'
// exclude group: 'org.apache.grails', module: 'grails-databinding'
// exclude group: 'org.apache.grails', module: 'grails-encoder'
// exclude group: 'org.springframework', module: 'spring-webmvc'
// exclude group: 'org.springframework', module: 'spring-context-support'
// }
// implementation 'org.springframework:spring-beans' // Autowired, Qualifier
// implementation 'org.springframework:spring-web' // HttpMethod, ResponseStatusException, HttpStatus
//
// runtimeOnly "org.apache.grails:grails-codecs:$grailsVersion" // Registers CodecLookup bean
// runtimeOnly "org.sitemesh:spring-boot-starter-sitemesh:$sitemeshVersion"
//
// compileOnly 'jakarta.servlet:jakarta.servlet-api' // Provided by servlet container
// compileOnly 'org.apache.groovy:groovy' // Provided by Grails Application
compileOnly 'jakarta.servlet:jakarta.servlet-api'

testImplementation 'jakarta.servlet:jakarta.servlet-api'
testImplementation 'org.spockframework:spock-core'
testImplementation 'org.springframework:spring-test'
testImplementation project(':grails-testing-support-web')
testImplementation 'org.objenesis:objenesis'
testRuntimeOnly 'net.bytebuddy:byte-buddy'
testRuntimeOnly 'org.slf4j:slf4j-nop'
}

apply {
Expand Down

This file was deleted.

Loading
Loading