Skip to content
Open
Show file tree
Hide file tree
Changes from 20 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 @@ -84,7 +84,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 @@ -149,6 +150,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 @@ -36,7 +36,7 @@ class GrailsVersionSpec extends Specification {
"3.2.0" | true
"3.1.0" | true
"3.3.0" | true
"7.1.0" | false
"7.1.0" | true
Comment thread
jdaugherty marked this conversation as resolved.
Outdated
}

@Unroll
Expand Down
54 changes: 17 additions & 37 deletions grails-gsp/grails-sitemesh3/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,48 +37,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.

Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,26 @@
* specific language governing permissions and limitations
* under the License.
*/

package org.grails.plugins.sitemesh3

import jakarta.servlet.DispatcherType
import jakarta.servlet.Filter
import jakarta.servlet.FilterChain
import jakarta.servlet.ServletRequest
import jakarta.servlet.ServletResponse

import org.springframework.boot.web.servlet.FilterRegistrationBean
import org.springframework.core.env.ConfigurableEnvironment
import org.springframework.core.env.MapPropertySource
import org.springframework.core.env.PropertySource

import grails.config.Config
import grails.core.DefaultGrailsApplication
import grails.plugins.Plugin
import grails.util.Environment
import grails.util.Metadata
import org.grails.config.PropertySourcesConfig
import org.grails.gsp.compiler.GroovyPageParser
import org.grails.plugins.web.taglib.RenderSitemeshTagLib
import org.grails.web.config.http.GrailsFilters
import org.grails.web.util.WebUtils

class Sitemesh3GrailsPlugin extends Plugin {
Expand All @@ -48,55 +55,73 @@ class Sitemesh3GrailsPlugin extends Plugin {
def loadBefore = ['groovyPages']

def providedArtefacts = [
RenderSitemeshTagLib,
RenderSitemeshTagLib,
Sitemesh3LayoutTagLib,
]

static PropertySource getDefaultPropertySource(ConfigurableEnvironment configurableEnvironment, String defaultLayout) {

Map props = [
'grails.gsp.view.layoutViewResolver': 'false',
'sitemesh.decorator.metaTag': 'layout',
'sitemesh.decorator.attribute': WebUtils.LAYOUT_ATTRIBUTE,
'sitemesh.decorator.prefix': '/layouts/',
'sitemesh.filter.order': GrailsFilters.SITEMESH_FILTER.order,
'sitemesh.decorator.tagRuleBundles': ['org.sitemesh.content.tagrules.html.Sm2TagRuleBundle']
]
if (defaultLayout) {
props['sitemesh.decorator.default'] = defaultLayout
}
// if property already exists, don't override
props.clone().each {
if (configurableEnvironment.getProperty(it.key)) {
props.remove(it.key)
}
}
return new MapPropertySource('defaultSitemesh3Properties', props)
new MapPropertySource('defaultSitemesh3Properties', props)
}

Closure doWithSpring() {
{ ->
ConfigurableEnvironment configurableEnvironment = grailsApplication.mainContext.environment as ConfigurableEnvironment
def propertySources = configurableEnvironment.getPropertySources()
// https://grails.apache.org/docs/latest/guide/single.html#layouts
// Default view should be application, but it is inefficient to add a rule for a page that may not exist.
String defaultLayout = grailsApplication.getConfig().getProperty('grails.sitemesh.default.layout')
propertySources.addFirst(getDefaultPropertySource(configurableEnvironment, defaultLayout))
propertySources.addFirst(new MapPropertySource('requiredSitemesh3Properties', [
(GroovyPageParser.CONFIG_PROPERTY_GSP_GRAILS_LAYOUT_PREPROCESS): 'false'
]))
(grailsApplication as DefaultGrailsApplication).config = new PropertySourcesConfig(propertySources)

grailsLayoutHandlerMapping(GrailsLayoutHandlerMapping)
Config config = grailsApplication.getConfig()
boolean developmentMode = Metadata.getCurrent().isDevelopmentEnvironmentAvailable()
Environment env = Environment.current
boolean enableReload = env.isReloadEnabled() ||
config.getProperty('grails.gsp.enable.reload', Boolean, false) ||
(developmentMode && env == Environment.DEVELOPMENT)
String resolvedDefaultLayout = config.getProperty('grails.sitemesh.default.layout') ?:
config.getProperty('sitemesh.decorator.default')
Comment thread
codeconsole marked this conversation as resolved.
Outdated

// Bean names match the @ConditionalOnMissingBean(name = "contentProcessor"/"decoratorSelector")
// guards on upstream's SiteMeshViewResolverAutoConfiguration, so
// our implementations replace upstream's defaults.
contentProcessor(CaptureAwareContentProcessor)

decoratorSelector(Sitemesh3LayoutFinder, ref('groovyPageLocator')) {
gspReloadEnabled = enableReload
defaultDecoratorName = resolvedDefaultLayout ?: null
}

// Replace the filter registration from
// org.sitemesh.autoconfigure.SiteMeshAutoConfiguration with a no-op
// filter bean under the same name. SiteMeshAutoConfiguration is
// @ConditionalOnMissingBean(name = "sitemesh") so registering this
// bean disables the upstream filter-based integration entirely.
// Decoration is done by the Spring MVC view resolver chain.
sitemesh(FilterRegistrationBean) { bean ->
Comment thread
codeconsole marked this conversation as resolved.
bean.autowire = false
filter = new NoopSitemeshFilter()
enabled = false
dispatcherTypes = EnumSet.of(DispatcherType.REQUEST)
}
}
}

void doWithApplicationContext() {}

void doWithDynamicMethods() {}

void onChange(Map<String, Object> event) {}

void onConfigChange(Map<String, Object> event) {}

void onShutdown(Map<String, Object> event) {}
static class NoopSitemeshFilter implements Filter {
@Override
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
chain.doFilter(request, response)
}
}
}
Loading
Loading