Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b0e0990
Make SiteMesh 3 the default GSP layout, mutually exclusive with grail…
codeconsole Jun 2, 2026
769e971
Merge branch 'deps/sitemesh3-3.3.0-snapshot' into feat/sitemesh3-defa…
jdaugherty Jun 3, 2026
d561d7d
Undo revert
jdaugherty Jun 3, 2026
9784068
Merge branch '8.0.x' into feat/sitemesh3-default-layout
jamesfredley Jun 3, 2026
a553b47
Model the GSP layout choice as a one-of option to fix the forge UI gl…
codeconsole Jun 4, 2026
021059d
Run CI test apps against SiteMesh 3 by default
codeconsole Jun 5, 2026
920c12c
Fix empty SiteMesh 3 output for undecorated GSP responses
codeconsole Jun 6, 2026
81d9147
Fix SiteMesh 3 applyLayout for embedded/nested field rendering
codeconsole Jun 6, 2026
6178aeb
Drop redundant grails-layout from starter-web test apps
codeconsole Jun 6, 2026
8a1fbcb
Don't apply a SiteMesh 3 layout to template/partial renders
codeconsole Jun 6, 2026
8b5db52
Default test apps to SiteMesh 3, opt into SiteMesh 2 via flag
codeconsole Jun 10, 2026
692ee12
Apply gspLayout option in the project generation endpoints
codeconsole Jun 10, 2026
852d132
Drop redundant user-agent param from the GitHub create endpoint
codeconsole Jun 10, 2026
db817eb
Fix if/else spacing in the SiteMesh test-app build files
codeconsole Jun 10, 2026
51d5e88
Make SiteMesh 3 the silent forge default, SiteMesh 2 a selectable fea…
codeconsole Jun 10, 2026
bb94c8c
Merge branch '8.0.x' into feat/sitemesh3-default-layout
jamesfredley Jun 10, 2026
a61691e
Re-enable the gsp-sitemesh3 test example
codeconsole Jun 10, 2026
6e83b25
Fix SiteMesh 3 layout chaining, error-page decoration and JSP rendering
codeconsole Jun 10, 2026
e7390ac
Drop SiteMesh 3 plugin workarounds superseded by the updated starter
codeconsole Jun 11, 2026
53ee6ca
Fix stale SiteMesh toggle in the hibernate7 test examples
codeconsole Jun 11, 2026
3589ba6
Don't mis-slice head content around title-prefixed elements
codeconsole Jun 11, 2026
751a937
Match SiteMesh 2's default layout config and application fallback
codeconsole Jun 11, 2026
159f812
Run the jetty and scaffolding examples against SiteMesh 3 by default
codeconsole Jun 11, 2026
8e6a989
Publish grails-gsp-spring-boot and re-enable its example
codeconsole Jun 11, 2026
7040b86
Support the full applyLayout attribute surface under SiteMesh 3
codeconsole Jun 11, 2026
c7a29df
Merge remote-tracking branch 'apache/8.0.x' into feat/sitemesh3-defau…
codeconsole Jun 11, 2026
14da8e4
Apply the published-module build conventions to grails-gsp-spring-boot
codeconsole Jun 11, 2026
5e636fd
Leave the view resolver unwrapped when the SiteMesh 3 beans are absent
codeconsole Jun 11, 2026
536da4d
Defer to SiteMesh 2's layout resolver instead of double-wrapping it
codeconsole Jun 11, 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
1 change: 1 addition & 0 deletions gradle/publish-root-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def publishedProjects = [
'grails-geb',
'grails-gsp',
'grails-gsp-core',
'grails-gsp-spring-boot',
'grails-i18n',
'grails-interceptors',
'grails-layout',
Expand Down
2 changes: 1 addition & 1 deletion grails-dependencies/starter-web/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def configurations = [
':grails-events',
':grails-gsp',
':grails-interceptors',
System.getenv('SITEMESH3_TESTING_ENABLED') == 'true' ? ':grails-sitemesh3' : ':grails-layout',
System.getenv('SITEMESH2_TESTING_ENABLED') == 'true' ? ':grails-layout' : ':grails-sitemesh3',
':grails-logging',
':grails-rest-transforms',
':grails-services',
Expand Down
5 changes: 4 additions & 1 deletion grails-doc/src/en/ref/Tags - GSP/applyLayout.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,12 @@ Attributes
* `name` - The name of the layout
* `template` - (optional) The template to apply the layout to
* `url` - (optional) The URL to retrieve the content from and apply a layout to
* `action` - (optional) The action whose output to include and apply a layout to, used together with `controller`
* `controller` - (optional) The controller whose action output to include and apply a layout to
* `contentType` (optional) - The content type to use, default is "text/html"
* `encoding` (optional) - The encoding to use
* `params` (optional) - The params to pass onto the page object (retrievable with the xref:pageProperty.html[pageProperty] tag)
* `params` (optional) - The params to pass onto the page object (retrievable with the xref:pageProperty.html[pageProperty] tag); for `action`/`controller` content they are also passed as request parameters to the include
* `model` (optional) - The model (as java.util.Map) to pass to the view and layout templates
* `parse` (optional) - If `true`, forces the content to be parsed for head, title and body sections instead of reusing an already captured page


Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,22 @@
import org.grails.forge.application.ApplicationType;
import org.grails.forge.application.generator.GeneratorContext;
import org.grails.forge.build.dependencies.Dependency;
import org.grails.forge.feature.Category;
import org.grails.forge.feature.DefaultFeature;
import org.grails.forge.feature.Feature;
import org.grails.forge.feature.view.GspLayout;
import org.grails.forge.options.Options;

@Singleton
public class Sitemesh3 implements Feature {
import java.util.Set;

public Sitemesh3() {
}
/**
* Default GSP layout decorator, backed by SiteMesh 3 ({@code grails-sitemesh3}).
* Applied automatically to web applications unless another {@link GspLayout}
* (e.g. {@code grails-layout}) is explicitly selected. Not visible: SiteMesh 3
* is silently the default, mirroring how the legacy {@code grails-layout} was
* silently the default before it.
*/
@Singleton
public class Sitemesh3 extends GspLayout implements DefaultFeature {

@Override
public String getName() {
Expand All @@ -38,29 +46,30 @@ public String getName() {

@Override
public String getTitle() {
return "Sitemesh 3";
return "GSP SiteMesh 3 Layouts";
}

@Override
public String getDescription() {
return "Adds support for Sitemesh3 based layouts instead of Sitemesh 2";
return "Adds support for SiteMesh 3 based GSP layouts";
}

@Override
public void apply(GeneratorContext generatorContext) {
generatorContext.addDependency(Dependency.builder()
.groupId("org.apache.grails")
.artifactId("grails-sitemesh3")
.implementation());
public boolean isVisible() {
return false;
}

@Override
public boolean supports(ApplicationType applicationType) {
return true;
public boolean shouldApply(ApplicationType applicationType, Options options, Set<Feature> selectedFeatures) {
return supports(applicationType) &&
selectedFeatures.stream().noneMatch(GspLayout.class::isInstance);
}

@Override
public String getCategory() {
return Category.VIEW;
public void apply(GeneratorContext generatorContext) {
generatorContext.addDependency(Dependency.builder()
.groupId("org.apache.grails")
.artifactId("grails-sitemesh3")
.implementation());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
import org.grails.forge.feature.DefaultFeature;
import org.grails.forge.feature.Feature;
import org.grails.forge.feature.FeatureContext;
import org.grails.forge.feature.sitemesh3.Sitemesh3;
import org.grails.forge.feature.web.GrailsWeb;
import org.grails.forge.options.Options;
import org.grails.forge.template.URLTemplate;
Expand Down Expand Up @@ -110,12 +109,6 @@ public void apply(GeneratorContext generatorContext) {
.groupId("org.apache.grails")
.artifactId("grails-gsp")
.implementation());
if (!generatorContext.isFeaturePresent(Sitemesh3.class)) {
generatorContext.addDependency(Dependency.builder()
.groupId("org.apache.grails")
.artifactId("grails-layout")
.implementation());
}

final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
generatorContext.addTemplate("mainLayout", new URLTemplate(getViewFolderPath() + "layouts/main.gsp", classLoader.getResource("gsp/main.gsp")));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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.
*/
package org.grails.forge.feature.view;

import jakarta.inject.Singleton;
import org.grails.forge.application.generator.GeneratorContext;
import org.grails.forge.build.dependencies.Dependency;

/**
* Opt-in GSP layout decorator backed by the legacy SiteMesh 2 based
* {@code grails-layout} plugin. Mutually exclusive with {@code sitemesh3};
* selecting this feature replaces the default SiteMesh 3 decorator.
*/
@Singleton
public class GrailsLayout extends GspLayout {

@Override
public String getName() {
return "grails-layout";
}

@Override
public String getTitle() {
return "GSP SiteMesh 2 Layouts";
}

@Override
public String getDescription() {
return "Adds support for legacy SiteMesh 2 based GSP layouts (grails-layout) instead of SiteMesh 3";
}

@Override
public void apply(GeneratorContext generatorContext) {
generatorContext.addDependency(Dependency.builder()
.groupId("org.apache.grails")
.artifactId("grails-layout")
.implementation());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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.
*/
package org.grails.forge.feature.view;

import org.grails.forge.application.ApplicationType;
import org.grails.forge.feature.Category;
import org.grails.forge.feature.OneOfFeature;

/**
* Common parent for the mutually exclusive GSP layout decorators: SiteMesh 3
* ({@code grails-sitemesh3}) and the legacy SiteMesh 2 based {@code grails-layout}.
* Because they share the same {@link #getFeatureClass()}, only one of them may be
* selected for a given application (enforced by the one-of feature validator).
*/
public abstract class GspLayout implements OneOfFeature {

@Override
public Class<?> getFeatureClass() {
return GspLayout.class;
}

@Override
public boolean supports(ApplicationType applicationType) {
return applicationType == ApplicationType.WEB || applicationType == ApplicationType.WEB_PLUGIN;
}

@Override
public String getCategory() {
return Category.VIEW;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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.
*/

package org.grails.forge.feature.view

import org.grails.forge.ApplicationContextSpec
import org.grails.forge.BuildBuilder
import org.grails.forge.application.ApplicationType
import spock.lang.Unroll

class GspLayoutSpec extends ApplicationContextSpec {

@Unroll
void "web #applicationType apps use SiteMesh 3 (grails-sitemesh3) by default"() {
when:
final String build = new BuildBuilder(beanContext)
.features(['gsp'])
.applicationType(applicationType)
.render()

then:
build.contains('implementation "org.apache.grails:grails-sitemesh3"')
!build.contains('implementation "org.apache.grails:grails-layout"')

where:
applicationType << [ApplicationType.WEB, ApplicationType.WEB_PLUGIN]
}

void "selecting grails-layout replaces the default SiteMesh 3 decorator"() {
when:
final String build = new BuildBuilder(beanContext)
.features(['gsp', 'grails-layout'])
.applicationType(ApplicationType.WEB)
.render()

then:
build.contains('implementation "org.apache.grails:grails-layout"')
!build.contains('implementation "org.apache.grails:grails-sitemesh3"')
}

void "sitemesh3 is the silent default and is not a selectable feature"() {
when:
new BuildBuilder(beanContext)
.features(['gsp', 'sitemesh3'])
.applicationType(ApplicationType.WEB)
.render()

then:
IllegalArgumentException e = thrown()
e.message.contains('The requested feature does not exist: sitemesh3')
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,6 @@
*/
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
Expand Down Expand Up @@ -80,7 +73,11 @@ class Sitemesh3GrailsPlugin extends Plugin {
{ ->
ConfigurableEnvironment configurableEnvironment = grailsApplication.mainContext.environment as ConfigurableEnvironment
def propertySources = configurableEnvironment.getPropertySources()
String defaultLayout = grailsApplication.getConfig().getProperty('grails.sitemesh.default.layout')
// The SiteMesh 3 specific key wins; fall back to the SiteMesh 2
// plugin's grails.views.layout.default so existing apps keep
// their configured default layout when switching.
String defaultLayout = grailsApplication.getConfig().getProperty('grails.sitemesh.default.layout') ?:
grailsApplication.getConfig().getProperty('grails.views.layout.default')
propertySources.addFirst(getDefaultPropertySource(configurableEnvironment, defaultLayout))
(grailsApplication as DefaultGrailsApplication).config = new PropertySourcesConfig(propertySources)

Expand All @@ -103,29 +100,10 @@ class Sitemesh3GrailsPlugin extends Plugin {
layoutCacheExpirationMillis = config.getProperty('grails.sitemesh.layout.cache.interval', Long, 5000L)
}

// 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 ->
bean.autowire = false
filter = new NoopSitemeshFilter()
enabled = false
dispatcherTypes = EnumSet.of(DispatcherType.REQUEST)
}
}
}

// This class is never invoked (the FilterRegistrationBean has enabled = false).
// It exists solely so we can register a bean named "sitemesh" and satisfy
// SiteMeshAutoConfiguration's @ConditionalOnMissingBean(name = "sitemesh")
// guard — preventing the upstream filter-based integration from activating.
static class NoopSitemeshFilter implements Filter {
@Override
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
chain.doFilter(request, response)
// Unwraps the SiteMesh view for "render template:" partials so
// they are never decorated with a layout (the SiteMesh 2 plugin
// does the same with its GrailsLayoutRenderViewMutator).
grailsRenderViewMutator(Sitemesh3RenderViewMutator)
}
}
}
Loading
Loading