From b0e0990395e5def7766381514412abab6cd9c6f2 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Tue, 2 Jun 2026 13:13:01 -0700 Subject: [PATCH 01/25] Make SiteMesh 3 the default GSP layout, mutually exclusive with grails-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. --- .../forge/feature/sitemesh3/Sitemesh3.java | 36 +++++----- .../grails/forge/feature/view/GrailsGsp.java | 7 -- .../forge/feature/view/GrailsLayout.java | 55 ++++++++++++++ .../grails/forge/feature/view/GspLayout.java | 47 ++++++++++++ .../forge/feature/view/GspLayoutSpec.groovy | 72 +++++++++++++++++++ 5 files changed, 193 insertions(+), 24 deletions(-) create mode 100644 grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GrailsLayout.java create mode 100644 grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GspLayout.java create mode 100644 grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/view/GspLayoutSpec.groovy diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/sitemesh3/Sitemesh3.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/sitemesh3/Sitemesh3.java index 73fd1dc66a3..7d5fcddb2fd 100644 --- a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/sitemesh3/Sitemesh3.java +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/sitemesh3/Sitemesh3.java @@ -22,14 +22,20 @@ 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. + */ +@Singleton +public class Sitemesh3 extends GspLayout implements DefaultFeature { @Override public String getName() { @@ -38,12 +44,18 @@ public String getName() { @Override public String getTitle() { - return "Sitemesh 3"; + return "SiteMesh 3"; } @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 boolean shouldApply(ApplicationType applicationType, Options options, Set selectedFeatures) { + return supports(applicationType) && + selectedFeatures.stream().noneMatch(GspLayout.class::isInstance); } @Override @@ -53,14 +65,4 @@ public void apply(GeneratorContext generatorContext) { .artifactId("grails-sitemesh3") .implementation()); } - - @Override - public boolean supports(ApplicationType applicationType) { - return true; - } - - @Override - public String getCategory() { - return Category.VIEW; - } } diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GrailsGsp.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GrailsGsp.java index 207777d7a29..3960b754f8c 100644 --- a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GrailsGsp.java +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GrailsGsp.java @@ -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; @@ -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"))); diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GrailsLayout.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GrailsLayout.java new file mode 100644 index 00000000000..5bd2b958b9c --- /dev/null +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GrailsLayout.java @@ -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()); + } +} diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GspLayout.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GspLayout.java new file mode 100644 index 00000000000..d818518645f --- /dev/null +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GspLayout.java @@ -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; + } +} diff --git a/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/view/GspLayoutSpec.groovy b/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/view/GspLayoutSpec.groovy new file mode 100644 index 00000000000..3ddadd80582 --- /dev/null +++ b/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/view/GspLayoutSpec.groovy @@ -0,0 +1,72 @@ +/* + * 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"') + + and: "a SNAPSHOT build exposes the SiteMesh 3 snapshot repo so org.sitemesh:*-SNAPSHOT resolves" + build.contains("https://central.sonatype.com/repository/maven-snapshots") + build.contains("includeVersionByRegex('org[.]sitemesh.*'") + + 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 and grails-layout cannot both be selected"() { + when: + new BuildBuilder(beanContext) + .features(["gsp", "sitemesh3", "grails-layout"]) + .applicationType(ApplicationType.WEB) + .render() + + then: + IllegalArgumentException e = thrown() + e.message.contains("There can only be one of the following features selected") + } +} From d561d7defa1eb04a73dca28956b48f4d51482974 Mon Sep 17 00:00:00 2001 From: James Daugherty Date: Wed, 3 Jun 2026 09:47:51 -0400 Subject: [PATCH 02/25] Undo revert --- grails-profiles/web/profile.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grails-profiles/web/profile.yml b/grails-profiles/web/profile.yml index bc8d2f0d979..f02234e38f6 100644 --- a/grails-profiles/web/profile.yml +++ b/grails-profiles/web/profile.yml @@ -44,7 +44,7 @@ dependencies: - scope: implementation coords: "org.apache.grails:grails-url-mappings" - scope: implementation - coords: "org.apache.grails:grails-layout" + coords: "org.apache.grails:grails-sitemesh3" - scope: implementation coords: "org.apache.grails:grails-interceptors" - scope: implementation From a553b4724a2ff1f0ea527110cda8767091dbcc4b Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Thu, 4 Jun 2026 11:24:36 -0700 Subject: [PATCH 03/25] Model the GSP layout choice as a one-of option to fix the forge UI glitch 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. --- .../forge/api/ApplicationController.java | 3 +- .../grails/forge/api/GspLayoutImplDTO.java | 100 ++++++++++++++++++ .../grails/forge/api/SelectOptionsDTO.java | 22 +++- .../options/GspLayoutImplSelectOptions.java | 42 ++++++++ .../SelectOptionsControllerSpec.groovy | 48 +++++++++ .../forge/cli/command/CreateCommand.java | 13 ++- .../cli/command/GspLayoutImplCandidates.java | 31 ++++++ .../cli/command/GspLayoutImplConverter.java | 41 +++++++ .../forge/application/ContextFactory.java | 10 +- .../forge/feature/sitemesh3/Sitemesh3.java | 12 +-- .../forge/feature/view/GrailsLayout.java | 15 ++- .../grails/forge/feature/view/GspLayout.java | 12 ++- .../grails/forge/options/FeatureFilter.java | 10 ++ .../grails/forge/options/GspLayoutImpl.java | 54 ++++++++++ .../org/grails/forge/options/Options.java | 31 +++++- .../org/grails/forge/BuildBuilder.groovy | 8 ++ .../forge/feature/view/GspLayoutSpec.groovy | 29 ++--- 17 files changed, 449 insertions(+), 32 deletions(-) create mode 100644 grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/GspLayoutImplDTO.java create mode 100644 grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/options/GspLayoutImplSelectOptions.java create mode 100644 grails-forge/grails-forge-api/src/test/groovy/org/grails/forge/api/options/SelectOptionsControllerSpec.groovy create mode 100644 grails-forge/grails-forge-cli/src/main/java/org/grails/forge/cli/command/GspLayoutImplCandidates.java create mode 100644 grails-forge/grails-forge-cli/src/main/java/org/grails/forge/cli/command/GspLayoutImplConverter.java create mode 100644 grails-forge/grails-forge-core/src/main/java/org/grails/forge/options/GspLayoutImpl.java diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/ApplicationController.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/ApplicationController.java index c3182db1e4e..64a0319236b 100644 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/ApplicationController.java +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/ApplicationController.java @@ -235,6 +235,7 @@ protected Options getOptions(@Nullable FeatureFilter filter, RequestInfo request filter.getGorm() == null ? GormImpl.DEFAULT_OPTION : filter.getGorm(), filter.getServlet() == null ? ServletImpl.DEFAULT_OPTION : filter.getServlet(), filter.getJavaVersion() == null ? JdkVersion.DEFAULT_OPTION : filter.getJavaVersion(), - getOperatingSystem(requestInfo.getUserAgent())); + getOperatingSystem(requestInfo.getUserAgent())) + .withGspLayoutImpl(filter.getGspLayout() == null ? GspLayoutImpl.DEFAULT_OPTION : filter.getGspLayout()); } } diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/GspLayoutImplDTO.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/GspLayoutImplDTO.java new file mode 100644 index 00000000000..c25b70329c3 --- /dev/null +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/GspLayoutImplDTO.java @@ -0,0 +1,100 @@ +/* + * 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.api; + +import io.micronaut.context.MessageSource; +import io.micronaut.core.annotation.Creator; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.naming.Described; +import io.micronaut.core.naming.Named; +import io.swagger.v3.oas.annotations.media.Schema; +import org.grails.forge.options.GspLayoutImpl; + +/** + * DTO objects for {@link GspLayoutImpl}. + * + * @since 8.0.0 + */ +@Schema(name = "GspLayoutImplInfo") +@Introspected +public class GspLayoutImplDTO extends Linkable implements Named, Described, Selectable { + + static final String MESSAGE_PREFIX = GrailsForgeConfiguration.PREFIX + ".gspLayoutImpl."; + + private final String name; + private final String description; + private final GspLayoutImpl value; + + /** + * @param gspLayoutImpl The {@link GspLayoutImpl} + */ + public GspLayoutImplDTO(GspLayoutImpl gspLayoutImpl) { + this.value = gspLayoutImpl; + this.name = gspLayoutImpl.getName(); + this.description = gspLayoutImpl.getName(); + } + + @Creator + @Internal + GspLayoutImplDTO(GspLayoutImpl gspLayoutImpl, + String name, + String description) { + this.value = gspLayoutImpl; + this.name = name; + this.description = description; + } + + @Internal + GspLayoutImplDTO(GspLayoutImpl gspLayoutImpl, + MessageSource messageSource, + MessageSource.MessageContext messageContext) { + this.value = gspLayoutImpl; + String name = gspLayoutImpl.getName(); + this.name = name; + this.description = messageSource.getMessage(MESSAGE_PREFIX + name + ".description", messageContext, name); + } + + @NonNull + @Override + @Schema(description = "A description of the GSP Layout Implementation") + public String getDescription() { + return description; + } + + @Override + @Schema(description = "The name of the GSP Layout Implementation") + @NonNull + public String getName() { + return name; + } + + @Override + @Schema(description = "The value of the GSP Layout Implementation for select options") + public GspLayoutImpl getValue() { + return value; + } + + @Override + @Schema(description = "The label of the GSP Layout Implementation for select options") + public String getLabel() { + return value.getLabel(); + } +} diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/SelectOptionsDTO.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/SelectOptionsDTO.java index f25ee04940f..b4345bf574a 100644 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/SelectOptionsDTO.java +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/SelectOptionsDTO.java @@ -52,6 +52,8 @@ public class SelectOptionsDTO { private ServletImplSelectOptions servlet; + private GspLayoutImplSelectOptions gspLayout; + SelectOptionsDTO() { } @@ -61,13 +63,15 @@ public SelectOptionsDTO(ApplicationTypeSelectOptions type, LanguageSelectOptions lang, DevelopmentReloadingSelectOptions reloading, GormImplSelectOptions gorm, - ServletImplSelectOptions servlet) { + ServletImplSelectOptions servlet, + GspLayoutImplSelectOptions gspLayout) { this.type = type; this.jdkVersion = jdkVersion; this.lang = lang; this.reloading = reloading; this.gorm = gorm; this.servlet = servlet; + this.gspLayout = gspLayout; } @Schema(description = "supported options for application type") @@ -100,6 +104,11 @@ public ServletImplSelectOptions getServlet() { return servlet; } + @Schema(description = "supported options for GSP Layout Implementation") + public GspLayoutImplSelectOptions getGspLayout() { + return gspLayout; + } + /** * Build the options * @@ -163,8 +172,17 @@ public static SelectOptionsDTO make(MessageSource messageSource, MessageSource.M new ServletImplDTO(ServletImpl.DEFAULT_OPTION, messageSource, messageContext) ); + List gspLayoutImpls = Arrays.stream(GspLayoutImpl.values()) + .map(it -> new GspLayoutImplDTO(it, messageSource, messageContext)) + .collect(Collectors.toList()); + + GspLayoutImplSelectOptions gspLayoutImplOpts = new GspLayoutImplSelectOptions( + gspLayoutImpls, + new GspLayoutImplDTO(GspLayoutImpl.DEFAULT_OPTION, messageSource, messageContext) + ); + - return new SelectOptionsDTO(applicationOpts, jdkVersionOpts, languageOpts, developmentReloadingOpts, gormImplOpts, servletImplOpts); + return new SelectOptionsDTO(applicationOpts, jdkVersionOpts, languageOpts, developmentReloadingOpts, gormImplOpts, servletImplOpts, gspLayoutImplOpts); } } diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/options/GspLayoutImplSelectOptions.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/options/GspLayoutImplSelectOptions.java new file mode 100644 index 00000000000..c7e76e431d5 --- /dev/null +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/options/GspLayoutImplSelectOptions.java @@ -0,0 +1,42 @@ +/* + * 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.api.options; + +import io.swagger.v3.oas.annotations.media.Schema; +import org.grails.forge.api.GspLayoutImplDTO; +import org.grails.forge.api.SelectOptionDTO; + +import java.util.List; + +@Schema(name = "GspLayoutImplSelectOptions") +public class GspLayoutImplSelectOptions extends SelectOptionDTO { + public GspLayoutImplSelectOptions(List options, GspLayoutImplDTO defaultOption) { + super(options, defaultOption); + } + + @Override + public List getOptions() { + return super.getOptions(); + } + + @Override + public GspLayoutImplDTO getDefaultOption() { + return super.getDefaultOption(); + } +} diff --git a/grails-forge/grails-forge-api/src/test/groovy/org/grails/forge/api/options/SelectOptionsControllerSpec.groovy b/grails-forge/grails-forge-api/src/test/groovy/org/grails/forge/api/options/SelectOptionsControllerSpec.groovy new file mode 100644 index 00000000000..349c156054f --- /dev/null +++ b/grails-forge/grails-forge-api/src/test/groovy/org/grails/forge/api/options/SelectOptionsControllerSpec.groovy @@ -0,0 +1,48 @@ +/* + * 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.api.options + +import io.micronaut.http.HttpRequest +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import org.grails.forge.api.SelectOptionsDTO +import org.grails.forge.options.GspLayoutImpl +import spock.lang.Specification + +@MicronautTest +class SelectOptionsControllerSpec extends Specification { + + @Inject + @Client("/") + HttpClient httpClient + + void "select options expose the GSP layout implementations with SiteMesh 3 as the default"() { + when: + SelectOptionsDTO selectOptions = httpClient.toBlocking() + .retrieve(HttpRequest.GET('/select-options'), SelectOptionsDTO) + + then: + selectOptions.gspLayout + selectOptions.gspLayout.defaultOption.value == GspLayoutImpl.SITEMESH3 + selectOptions.gspLayout.options*.value as Set == [GspLayoutImpl.SITEMESH3, GspLayoutImpl.GRAILS_LAYOUT] as Set + } +} diff --git a/grails-forge/grails-forge-cli/src/main/java/org/grails/forge/cli/command/CreateCommand.java b/grails-forge/grails-forge-cli/src/main/java/org/grails/forge/cli/command/CreateCommand.java index aa478ae6749..0f1272ed53f 100644 --- a/grails-forge/grails-forge-cli/src/main/java/org/grails/forge/cli/command/CreateCommand.java +++ b/grails-forge/grails-forge-cli/src/main/java/org/grails/forge/cli/command/CreateCommand.java @@ -57,6 +57,10 @@ public abstract class CreateCommand extends BaseCommand implements Callable getAdditionalOptions() { public Integer call() throws Exception { if (listFeatures) { new ListFeatures(availableFeatures, - new Options(reloading, gormImpl, servletImpl, getJdkVersion(), getOperatingSystem()), + new Options(reloading, gormImpl, servletImpl, getJdkVersion(), getOperatingSystem()).withGspLayoutImpl(getGspLayoutImpl()), applicationType, getOperatingSystem(), contextFactory).output(this); @@ -122,11 +126,16 @@ public void generate(OutputHandler outputHandler) throws Exception { } public void generate(Project project, OutputHandler outputHandler) throws Exception { - Options options = new Options(reloading, gormImpl, servletImpl, getJdkVersion(), getOperatingSystem(), getAdditionalOptions()); + Options options = new Options(reloading, gormImpl, servletImpl, getJdkVersion(), getOperatingSystem(), getAdditionalOptions()) + .withGspLayoutImpl(getGspLayoutImpl()); projectGenerator.generate(applicationType, project, options, getOperatingSystem(), getSelectedFeatures(), outputHandler, this); } + private GspLayoutImpl getGspLayoutImpl() { + return gspLayoutImpl == null ? GspLayoutImpl.DEFAULT_OPTION : gspLayoutImpl; + } + private JdkVersion getJdkVersion() { if (javaVersion == null) { return JdkVersion.DEFAULT_OPTION; diff --git a/grails-forge/grails-forge-cli/src/main/java/org/grails/forge/cli/command/GspLayoutImplCandidates.java b/grails-forge/grails-forge-cli/src/main/java/org/grails/forge/cli/command/GspLayoutImplCandidates.java new file mode 100644 index 00000000000..f8bd3d75075 --- /dev/null +++ b/grails-forge/grails-forge-cli/src/main/java/org/grails/forge/cli/command/GspLayoutImplCandidates.java @@ -0,0 +1,31 @@ +/* + * 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.cli.command; + +import org.grails.forge.options.GspLayoutImpl; + +import java.util.ArrayList; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class GspLayoutImplCandidates extends ArrayList { + public GspLayoutImplCandidates() { + super(Stream.of(GspLayoutImpl.values()).map(GspLayoutImpl::getName).collect(Collectors.toList())); + } +} diff --git a/grails-forge/grails-forge-cli/src/main/java/org/grails/forge/cli/command/GspLayoutImplConverter.java b/grails-forge/grails-forge-cli/src/main/java/org/grails/forge/cli/command/GspLayoutImplConverter.java new file mode 100644 index 00000000000..8e3efad2727 --- /dev/null +++ b/grails-forge/grails-forge-cli/src/main/java/org/grails/forge/cli/command/GspLayoutImplConverter.java @@ -0,0 +1,41 @@ +/* + * 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.cli.command; + +import io.micronaut.core.annotation.Introspected; +import org.grails.forge.options.GspLayoutImpl; +import picocli.CommandLine; + +@Introspected +public class GspLayoutImplConverter implements CommandLine.ITypeConverter { + + @Override + public GspLayoutImpl convert(String value) throws Exception { + if (value == null) { + return GspLayoutImpl.DEFAULT_OPTION; + } else { + for (GspLayoutImpl impl : GspLayoutImpl.values()) { + if (value.equalsIgnoreCase(impl.getName()) || value.equalsIgnoreCase(impl.name())) { + return impl; + } + } + } + throw new CommandLine.TypeConversionException("Invalid GSP layout implementation selection: " + value); + } +} diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/application/ContextFactory.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/application/ContextFactory.java index f499008902b..27fc48ca266 100644 --- a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/application/ContextFactory.java +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/application/ContextFactory.java @@ -65,7 +65,8 @@ public FeatureContext createFeatureContext(AvailableFeatures availableFeatures, Options newOptions = options .withDevelopmentReloading(determineDevelopmentReloading(options.getDevelopmentReloading())) .withGormImpl(determineGormImpl(options.getGormImpl())) - .withServletImpl(determineServletImpl(options.getServletImpl())); + .withServletImpl(determineServletImpl(options.getServletImpl())) + .withGspLayoutImpl(determineGspLayoutImpl(options.getGspLayoutImpl())); availableFeatures.getAllFeatures() .filter(f -> f instanceof DefaultFeature) @@ -120,6 +121,13 @@ GormImpl determineGormImpl(GormImpl gormImpl) { return gormImpl; } + GspLayoutImpl determineGspLayoutImpl(GspLayoutImpl gspLayoutImpl) { + if (gspLayoutImpl == null) { + gspLayoutImpl = GspLayoutImpl.DEFAULT_OPTION; + } + return gspLayoutImpl; + } + ServletImpl determineServletImpl(ServletImpl servletImpl) { if (servletImpl == null) { servletImpl = ServletImpl.DEFAULT_OPTION; diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/sitemesh3/Sitemesh3.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/sitemesh3/Sitemesh3.java index 7d5fcddb2fd..ab5301439ed 100644 --- a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/sitemesh3/Sitemesh3.java +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/sitemesh3/Sitemesh3.java @@ -22,20 +22,20 @@ 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.DefaultFeature; import org.grails.forge.feature.Feature; import org.grails.forge.feature.view.GspLayout; +import org.grails.forge.options.GspLayoutImpl; import org.grails.forge.options.Options; import java.util.Set; /** * 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. + * Applied automatically to web applications unless the {@link GspLayoutImpl} + * option selects the legacy SiteMesh 2 based {@code grails-layout} decorator. */ @Singleton -public class Sitemesh3 extends GspLayout implements DefaultFeature { +public class Sitemesh3 extends GspLayout { @Override public String getName() { @@ -44,7 +44,7 @@ public String getName() { @Override public String getTitle() { - return "SiteMesh 3"; + return "GSP SiteMesh 3 Layouts"; } @Override @@ -55,7 +55,7 @@ public String getDescription() { @Override public boolean shouldApply(ApplicationType applicationType, Options options, Set selectedFeatures) { return supports(applicationType) && - selectedFeatures.stream().noneMatch(GspLayout.class::isInstance); + options.getGspLayoutImpl() == GspLayoutImpl.SITEMESH3; } @Override diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GrailsLayout.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GrailsLayout.java index 5bd2b958b9c..52ab66af69d 100644 --- a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GrailsLayout.java +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GrailsLayout.java @@ -19,13 +19,20 @@ package org.grails.forge.feature.view; import jakarta.inject.Singleton; +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.Feature; +import org.grails.forge.options.GspLayoutImpl; +import org.grails.forge.options.Options; + +import java.util.Set; /** * 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. + * applied when the {@link GspLayoutImpl} option selects {@code grails-layout} + * instead of the default SiteMesh 3 decorator. */ @Singleton public class GrailsLayout extends GspLayout { @@ -45,6 +52,12 @@ public String getDescription() { return "Adds support for legacy SiteMesh 2 based GSP layouts (grails-layout) instead of SiteMesh 3"; } + @Override + public boolean shouldApply(ApplicationType applicationType, Options options, Set selectedFeatures) { + return supports(applicationType) && + options.getGspLayoutImpl() == GspLayoutImpl.GRAILS_LAYOUT; + } + @Override public void apply(GeneratorContext generatorContext) { generatorContext.addDependency(Dependency.builder() diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GspLayout.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GspLayout.java index d818518645f..1f45ac85511 100644 --- a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GspLayout.java +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GspLayout.java @@ -20,6 +20,7 @@ import org.grails.forge.application.ApplicationType; import org.grails.forge.feature.Category; +import org.grails.forge.feature.DefaultFeature; import org.grails.forge.feature.OneOfFeature; /** @@ -27,8 +28,12 @@ * ({@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). + * + *

The choice is driven by the {@link org.grails.forge.options.GspLayoutImpl} option + * rather than by selecting a feature, so the members are not visible as standalone + * features. Each member applies based on the selected option (see {@code shouldApply}). */ -public abstract class GspLayout implements OneOfFeature { +public abstract class GspLayout implements OneOfFeature, DefaultFeature { @Override public Class getFeatureClass() { @@ -44,4 +49,9 @@ public boolean supports(ApplicationType applicationType) { public String getCategory() { return Category.VIEW; } + + @Override + public boolean isVisible() { + return false; + } } diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/options/FeatureFilter.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/options/FeatureFilter.java index a0cc294b4e5..d5191209f6c 100644 --- a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/options/FeatureFilter.java +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/options/FeatureFilter.java @@ -29,6 +29,8 @@ public class FeatureFilter { private GormImpl gorm; @JsonProperty("servlet") private ServletImpl servlet; + @JsonProperty("gspLayout") + private GspLayoutImpl gspLayout; @JsonProperty("javaVersion") private JdkVersion javaVersion; @@ -56,6 +58,14 @@ public void setServlet(ServletImpl servlet) { this.servlet = servlet; } + public GspLayoutImpl getGspLayout() { + return gspLayout; + } + + public void setGspLayout(GspLayoutImpl gspLayout) { + this.gspLayout = gspLayout; + } + public JdkVersion getJavaVersion() { return javaVersion; } diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/options/GspLayoutImpl.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/options/GspLayoutImpl.java new file mode 100644 index 00000000000..667e0692f42 --- /dev/null +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/options/GspLayoutImpl.java @@ -0,0 +1,54 @@ +/* + * 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.options; + +import io.micronaut.core.annotation.NonNull; + +/** + * The GSP layout decorator implementation. SiteMesh 3 ({@code grails-sitemesh3}) + * is the default; the legacy SiteMesh 2 based {@code grails-layout} plugin is the + * opt-in alternative. The two are mutually exclusive, so the choice is modelled as + * a single option rather than competing features. + * + * @since 8.0.0 + */ +public enum GspLayoutImpl { + + SITEMESH3("sitemesh3", "SiteMesh 3"), + GRAILS_LAYOUT("grails-layout", "SiteMesh 2"); + + public static final GspLayoutImpl DEFAULT_OPTION = SITEMESH3; + private final String featureName; + private final String label; + + GspLayoutImpl(String featureName, String label) { + this.featureName = featureName; + this.label = label; + } + + @NonNull + public String getName() { + return featureName; + } + + @NonNull + public String getLabel() { + return label; + } +} diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/options/Options.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/options/Options.java index b3ab6a01bb3..c78c4c8b0bb 100644 --- a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/options/Options.java +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/options/Options.java @@ -34,6 +34,7 @@ public class Options implements ConvertibleValues { private final BuildTool buildTool; private final GormImpl gormImpl; private final ServletImpl servletImpl; + private final GspLayoutImpl gspLayoutImpl; private final JdkVersion javaVersion; private final ConvertibleValuesMap additionalOptions; @@ -44,10 +45,22 @@ public Options(DevelopmentReloading reloading, OperatingSystem operatingSystem, Map additionalOptions) { + this(reloading, gormImpl, servletImpl, GspLayoutImpl.DEFAULT_OPTION, javaVersion, operatingSystem, additionalOptions); + } + + public Options(DevelopmentReloading reloading, + GormImpl gormImpl, + ServletImpl servletImpl, + GspLayoutImpl gspLayoutImpl, + JdkVersion javaVersion, + OperatingSystem operatingSystem, + Map additionalOptions) { + this.reloading = reloading; this.buildTool = BuildTool.DEFAULT_OPTION; this.gormImpl = gormImpl; this.servletImpl = servletImpl; + this.gspLayoutImpl = gspLayoutImpl; this.javaVersion = javaVersion; this.operatingSystem = operatingSystem; this.additionalOptions = new ConvertibleValuesMap<>(additionalOptions); @@ -117,6 +130,10 @@ public ServletImpl getServletImpl() { return servletImpl; } + public GspLayoutImpl getGspLayoutImpl() { + return gspLayoutImpl; + } + @Override public Set names() { return additionalOptions.names(); @@ -137,22 +154,26 @@ public JdkVersion getJavaVersion() { } public Options withOperatingSystem(OperatingSystem operatingSystem) { - return new Options(reloading, gormImpl, servletImpl, javaVersion, operatingSystem, additionalOptions.asMap()); + return new Options(reloading, gormImpl, servletImpl, gspLayoutImpl, javaVersion, operatingSystem, additionalOptions.asMap()); } public Options withDevelopmentReloading(DevelopmentReloading reloading) { - return new Options(reloading, gormImpl, servletImpl, javaVersion, operatingSystem, additionalOptions.asMap()); + return new Options(reloading, gormImpl, servletImpl, gspLayoutImpl, javaVersion, operatingSystem, additionalOptions.asMap()); } public Options withGormImpl(GormImpl gormImpl) { - return new Options(reloading, gormImpl, servletImpl, javaVersion, operatingSystem, additionalOptions.asMap()); + return new Options(reloading, gormImpl, servletImpl, gspLayoutImpl, javaVersion, operatingSystem, additionalOptions.asMap()); } public Options withServletImpl(ServletImpl servletImpl) { - return new Options(reloading, gormImpl, servletImpl, javaVersion, operatingSystem, additionalOptions.asMap()); + return new Options(reloading, gormImpl, servletImpl, gspLayoutImpl, javaVersion, operatingSystem, additionalOptions.asMap()); + } + + public Options withGspLayoutImpl(GspLayoutImpl gspLayoutImpl) { + return new Options(reloading, gormImpl, servletImpl, gspLayoutImpl, javaVersion, operatingSystem, additionalOptions.asMap()); } public Options withJavaVersion(JdkVersion javaVersion) { - return new Options(reloading, gormImpl, servletImpl, javaVersion, operatingSystem, additionalOptions.asMap()); + return new Options(reloading, gormImpl, servletImpl, gspLayoutImpl, javaVersion, operatingSystem, additionalOptions.asMap()); } } diff --git a/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/BuildBuilder.groovy b/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/BuildBuilder.groovy index 3a2cfa91fd5..a7838228a32 100644 --- a/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/BuildBuilder.groovy +++ b/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/BuildBuilder.groovy @@ -48,6 +48,7 @@ class BuildBuilder implements ProjectFixture, ContextFixture { private JdkVersion jdkVersion private GormImpl gormImpl private ServletImpl servletImpl + private GspLayoutImpl gspLayoutImpl private OperatingSystem operatingSystem private Project project private ApplicationContext ctx @@ -57,6 +58,7 @@ class BuildBuilder implements ProjectFixture, ContextFixture { this.ctx = ctx this.gormImpl = GormImpl.DEFAULT_OPTION this.servletImpl = ServletImpl.DEFAULT_OPTION + this.gspLayoutImpl = GspLayoutImpl.DEFAULT_OPTION this.operatingSystem = OperatingSystem.DEFAULT } @@ -98,6 +100,11 @@ class BuildBuilder implements ProjectFixture, ContextFixture { this } + BuildBuilder gspLayoutImpl(GspLayoutImpl gspLayoutImpl) { + this.gspLayoutImpl = gspLayoutImpl + this + } + BuildBuilder project(Project project) { this.project = project this @@ -111,6 +118,7 @@ class BuildBuilder implements ProjectFixture, ContextFixture { JdkVersion jdkVersion = this.jdkVersion ?: JdkVersion.DEFAULT_OPTION final Options options = new Options(reloading, gormImpl, servletImpl, jdkVersion, operatingSystem) + .withGspLayoutImpl(gspLayoutImpl ?: GspLayoutImpl.DEFAULT_OPTION) Features features = getFeatures(featureNames, options, type) GradleBuild build = gradleBuild(options, features, project, type) diff --git a/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/view/GspLayoutSpec.groovy b/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/view/GspLayoutSpec.groovy index 3ddadd80582..b947f56ddd2 100644 --- a/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/view/GspLayoutSpec.groovy +++ b/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/view/GspLayoutSpec.groovy @@ -22,6 +22,7 @@ package org.grails.forge.feature.view import org.grails.forge.ApplicationContextSpec import org.grails.forge.BuildBuilder import org.grails.forge.application.ApplicationType +import org.grails.forge.options.GspLayoutImpl import spock.lang.Unroll class GspLayoutSpec extends ApplicationContextSpec { @@ -30,7 +31,7 @@ class GspLayoutSpec extends ApplicationContextSpec { void "web #applicationType apps use SiteMesh 3 (grails-sitemesh3) by default"() { when: final String build = new BuildBuilder(beanContext) - .features(["gsp"]) + .features(['gsp']) .applicationType(applicationType) .render() @@ -38,35 +39,37 @@ class GspLayoutSpec extends ApplicationContextSpec { build.contains('implementation "org.apache.grails:grails-sitemesh3"') !build.contains('implementation "org.apache.grails:grails-layout"') - and: "a SNAPSHOT build exposes the SiteMesh 3 snapshot repo so org.sitemesh:*-SNAPSHOT resolves" - build.contains("https://central.sonatype.com/repository/maven-snapshots") - build.contains("includeVersionByRegex('org[.]sitemesh.*'") - where: applicationType << [ApplicationType.WEB, ApplicationType.WEB_PLUGIN] } - void "selecting grails-layout replaces the default SiteMesh 3 decorator"() { + @Unroll + void "the grails-layout option replaces the default SiteMesh 3 decorator for #applicationType"() { when: final String build = new BuildBuilder(beanContext) - .features(["gsp", "grails-layout"]) - .applicationType(ApplicationType.WEB) + .features(['gsp']) + .gspLayoutImpl(GspLayoutImpl.GRAILS_LAYOUT) + .applicationType(applicationType) .render() then: build.contains('implementation "org.apache.grails:grails-layout"') !build.contains('implementation "org.apache.grails:grails-sitemesh3"') + + where: + applicationType << [ApplicationType.WEB, ApplicationType.WEB_PLUGIN] } - void "sitemesh3 and grails-layout cannot both be selected"() { + void "the sitemesh3 option keeps the default SiteMesh 3 decorator"() { when: - new BuildBuilder(beanContext) - .features(["gsp", "sitemesh3", "grails-layout"]) + final String build = new BuildBuilder(beanContext) + .features(['gsp']) + .gspLayoutImpl(GspLayoutImpl.SITEMESH3) .applicationType(ApplicationType.WEB) .render() then: - IllegalArgumentException e = thrown() - e.message.contains("There can only be one of the following features selected") + build.contains('implementation "org.apache.grails:grails-sitemesh3"') + !build.contains('implementation "org.apache.grails:grails-layout"') } } From 021059d03e411e7e19cc6465ec1fdb0505db607b Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Fri, 5 Jun 2026 13:12:20 -0700 Subject: [PATCH 04/25] Run CI test apps against SiteMesh 3 by default 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. --- .github/workflows/gradle.yml | 5 +++++ .github/workflows/groovy-joint-workflow.yml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index b5e85454e5b..d380c490259 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -24,6 +24,11 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} +# Exercise SiteMesh 3 by default in the test apps. The example/suite builds read this +# flag to choose grails-sitemesh3 over the legacy grails-layout; unset it (or set it to +# anything other than 'true') to fall back to SiteMesh 2. +env: + SITEMESH3_TESTING_ENABLED: 'true' jobs: validateDependencies: name: 'Validate Dependency Versions' diff --git a/.github/workflows/groovy-joint-workflow.yml b/.github/workflows/groovy-joint-workflow.yml index 1b4f0cc430a..18e10766488 100644 --- a/.github/workflows/groovy-joint-workflow.yml +++ b/.github/workflows/groovy-joint-workflow.yml @@ -28,6 +28,11 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} permissions: contents: read +# Exercise SiteMesh 3 by default in the test apps. The example/suite builds read this +# flag to choose grails-sitemesh3 over the legacy grails-layout; unset it (or set it to +# anything other than 'true') to fall back to SiteMesh 2. +env: + SITEMESH3_TESTING_ENABLED: 'true' jobs: build_groovy: if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }} From 920c12c9019d879a11f6f42e2066ea9aec5cf72e Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Fri, 5 Jun 2026 22:06:15 -0700 Subject: [PATCH 05/25] Fix empty SiteMesh 3 output for undecorated GSP responses In the filterless SiteMesh 3 pipeline the GSP capture taglib writes the full / 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 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 . 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). --- .../plugins/sitemesh3/CaptureAwareContentProcessor.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/CaptureAwareContentProcessor.java b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/CaptureAwareContentProcessor.java index 9dc2fada835..9ab8864c509 100644 --- a/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/CaptureAwareContentProcessor.java +++ b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/CaptureAwareContentProcessor.java @@ -80,6 +80,14 @@ public Content build(CharBuffer data, SiteMeshContext context) throws IOExceptio } if (captured != null && captured.isUsed()) { + // The capture taglib also writes the full / markup to + // the response buffer, so `data` is the complete original page. + // Attach it as the page's rendered data so that, when no decorator + // is selected, SiteMeshView writes the original page back instead + // of the (empty) reconstructed-from-properties data chunk. Head/body + // child properties are still materialized for any decorator that IS + // selected, so meta-layout pages are unaffected. + captured.setRenderedContent(data); return captured; } return fallback.build(data, context); From 81d9147c89b1bb544cfad923150cf2396b7f8e0f Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Sat, 6 Jun 2026 13:58:14 -0700 Subject: [PATCH 06/25] Fix SiteMesh 3 applyLayout for embedded/nested field rendering The grails-fields plugin renders embedded objects by wrapping the sub-fields in . 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 rendered empty and / pageProperty(name:'type') had no values. The result was an empty
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 works, applies each params entry as a page property for , 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. --- .../web/taglib/RenderSitemeshTagLib.groovy | 57 +++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/web/taglib/RenderSitemeshTagLib.groovy b/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/web/taglib/RenderSitemeshTagLib.groovy index 0b5f1549d2b..e5e6d452b93 100644 --- a/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/web/taglib/RenderSitemeshTagLib.groovy +++ b/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/web/taglib/RenderSitemeshTagLib.groovy @@ -18,8 +18,6 @@ */ package org.grails.plugins.web.taglib -import java.nio.CharBuffer - import org.sitemesh.DecoratorSelector import org.sitemesh.SiteMeshContext import org.sitemesh.content.Content @@ -35,9 +33,12 @@ import org.springframework.web.servlet.ViewResolver import grails.artefact.TagLibrary import grails.gsp.TagLib +import org.grails.buffer.FastStringWriter +import org.grails.buffer.StreamCharBuffer import org.grails.encoder.CodecLookup import org.grails.encoder.Encoder import org.grails.plugins.sitemesh3.GrailsSiteMeshViewContext +import org.grails.plugins.sitemesh3.Sitemesh3CapturedPage import org.grails.web.util.WebUtils /** @@ -76,15 +77,52 @@ class RenderSitemeshTagLib implements TagLibrary { // causing "request is not active anymore" errors. Closure applyLayout = { Map attrs, body -> String savedAttribute = request.getAttribute(WebUtils.LAYOUT_ATTRIBUTE) + // Save the request-scoped captured page (the one being decorated by the + // outer SiteMesh render) and push a fresh one for the duration of the + // body() render. The body of is plain markup (e.g. the + // grails-fields embedded fieldset content) with no taglibs, + // so its content must be wired into the page explicitly rather than + // relying on capture. Restoring the outer page afterwards prevents the + // body fragment from clobbering the outer page's body/title/properties. + Object savedCapturedPage = request.getAttribute(Sitemesh3CapturedPage.REQUEST_ATTRIBUTE) GrailsSiteMeshViewContext context = new GrailsSiteMeshViewContext( 'text/html', request, response, servletContext, contentProcessor, new ResponseMetaData(), false, viewResolver, request.getLocale()) try { - Content content = contentProcessor.build(CharBuffer.wrap(body()), context) + Sitemesh3CapturedPage bodyPage = new Sitemesh3CapturedPage() + request.setAttribute(Sitemesh3CapturedPage.REQUEST_ATTRIBUTE, bodyPage) + + Object renderedBody = body() + StreamCharBuffer bodyBuffer + if (renderedBody instanceof StreamCharBuffer) { + bodyBuffer = (StreamCharBuffer) renderedBody + } else { + FastStringWriter stringWriter = new FastStringWriter() + stringWriter.print(renderedBody) + bodyBuffer = stringWriter.buffer + } + bodyBuffer.setPreferSubChunkWhenWritingToOtherBuffer(true) + bodyPage.setBodyBuffer(bodyBuffer) + + // Expose entries as page properties so + // the layout can read them via . This + // mirrors SiteMesh 2's GrailsLayoutTagLib, which calls + // page.addProperty(k, v) for each params entry. + Map params = attrs.params instanceof Map ? (Map) attrs.params : null + if (params) { + params.each { k, v -> + if (k != null && v != null) { + bodyPage.addProperty(k.toString(), v.toString()) + } + } + } + + Content content = bodyPage if (attrs.name) { request.setAttribute(WebUtils.LAYOUT_ATTRIBUTE, attrs.name) } + boolean decorated = false String[] decoratorPaths = decoratorSelector.selectDecoratorPaths(content, context) for (String decoratorPath : decoratorPaths) { Content next = context.decorate(decoratorPath, content) @@ -92,11 +130,22 @@ class RenderSitemeshTagLib implements TagLibrary { break } content = next + decorated = true } - if (content != null) { + if (decorated) { content.getData().writeValueTo(out) + } else { + // No layout was resolved: emit the undecorated body verbatim, + // matching SiteMesh 2's GrailsLayoutTagLib which writes the raw + // content when no decorator is found. + out << bodyBuffer } } finally { + if (savedCapturedPage != null) { + request.setAttribute(Sitemesh3CapturedPage.REQUEST_ATTRIBUTE, savedCapturedPage) + } else { + request.removeAttribute(Sitemesh3CapturedPage.REQUEST_ATTRIBUTE) + } if (savedAttribute != null) { request.setAttribute(WebUtils.LAYOUT_ATTRIBUTE, savedAttribute) } else { From 6178aeb3f9d73b0c8f2eeebb9289915f0c521c54 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Sat, 6 Jun 2026 13:58:14 -0700 Subject: [PATCH 07/25] Drop redundant grails-layout from starter-web test apps 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. --- grails-test-examples/database-cleanup/build.gradle | 1 - grails-test-examples/scaffolding-fields/build.gradle | 1 - grails-test-examples/test-phases/build.gradle | 1 - 3 files changed, 3 deletions(-) diff --git a/grails-test-examples/database-cleanup/build.gradle b/grails-test-examples/database-cleanup/build.gradle index 2b5f1119d4d..74528c2e70d 100644 --- a/grails-test-examples/database-cleanup/build.gradle +++ b/grails-test-examples/database-cleanup/build.gradle @@ -34,7 +34,6 @@ dependencies { implementation 'org.apache.grails:grails-dependencies-starter-web' implementation 'com.h2database:h2' - implementation 'org.apache.grails:grails-layout' implementation 'org.apache.grails:grails-data-hibernate5' runtimeOnly 'org.hibernate:hibernate-ehcache', { exclude group: 'org.hibernate', module: 'hibernate-core' diff --git a/grails-test-examples/scaffolding-fields/build.gradle b/grails-test-examples/scaffolding-fields/build.gradle index 3afbfc6fcc1..47ac9558c96 100644 --- a/grails-test-examples/scaffolding-fields/build.gradle +++ b/grails-test-examples/scaffolding-fields/build.gradle @@ -35,7 +35,6 @@ dependencies { implementation 'org.apache.grails:grails-dependencies-starter-web' implementation 'com.h2database:h2' - implementation 'org.apache.grails:grails-layout' implementation 'org.apache.grails:grails-data-hibernate5' implementation 'org.apache.grails:grails-scaffolding' implementation 'org.apache.grails:grails-fields' diff --git a/grails-test-examples/test-phases/build.gradle b/grails-test-examples/test-phases/build.gradle index e0c7c50804b..dd64376e02d 100644 --- a/grails-test-examples/test-phases/build.gradle +++ b/grails-test-examples/test-phases/build.gradle @@ -39,7 +39,6 @@ dependencies { implementation 'org.apache.grails:grails-dependencies-starter-web' implementation 'com.h2database:h2' - implementation 'org.apache.grails:grails-layout' implementation 'org.apache.grails:grails-data-hibernate5' testImplementation 'org.apache.grails:grails-testing-support-web' From 8a1fbcbcd1e643c302d3f6c465fc69c9c6ab1d25 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Sat, 6 Jun 2026 15:29:47 -0700 Subject: [PATCH 08/25] Don't apply a SiteMesh 3 layout to template/partial renders 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. --- .../plugins/sitemesh3/Sitemesh3LayoutFinder.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3LayoutFinder.java b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3LayoutFinder.java index 0c043a51961..3e07fbbca75 100644 --- a/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3LayoutFinder.java +++ b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3LayoutFinder.java @@ -106,6 +106,19 @@ public String[] selectDecoratorPaths(Content content, SiteMeshContext context) { } HttpServletRequest request = ((WebAppContext) context).getRequest(); + // A controller's "render template:" (or "render view:" with renderView + // disabled) renders the GSP directly and sets renderView=false. Such + // partial/template renders must not be decorated with a layout, matching + // SiteMesh 2 behaviour (see GrailsLayoutSelector). When an explicit + // layout was requested (layout: 'x') the LAYOUT_ATTRIBUTE is set and the + // checks below still apply it. + if (request.getAttribute(WebUtils.LAYOUT_ATTRIBUTE) == null) { + GrailsWebRequest webRequest = GrailsWebRequest.lookup(request); + if (webRequest != null && !webRequest.isRenderView()) { + return new String[0]; + } + } + Object layoutAttribute = request.getAttribute(WebUtils.LAYOUT_ATTRIBUTE); String layoutName = layoutAttribute == null ? null : layoutAttribute.toString(); From 8b5db5286b184cb5496ad572f05f10272f545d44 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Tue, 9 Jun 2026 19:51:54 -0500 Subject: [PATCH 09/25] Default test apps to SiteMesh 3, opt into SiteMesh 2 via flag Invert the layout toggle so the default build resolves grails-sitemesh3; set SITEMESH2_TESTING_ENABLED=true to test the legacy grails-layout instead. Drop the now-redundant SITEMESH3_TESTING_ENABLED env from the CI workflows since SiteMesh 3 is the default. SiteMesh 2 keeps coverage through the hardcoded gsp-layout/jetty/scaffolding anchors. --- .github/workflows/gradle.yml | 5 ----- .github/workflows/groovy-joint-workflow.yml | 5 ----- grails-dependencies/starter-web/build.gradle | 2 +- grails-test-examples/app1/build.gradle | 6 +++--- grails-test-examples/app2/build.gradle | 6 +++--- grails-test-examples/app3/build.gradle | 6 +++--- grails-test-examples/async-events-pubsub-demo/build.gradle | 6 +++--- grails-test-examples/cache/build.gradle | 6 +++--- grails-test-examples/config-report/build.gradle | 6 +++--- grails-test-examples/datasources/build.gradle | 6 +++--- grails-test-examples/demo33/build.gradle | 6 +++--- grails-test-examples/exploded/build.gradle | 6 +++--- grails-test-examples/external-configuration/build.gradle | 6 +++--- grails-test-examples/geb-context-path/build.gradle | 6 +++--- grails-test-examples/geb-gebconfig/build.gradle | 6 +++--- grails-test-examples/geb/build.gradle | 6 +++--- grails-test-examples/gorm/build.gradle | 6 +++--- .../hibernate5/grails-database-per-tenant/build.gradle | 6 +++--- .../hibernate5/grails-hibernate/build.gradle | 6 +++--- .../grails-partitioned-multi-tenancy/build.gradle | 6 +++--- .../hibernate5/grails-schema-per-tenant/build.gradle | 6 +++--- grails-test-examples/hibernate5/issue450/build.gradle | 6 +++--- grails-test-examples/hyphenated/build.gradle | 6 +++--- grails-test-examples/issue-11102/build.gradle | 6 +++--- grails-test-examples/issue-698-domain-save-npe/build.gradle | 6 +++--- grails-test-examples/issue-views-182/build.gradle | 6 +++--- grails-test-examples/mongodb/base/build.gradle | 6 +++--- .../mongodb/database-per-tenant/build.gradle | 6 +++--- grails-test-examples/mongodb/gson-templates/build.gradle | 6 +++--- grails-test-examples/mongodb/hibernate5/build.gradle | 6 +++--- grails-test-examples/namespaces/build.gradle | 6 +++--- grails-test-examples/plugins/exploded/build.gradle | 6 +++--- grails-test-examples/plugins/issue11005/build.gradle | 6 +++--- grails-test-examples/plugins/loadafter/build.gradle | 6 +++--- grails-test-examples/plugins/loadfirst/build.gradle | 6 +++--- grails-test-examples/plugins/loadsecond/build.gradle | 6 +++--- grails-test-examples/views-functional-tests/build.gradle | 6 +++--- grails-test-suite-uber/build.gradle | 6 +++--- 38 files changed, 106 insertions(+), 116 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index d380c490259..b5e85454e5b 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -24,11 +24,6 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} -# Exercise SiteMesh 3 by default in the test apps. The example/suite builds read this -# flag to choose grails-sitemesh3 over the legacy grails-layout; unset it (or set it to -# anything other than 'true') to fall back to SiteMesh 2. -env: - SITEMESH3_TESTING_ENABLED: 'true' jobs: validateDependencies: name: 'Validate Dependency Versions' diff --git a/.github/workflows/groovy-joint-workflow.yml b/.github/workflows/groovy-joint-workflow.yml index 18e10766488..1b4f0cc430a 100644 --- a/.github/workflows/groovy-joint-workflow.yml +++ b/.github/workflows/groovy-joint-workflow.yml @@ -28,11 +28,6 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} permissions: contents: read -# Exercise SiteMesh 3 by default in the test apps. The example/suite builds read this -# flag to choose grails-sitemesh3 over the legacy grails-layout; unset it (or set it to -# anything other than 'true') to fall back to SiteMesh 2. -env: - SITEMESH3_TESTING_ENABLED: 'true' jobs: build_groovy: if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }} diff --git a/grails-dependencies/starter-web/build.gradle b/grails-dependencies/starter-web/build.gradle index 46f99f7d155..df50d25e9e1 100644 --- a/grails-dependencies/starter-web/build.gradle +++ b/grails-dependencies/starter-web/build.gradle @@ -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', diff --git a/grails-test-examples/app1/build.gradle b/grails-test-examples/app1/build.gradle index 7876d7281a6..bef37389f4e 100644 --- a/grails-test-examples/app1/build.gradle +++ b/grails-test-examples/app1/build.gradle @@ -39,11 +39,11 @@ dependencies { implementation 'org.apache.grails:grails-dependencies-starter-web' implementation 'com.h2database:h2' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-data-hibernate5' diff --git a/grails-test-examples/app2/build.gradle b/grails-test-examples/app2/build.gradle index d0cfa927b37..50e881e677f 100644 --- a/grails-test-examples/app2/build.gradle +++ b/grails-test-examples/app2/build.gradle @@ -39,11 +39,11 @@ dependencies { implementation 'org.apache.grails:grails-dependencies-starter-web' implementation 'com.h2database:h2' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-data-hibernate5' diff --git a/grails-test-examples/app3/build.gradle b/grails-test-examples/app3/build.gradle index 7a9bd1ac473..7ccf0f9b905 100644 --- a/grails-test-examples/app3/build.gradle +++ b/grails-test-examples/app3/build.gradle @@ -38,11 +38,11 @@ dependencies { implementation 'org.apache.grails:grails-dependencies-starter-web' implementation 'com.h2database:h2' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-data-hibernate5' diff --git a/grails-test-examples/async-events-pubsub-demo/build.gradle b/grails-test-examples/async-events-pubsub-demo/build.gradle index 81db50e646e..27d6a126a3d 100644 --- a/grails-test-examples/async-events-pubsub-demo/build.gradle +++ b/grails-test-examples/async-events-pubsub-demo/build.gradle @@ -47,11 +47,11 @@ dependencies { implementation 'org.apache.grails:grails-services' implementation 'org.apache.grails:grails-url-mappings' implementation 'org.apache.grails:grails-web-boot' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-views-gson' diff --git a/grails-test-examples/cache/build.gradle b/grails-test-examples/cache/build.gradle index 1a6c9a63cf8..a51112a1217 100644 --- a/grails-test-examples/cache/build.gradle +++ b/grails-test-examples/cache/build.gradle @@ -49,11 +49,11 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-logging' implementation 'org.springframework.boot:spring-boot-starter-tomcat' implementation 'org.springframework.boot:spring-boot-starter-validation' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } console 'org.apache.grails:grails-console' diff --git a/grails-test-examples/config-report/build.gradle b/grails-test-examples/config-report/build.gradle index b141104ee46..d1269136d83 100644 --- a/grails-test-examples/config-report/build.gradle +++ b/grails-test-examples/config-report/build.gradle @@ -38,11 +38,11 @@ dependencies { implementation 'org.apache.grails:grails-services' implementation 'org.apache.grails:grails-url-mappings' implementation 'org.apache.grails:grails-web-boot' - if (System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-data-hibernate5' implementation 'org.springframework.boot:spring-boot-autoconfigure' diff --git a/grails-test-examples/datasources/build.gradle b/grails-test-examples/datasources/build.gradle index b8c928826ed..de5c5aae51d 100644 --- a/grails-test-examples/datasources/build.gradle +++ b/grails-test-examples/datasources/build.gradle @@ -33,11 +33,11 @@ dependencies { implementation 'org.apache.grails:grails-dependencies-starter-web' implementation 'com.h2database:h2' implementation 'org.apache.grails:grails-web-boot' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-data-hibernate5' diff --git a/grails-test-examples/demo33/build.gradle b/grails-test-examples/demo33/build.gradle index a762ffe5064..5635b15151f 100644 --- a/grails-test-examples/demo33/build.gradle +++ b/grails-test-examples/demo33/build.gradle @@ -45,11 +45,11 @@ dependencies { implementation 'org.apache.grails:grails-interceptors' implementation 'org.apache.grails:grails-web-boot' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-data-hibernate5' implementation 'org.apache.grails:grails-views-gson' diff --git a/grails-test-examples/exploded/build.gradle b/grails-test-examples/exploded/build.gradle index 58113281ce0..0bd07434391 100644 --- a/grails-test-examples/exploded/build.gradle +++ b/grails-test-examples/exploded/build.gradle @@ -37,10 +37,10 @@ dependencies { implementation 'org.apache.grails:grails-dependencies-starter-web' implementation 'com.h2database:h2' - if (System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' - } else { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' + } else { + implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-data-hibernate5' diff --git a/grails-test-examples/external-configuration/build.gradle b/grails-test-examples/external-configuration/build.gradle index e1d294a0cac..dabc068b42f 100644 --- a/grails-test-examples/external-configuration/build.gradle +++ b/grails-test-examples/external-configuration/build.gradle @@ -38,11 +38,11 @@ dependencies { implementation 'org.apache.grails:grails-services' implementation 'org.apache.grails:grails-url-mappings' implementation 'org.apache.grails:grails-web-boot' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-data-hibernate5' implementation 'org.apache.grails:grails-scaffolding' diff --git a/grails-test-examples/geb-context-path/build.gradle b/grails-test-examples/geb-context-path/build.gradle index 14205713c24..8be98a7f37d 100644 --- a/grails-test-examples/geb-context-path/build.gradle +++ b/grails-test-examples/geb-context-path/build.gradle @@ -42,11 +42,11 @@ dependencies { implementation 'org.apache.grails:grails-url-mappings' implementation 'org.apache.grails:grails-web-boot' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-data-hibernate5' implementation 'org.springframework.boot:spring-boot-autoconfigure' diff --git a/grails-test-examples/geb-gebconfig/build.gradle b/grails-test-examples/geb-gebconfig/build.gradle index 8fbe476fee3..9f9c58d1913 100644 --- a/grails-test-examples/geb-gebconfig/build.gradle +++ b/grails-test-examples/geb-gebconfig/build.gradle @@ -43,11 +43,11 @@ dependencies { implementation 'org.apache.grails:grails-url-mappings' implementation 'org.apache.grails:grails-web-boot' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-data-hibernate5' implementation 'org.apache.grails:grails-scaffolding' diff --git a/grails-test-examples/geb/build.gradle b/grails-test-examples/geb/build.gradle index 9c87bb95993..5bbf216b708 100644 --- a/grails-test-examples/geb/build.gradle +++ b/grails-test-examples/geb/build.gradle @@ -42,11 +42,11 @@ dependencies { implementation 'org.apache.grails:grails-url-mappings' implementation 'org.apache.grails:grails-web-boot' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-data-hibernate5' implementation 'org.apache.grails:grails-scaffolding' diff --git a/grails-test-examples/gorm/build.gradle b/grails-test-examples/gorm/build.gradle index 811c2ba9145..bd74d355110 100644 --- a/grails-test-examples/gorm/build.gradle +++ b/grails-test-examples/gorm/build.gradle @@ -34,11 +34,11 @@ dependencies { implementation 'org.apache.grails:grails-dependencies-starter-web' implementation 'com.h2database:h2' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-data-hibernate5' diff --git a/grails-test-examples/hibernate5/grails-database-per-tenant/build.gradle b/grails-test-examples/hibernate5/grails-database-per-tenant/build.gradle index 85e53a201fd..83849045d1f 100644 --- a/grails-test-examples/hibernate5/grails-database-per-tenant/build.gradle +++ b/grails-test-examples/hibernate5/grails-database-per-tenant/build.gradle @@ -36,11 +36,11 @@ dependencies { implementation 'org.apache.grails:grails-core' implementation 'org.apache.grails:grails-rest-transforms' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } testAndDevelopmentOnly platform(project(':grails-bom')) diff --git a/grails-test-examples/hibernate5/grails-hibernate/build.gradle b/grails-test-examples/hibernate5/grails-hibernate/build.gradle index 9233fc1f524..0ee797d042e 100644 --- a/grails-test-examples/hibernate5/grails-hibernate/build.gradle +++ b/grails-test-examples/hibernate5/grails-hibernate/build.gradle @@ -38,11 +38,11 @@ dependencies { implementation 'org.apache.grails:grails-core' implementation 'org.apache.grails:grails-rest-transforms' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } testAndDevelopmentOnly platform(project(':grails-bom')) diff --git a/grails-test-examples/hibernate5/grails-partitioned-multi-tenancy/build.gradle b/grails-test-examples/hibernate5/grails-partitioned-multi-tenancy/build.gradle index 72a86f126fc..ebfdf4035d2 100644 --- a/grails-test-examples/hibernate5/grails-partitioned-multi-tenancy/build.gradle +++ b/grails-test-examples/hibernate5/grails-partitioned-multi-tenancy/build.gradle @@ -36,11 +36,11 @@ dependencies { implementation 'org.apache.grails:grails-core' implementation 'org.apache.grails:grails-rest-transforms' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } testAndDevelopmentOnly platform(project(':grails-bom')) diff --git a/grails-test-examples/hibernate5/grails-schema-per-tenant/build.gradle b/grails-test-examples/hibernate5/grails-schema-per-tenant/build.gradle index 41a4c1a3ff2..3a1040e3f0f 100644 --- a/grails-test-examples/hibernate5/grails-schema-per-tenant/build.gradle +++ b/grails-test-examples/hibernate5/grails-schema-per-tenant/build.gradle @@ -36,11 +36,11 @@ dependencies { implementation 'org.apache.grails:grails-core' implementation 'org.apache.grails:grails-rest-transforms' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } testAndDevelopmentOnly platform(project(':grails-bom')) diff --git a/grails-test-examples/hibernate5/issue450/build.gradle b/grails-test-examples/hibernate5/issue450/build.gradle index 27187b057c6..c88d7a4e2d6 100644 --- a/grails-test-examples/hibernate5/issue450/build.gradle +++ b/grails-test-examples/hibernate5/issue450/build.gradle @@ -36,11 +36,11 @@ dependencies { implementation 'org.apache.grails:grails-core' implementation 'org.apache.grails:grails-rest-transforms' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } testAndDevelopmentOnly platform(project(':grails-bom')) diff --git a/grails-test-examples/hyphenated/build.gradle b/grails-test-examples/hyphenated/build.gradle index 87589bf3c32..6f03f0672fb 100644 --- a/grails-test-examples/hyphenated/build.gradle +++ b/grails-test-examples/hyphenated/build.gradle @@ -38,11 +38,11 @@ dependencies { implementation 'org.apache.grails:grails-dependencies-starter-web' implementation 'com.h2database:h2' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-data-hibernate5' diff --git a/grails-test-examples/issue-11102/build.gradle b/grails-test-examples/issue-11102/build.gradle index e79162954e1..48dcad6e7d8 100644 --- a/grails-test-examples/issue-11102/build.gradle +++ b/grails-test-examples/issue-11102/build.gradle @@ -42,11 +42,11 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-tomcat' implementation 'org.apache.grails:grails-web-boot' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-logging' implementation 'org.apache.grails:grails-rest-transforms' diff --git a/grails-test-examples/issue-698-domain-save-npe/build.gradle b/grails-test-examples/issue-698-domain-save-npe/build.gradle index 4d5c9916c2e..be445a246be 100644 --- a/grails-test-examples/issue-698-domain-save-npe/build.gradle +++ b/grails-test-examples/issue-698-domain-save-npe/build.gradle @@ -32,11 +32,11 @@ dependencies { implementation 'org.apache.grails:grails-dependencies-starter-web' implementation 'com.h2database:h2' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-data-hibernate5' diff --git a/grails-test-examples/issue-views-182/build.gradle b/grails-test-examples/issue-views-182/build.gradle index 07122b9739a..9f2be833e62 100644 --- a/grails-test-examples/issue-views-182/build.gradle +++ b/grails-test-examples/issue-views-182/build.gradle @@ -48,11 +48,11 @@ dependencies { implementation 'org.apache.grails:grails-datasource' implementation 'org.apache.grails:grails-databinding' implementation 'org.apache.grails:grails-web-boot' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-logging' implementation 'org.apache.grails:grails-cache' diff --git a/grails-test-examples/mongodb/base/build.gradle b/grails-test-examples/mongodb/base/build.gradle index 207878339dd..c27d9c15f53 100644 --- a/grails-test-examples/mongodb/base/build.gradle +++ b/grails-test-examples/mongodb/base/build.gradle @@ -36,11 +36,11 @@ dependencies { implementation 'org.apache.grails:grails-core' implementation 'org.apache.grails:grails-rest-transforms' implementation 'org.apache.grails:grails-web-boot' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-gsp' diff --git a/grails-test-examples/mongodb/database-per-tenant/build.gradle b/grails-test-examples/mongodb/database-per-tenant/build.gradle index 69c9379b88f..e7d0cb11a29 100644 --- a/grails-test-examples/mongodb/database-per-tenant/build.gradle +++ b/grails-test-examples/mongodb/database-per-tenant/build.gradle @@ -37,11 +37,11 @@ dependencies { implementation 'org.apache.grails:grails-rest-transforms' implementation 'org.apache.grails:grails-web-boot' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } testAndDevelopmentOnly platform(project(':grails-bom')) diff --git a/grails-test-examples/mongodb/gson-templates/build.gradle b/grails-test-examples/mongodb/gson-templates/build.gradle index 6c568154894..2f49f5d9860 100644 --- a/grails-test-examples/mongodb/gson-templates/build.gradle +++ b/grails-test-examples/mongodb/gson-templates/build.gradle @@ -36,11 +36,11 @@ dependencies { implementation 'org.apache.grails:grails-views-gson' implementation 'org.apache.grails:grails-views-markup' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-databinding' implementation 'org.apache.grails:grails-rest-transforms' diff --git a/grails-test-examples/mongodb/hibernate5/build.gradle b/grails-test-examples/mongodb/hibernate5/build.gradle index b6c543c4ead..e43413ae0c4 100644 --- a/grails-test-examples/mongodb/hibernate5/build.gradle +++ b/grails-test-examples/mongodb/hibernate5/build.gradle @@ -37,11 +37,11 @@ dependencies { implementation 'org.apache.grails:grails-rest-transforms' implementation 'org.apache.grails:grails-web-boot' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-data-hibernate5' diff --git a/grails-test-examples/namespaces/build.gradle b/grails-test-examples/namespaces/build.gradle index 99da2a0e3b6..74c78757814 100644 --- a/grails-test-examples/namespaces/build.gradle +++ b/grails-test-examples/namespaces/build.gradle @@ -38,11 +38,11 @@ dependencies { implementation 'org.apache.grails:grails-dependencies-starter-web' implementation 'com.h2database:h2' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-data-hibernate5' diff --git a/grails-test-examples/plugins/exploded/build.gradle b/grails-test-examples/plugins/exploded/build.gradle index 6977b56426e..1f852a7bc9e 100644 --- a/grails-test-examples/plugins/exploded/build.gradle +++ b/grails-test-examples/plugins/exploded/build.gradle @@ -32,10 +32,10 @@ apply plugin: 'org.apache.grails.gradle.grails-exploded' dependencies { implementation platform(project(':grails-bom')) - if (System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - api 'org.apache.grails:grails-sitemesh3' - } else { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { api 'org.apache.grails:grails-layout' + } else { + api 'org.apache.grails:grails-sitemesh3' } api 'org.apache.grails:grails-dependencies-starter-web' api 'com.h2database:h2' diff --git a/grails-test-examples/plugins/issue11005/build.gradle b/grails-test-examples/plugins/issue11005/build.gradle index 4b08b455f5b..48efe2a3d6e 100644 --- a/grails-test-examples/plugins/issue11005/build.gradle +++ b/grails-test-examples/plugins/issue11005/build.gradle @@ -31,11 +31,11 @@ group = 'com.example.grails.plugins' dependencies { implementation platform(project(':grails-bom')) - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } api 'org.apache.grails:grails-dependencies-starter-web' api 'com.h2database:h2' diff --git a/grails-test-examples/plugins/loadafter/build.gradle b/grails-test-examples/plugins/loadafter/build.gradle index f9ef09f690d..4435c5235ae 100644 --- a/grails-test-examples/plugins/loadafter/build.gradle +++ b/grails-test-examples/plugins/loadafter/build.gradle @@ -31,11 +31,11 @@ apply plugin: 'org.apache.grails.gradle.grails-gsp' dependencies { implementation platform(project(':grails-bom')) - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - api 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + api 'org.apache.grails:grails-layout' } else { - api 'org.apache.grails:grails-layout' + api 'org.apache.grails:grails-sitemesh3' } api 'org.apache.grails:grails-dependencies-starter-web' api 'com.h2database:h2' diff --git a/grails-test-examples/plugins/loadfirst/build.gradle b/grails-test-examples/plugins/loadfirst/build.gradle index ef3ddc7dd2d..7b3a23270ee 100644 --- a/grails-test-examples/plugins/loadfirst/build.gradle +++ b/grails-test-examples/plugins/loadfirst/build.gradle @@ -31,11 +31,11 @@ apply plugin: 'org.apache.grails.gradle.grails-gsp' dependencies { implementation platform(project(':grails-bom')) - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - api 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + api 'org.apache.grails:grails-layout' } else { - api 'org.apache.grails:grails-layout' + api 'org.apache.grails:grails-sitemesh3' } api 'org.apache.grails:grails-dependencies-starter-web' api 'com.h2database:h2' diff --git a/grails-test-examples/plugins/loadsecond/build.gradle b/grails-test-examples/plugins/loadsecond/build.gradle index 52239603e69..11d011c74c9 100644 --- a/grails-test-examples/plugins/loadsecond/build.gradle +++ b/grails-test-examples/plugins/loadsecond/build.gradle @@ -30,11 +30,11 @@ apply plugin: 'org.apache.grails.gradle.grails-gsp' dependencies { implementation platform(project(':grails-bom')) - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - api 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + api 'org.apache.grails:grails-layout' } else { - api 'org.apache.grails:grails-layout' + api 'org.apache.grails:grails-sitemesh3' } api 'org.apache.grails:grails-dependencies-starter-web' api 'com.h2database:h2' diff --git a/grails-test-examples/views-functional-tests/build.gradle b/grails-test-examples/views-functional-tests/build.gradle index 43e92420d06..70ac6f07b4e 100644 --- a/grails-test-examples/views-functional-tests/build.gradle +++ b/grails-test-examples/views-functional-tests/build.gradle @@ -39,11 +39,11 @@ dependencies { implementation 'org.apache.grails:grails-core' implementation 'org.apache.grails:grails-logging' implementation 'org.apache.grails:grails-web-boot' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' } else { - implementation 'org.apache.grails:grails-layout' + implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-databinding' diff --git a/grails-test-suite-uber/build.gradle b/grails-test-suite-uber/build.gradle index 9c9246be46f..4a1fa030a9a 100644 --- a/grails-test-suite-uber/build.gradle +++ b/grails-test-suite-uber/build.gradle @@ -50,11 +50,11 @@ dependencies { testImplementation 'tools.jackson.core:jackson-databind' testImplementation project(':grails-data-hibernate5-core') testImplementation project(':grails-gsp') - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation project(':grails-sitemesh3') + if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation project(':grails-layout') } else { - implementation project(':grails-layout') + implementation project(':grails-sitemesh3') } testImplementation project(':grails-testing-support-datamapping'), { // This is a local project dependency From 692ee12d9fef7ce308a2bd5a9c2efc08cc99f328 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Tue, 9 Jun 2026 20:23:23 -0500 Subject: [PATCH 10/25] Apply gspLayout option in the project generation endpoints The features endpoint already read gspLayout, but the preview, create (zip), GitHub and diff endpoints built Options from explicit query params and ignored it, so selecting SiteMesh 2 had no effect on the generated project. Thread a gspLayout query option through those controllers and their operation interfaces, mirroring servlet, so the generated build resolves grails-layout when gspLayout=GRAILS_LAYOUT and grails-sitemesh3 by default. --- .../forge/api/create/AbstractCreateController.java | 4 +++- .../api/create/github/GitHubCreateController.java | 6 ++++-- .../api/create/github/GitHubCreateOperation.java | 2 ++ .../forge/api/create/github/GitHubCreateService.java | 4 +++- .../forge/api/create/zip/ZipCreateController.java | 12 ++++++++---- .../forge/api/create/zip/ZipCreateOperation.java | 2 ++ .../org/grails/forge/api/diff/DiffController.java | 10 ++++++---- .../org/grails/forge/api/diff/DiffOperations.java | 4 ++++ .../grails/forge/api/preview/PreviewController.java | 6 ++++-- .../grails/forge/api/preview/PreviewOperations.java | 3 +++ 10 files changed, 39 insertions(+), 14 deletions(-) diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/AbstractCreateController.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/AbstractCreateController.java index bf9286a94a7..314aa9c504d 100644 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/AbstractCreateController.java +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/AbstractCreateController.java @@ -73,6 +73,7 @@ public GeneratorContext createProjectGeneratorContext( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, + @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @Nullable @Header(HttpHeaders.USER_AGENT) String userAgent) { Project project; @@ -93,7 +94,8 @@ public GeneratorContext createProjectGeneratorContext( gormImpl == null ? GormImpl.DEFAULT_OPTION : gormImpl, servletImpl == null ? ServletImpl.DEFAULT_OPTION : servletImpl, javaVersion == null ? JdkVersion.DEFAULT_OPTION : javaVersion, - getOperatingSystem(userAgent)), + getOperatingSystem(userAgent)) + .withGspLayoutImpl(gspLayout != null ? gspLayout : GspLayoutImpl.DEFAULT_OPTION), getOperatingSystem(userAgent), features != null ? features : Collections.emptyList(), ConsoleOutput.NOOP diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateController.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateController.java index b217f4ccb0d..b8f74359cc6 100644 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateController.java +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateController.java @@ -42,6 +42,7 @@ import org.grails.forge.options.BuildTool; import org.grails.forge.options.GormImpl; import org.grails.forge.options.JdkVersion; +import org.grails.forge.options.GspLayoutImpl; import org.grails.forge.options.ServletImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -85,7 +86,7 @@ public GitHubCreateController(GitHubCreateService gitHubCreateService, * @return A json containing the generated application details. */ @Override - @Get(uri = "/github/{type}/{name}{?features,gorm,servlet,build,reloading,javaVersion,code,state}", produces = MediaType.APPLICATION_JSON) + @Get(uri = "/github/{type}/{name}{?features,gorm,servlet,gspLayout,build,reloading,javaVersion,code,state}", produces = MediaType.APPLICATION_JSON) @ApiResponses(value = { @ApiResponse( responseCode = "200", @@ -112,6 +113,7 @@ public HttpResponse createApp( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, + @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @Nullable String code, @Nullable String state, @@ -123,7 +125,7 @@ public HttpResponse createApp( return HttpResponse.temporaryRedirect(redirectService.constructOAuthRedirectUrl(requestInfo)); } else { GitHubRepository repository = gitHubCreateService.creatApp( - type, name, features, build, reloading, gorm, servlet, javaVersion, code, state, userAgent); + type, name, features, build, reloading, gorm, servlet, gspLayout, javaVersion, code, state, userAgent); if (launcherURI == null) { return HttpResponse.ok(new GitHubCreateDTO(repository.getUrl(), repository.getCloneUrl(), repository.getHtmlUrl())); diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateOperation.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateOperation.java index 6800efa71f6..dcb29060d7c 100644 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateOperation.java +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateOperation.java @@ -28,6 +28,7 @@ import org.grails.forge.application.ApplicationType; import org.grails.forge.options.BuildTool; import org.grails.forge.options.GormImpl; +import org.grails.forge.options.GspLayoutImpl; import org.grails.forge.options.JdkVersion; import org.grails.forge.options.ServletImpl; @@ -67,6 +68,7 @@ HttpResponse createApp( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, + @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @NonNull String code, @NonNull String state, diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateService.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateService.java index ba40f554a7a..1a0373e5319 100644 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateService.java +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateService.java @@ -41,6 +41,7 @@ import org.grails.forge.options.BuildTool; import org.grails.forge.options.GormImpl; import org.grails.forge.options.JdkVersion; +import org.grails.forge.options.GspLayoutImpl; import org.grails.forge.options.ServletImpl; import org.grails.forge.util.GitHubUtil; import org.slf4j.Logger; @@ -95,6 +96,7 @@ protected GitHubRepository creatApp( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, + @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @NonNull String code, @NonNull String state, @@ -105,7 +107,7 @@ protected GitHubRepository creatApp( GitHubUser gitHubUser = getGitHubUser(authToken); GeneratorContext generatorContext = createProjectGeneratorContext( - type, name, features, build, reloading, gorm, servlet, javaVersion, userAgent); + type, name, features, build, reloading, gorm, servlet, gspLayout, javaVersion, userAgent); String repoName = generatorContext.getProject().getName(); String repoDescription = String.format("Grails %s Application", generatorContext.getProject().getNaturalName()); diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/zip/ZipCreateController.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/zip/ZipCreateController.java index f2682c1a199..9f0637e2af0 100644 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/zip/ZipCreateController.java +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/zip/ZipCreateController.java @@ -43,6 +43,7 @@ import org.grails.forge.options.BuildTool; import org.grails.forge.options.GormImpl; import org.grails.forge.options.JdkVersion; +import org.grails.forge.options.GspLayoutImpl; import org.grails.forge.options.ServletImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -110,9 +111,10 @@ public HttpResponse createApp( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, + @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @Nullable @Header(HttpHeaders.USER_AGENT) String userAgent) { - return generateAppIntoZipFile(type, name, features, build, reloading, gorm, servlet, javaVersion, userAgent); + return generateAppIntoZipFile(type, name, features, build, reloading, gorm, servlet, gspLayout, javaVersion, userAgent); } /** @@ -129,7 +131,7 @@ public HttpResponse createApp( * @param userAgent The browser user-agent * @return A Zip file containing the application */ - @Get(uri = "/{name}.zip{?type,features,gorm,servlet,build,reloading}", produces = MEDIA_TYPE_APPLICATION_ZIP) + @Get(uri = "/{name}.zip{?type,features,gorm,servlet,gspLayout,build,reloading}", produces = MEDIA_TYPE_APPLICATION_ZIP) @ApiResponse( description = "A ZIP file containing the generated application.", content = @Content( @@ -144,9 +146,10 @@ public HttpResponse createZip( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, + @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @Nullable @Header("User-Agent") String userAgent) { - return generateAppIntoZipFile(type, name, features, build, reloading, gorm, servlet, javaVersion, userAgent); + return generateAppIntoZipFile(type, name, features, build, reloading, gorm, servlet, gspLayout, javaVersion, userAgent); } public HttpResponse generateAppIntoZipFile( @@ -157,10 +160,11 @@ public HttpResponse generateAppIntoZipFile( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, + @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @Nullable String userAgent) { - GeneratorContext generatorContext = createProjectGeneratorContext(type, name, features, buildTool, reloading, gorm, servlet, javaVersion, userAgent); + GeneratorContext generatorContext = createProjectGeneratorContext(type, name, features, buildTool, reloading, gorm, servlet, gspLayout, javaVersion, userAgent); MutableHttpResponse response = HttpResponse.created(new Writable() { @Override public void writeTo(OutputStream outputStream, @Nullable Charset charset) throws IOException { diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/zip/ZipCreateOperation.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/zip/ZipCreateOperation.java index 3d74bd1470d..9adffad8ba7 100644 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/zip/ZipCreateOperation.java +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/zip/ZipCreateOperation.java @@ -27,6 +27,7 @@ import org.grails.forge.application.ApplicationType; import org.grails.forge.options.BuildTool; import org.grails.forge.options.GormImpl; +import org.grails.forge.options.GspLayoutImpl; import org.grails.forge.options.ServletImpl; import org.grails.forge.options.JdkVersion; @@ -63,6 +64,7 @@ HttpResponse createApp( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, + @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @Nullable @Header(HttpHeaders.USER_AGENT) String userAgent ); diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/diff/DiffController.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/diff/DiffController.java index 4f71dc63d54..50387a00a15 100644 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/diff/DiffController.java +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/diff/DiffController.java @@ -87,7 +87,7 @@ public DiffController(ProjectGenerator projectGenerator, FeatureDiffer featureDi * @param requestInfo The request info * @return A string representing the difference */ - @Get(uri = "/{type}/feature/{feature}{?gorm,servlet,build,test,javaVersion,name}", + @Get(uri = "/{type}/feature/{feature}{?gorm,servlet,gspLayout,build,test,javaVersion,name}", produces = MediaType.TEXT_PLAIN) @Override @ApiResponse(responseCode = "404", description = "If no difference is found") @@ -101,6 +101,7 @@ public Publisher diffFeature( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, + @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @Parameter(hidden = true) RequestInfo requestInfo) { @@ -113,7 +114,7 @@ public Publisher diffFeature( gorm != null ? gorm : GormImpl.DEFAULT_OPTION, servlet != null ? servlet : ServletImpl.DEFAULT_OPTION, javaVersion != null ? javaVersion : JdkVersion.DEFAULT_OPTION - ); + ).withGspLayoutImpl(gspLayout != null ? gspLayout : GspLayoutImpl.DEFAULT_OPTION); projectGenerator = this.projectGenerator; generatorContext = projectGenerator.createGeneratorContext( type, @@ -142,7 +143,7 @@ public Publisher diffFeature( * @param requestInfo The request info * @return An HTTP response that emits a writable */ - @Get(uri = "/{type}/{name}{?features,gorm,servlet,build,test,javaVersion}", produces = MediaType.TEXT_PLAIN) + @Get(uri = "/{type}/{name}{?features,gorm,servlet,gspLayout,build,test,javaVersion}", produces = MediaType.TEXT_PLAIN) @Override @ApiResponse(responseCode = "404", description = "If no difference is found") @ApiResponse(responseCode = "400", description = "If the supplied parameters are invalid") @@ -155,6 +156,7 @@ public Publisher diffApp( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, + @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @Parameter(hidden = true) RequestInfo requestInfo) throws IOException { ProjectGenerator projectGenerator; @@ -166,7 +168,7 @@ public Publisher diffApp( gorm != null ? gorm : GormImpl.DEFAULT_OPTION, servlet != null ? servlet : ServletImpl.DEFAULT_OPTION, javaVersion != null ? javaVersion : JdkVersion.DEFAULT_OPTION - ); + ).withGspLayoutImpl(gspLayout != null ? gspLayout : GspLayoutImpl.DEFAULT_OPTION); projectGenerator = this.projectGenerator; generatorContext = projectGenerator.createGeneratorContext( type, diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/diff/DiffOperations.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/diff/DiffOperations.java index 17cafc7fd26..541d28edbb5 100644 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/diff/DiffOperations.java +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/diff/DiffOperations.java @@ -48,6 +48,7 @@ public interface DiffOperations { * @param reloading The development reloading * @param gorm The GORM implementation * @param servlet The Servlet implementation + * @param gspLayout The GSP layout implementation * @param javaVersion The java version * @param requestInfo The request info * @return An HTTP response that emits a writable @@ -61,6 +62,7 @@ Publisher diffFeature( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, + @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @Parameter(hidden = true) RequestInfo requestInfo) throws IOException; @@ -74,6 +76,7 @@ Publisher diffFeature( * @param reloading The reloading framework * @param gorm The GORM implementation * @param servlet The Servlet implementation + * @param gspLayout The GSP layout implementation * @param javaVersion The java version * @param requestInfo The request info * @return An HTTP response that emits a writable @@ -87,6 +90,7 @@ Publisher diffApp( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, + @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @Parameter(hidden = true) RequestInfo requestInfo) throws IOException; } diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/preview/PreviewController.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/preview/PreviewController.java index 5f92955ddd0..e4bf3b493ba 100644 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/preview/PreviewController.java +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/preview/PreviewController.java @@ -77,7 +77,7 @@ public PreviewController(ProjectGenerator projectGenerator, ApplicationEventPubl * @param servlet The Servlet (optional, defaults to Embedded Tomcat) * @return A preview of the application contents. */ - @Get(uri = "/{type}/{name}{?features,gorm,servlet,build,reloading,javaVersion}", produces = MediaType.APPLICATION_JSON) + @Get(uri = "/{type}/{name}{?features,gorm,servlet,gspLayout,build,reloading,javaVersion}", produces = MediaType.APPLICATION_JSON) @Override public PreviewDTO previewApp( ApplicationType type, @@ -87,6 +87,7 @@ public PreviewDTO previewApp( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, + @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @Parameter(hidden = true) RequestInfo requestInfo) throws IOException { try { @@ -99,7 +100,8 @@ public PreviewDTO previewApp( gorm == null ? GormImpl.DEFAULT_OPTION : gorm, servlet == null ? ServletImpl.DEFAULT_OPTION : servlet, javaVersion == null ? JdkVersion.DEFAULT_OPTION : javaVersion, - getOperatingSystem(requestInfo.getUserAgent())), + getOperatingSystem(requestInfo.getUserAgent())) + .withGspLayoutImpl(gspLayout != null ? gspLayout : GspLayoutImpl.DEFAULT_OPTION), getOperatingSystem(requestInfo.getUserAgent()), features == null ? Collections.emptyList() : features, outputHandler, diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/preview/PreviewOperations.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/preview/PreviewOperations.java index e16700b32da..53467d8543f 100644 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/preview/PreviewOperations.java +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/preview/PreviewOperations.java @@ -26,6 +26,7 @@ import org.grails.forge.application.ApplicationType; import org.grails.forge.options.BuildTool; import org.grails.forge.options.GormImpl; +import org.grails.forge.options.GspLayoutImpl; import org.grails.forge.options.JdkVersion; import org.grails.forge.options.ServletImpl; @@ -43,6 +44,7 @@ public interface PreviewOperations { * @param reloading The development reloading * @param gorm The GORM * @param servlet The Servlet + * @param gspLayout The GSP layout implementation * @param javaVersion The java version * @param requestInfo The request info * @return An HTTP response that emits a writable @@ -56,6 +58,7 @@ PreviewDTO previewApp( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, + @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @Parameter(hidden = true) RequestInfo requestInfo) throws IOException; } From 852d1321aa4f9a46e58e5a7d4886bf87c7540638 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Tue, 9 Jun 2026 23:52:56 -0600 Subject: [PATCH 11/25] Drop redundant user-agent param from the GitHub create endpoint Adding the gspLayout query option pushed GitHubCreateController.createApp and its operation interface to 13 parameters, over the Checkstyle ParameterNumber limit. The endpoint already receives a RequestInfo, which carries the user agent, so the separate @Header user-agent parameter was redundant; drop it and read requestInfo.getUserAgent() instead. This keeps the method within the limit without suppressing the rule. --- .../forge/api/create/github/GitHubCreateController.java | 5 +---- .../forge/api/create/github/GitHubCreateOperation.java | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateController.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateController.java index b8f74359cc6..b1d2113680c 100644 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateController.java +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateController.java @@ -22,12 +22,10 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.StringUtils; -import io.micronaut.http.HttpHeaders; import io.micronaut.http.HttpResponse; import io.micronaut.http.MediaType; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; -import io.micronaut.http.annotation.Header; import io.micronaut.http.annotation.QueryValue; import io.micronaut.scheduling.TaskExecutors; import io.micronaut.scheduling.annotation.ExecuteOn; @@ -117,7 +115,6 @@ public HttpResponse createApp( @Nullable JdkVersion javaVersion, @Nullable String code, @Nullable String state, - @Nullable @Header(HttpHeaders.USER_AGENT) String userAgent, @Parameter(hidden = true) @NonNull RequestInfo requestInfo) { URI launcherURI = redirectService.getLauncherURI(); try { @@ -125,7 +122,7 @@ public HttpResponse createApp( return HttpResponse.temporaryRedirect(redirectService.constructOAuthRedirectUrl(requestInfo)); } else { GitHubRepository repository = gitHubCreateService.creatApp( - type, name, features, build, reloading, gorm, servlet, gspLayout, javaVersion, code, state, userAgent); + type, name, features, build, reloading, gorm, servlet, gspLayout, javaVersion, code, state, requestInfo.getUserAgent()); if (launcherURI == null) { return HttpResponse.ok(new GitHubCreateDTO(repository.getUrl(), repository.getCloneUrl(), repository.getHtmlUrl())); diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateOperation.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateOperation.java index dcb29060d7c..387ddc28613 100644 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateOperation.java +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateOperation.java @@ -20,9 +20,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; -import io.micronaut.http.HttpHeaders; import io.micronaut.http.HttpResponse; -import io.micronaut.http.annotation.Header; import org.grails.forge.api.RequestInfo; import org.grails.forge.api.DevelopmentReloading; import org.grails.forge.application.ApplicationType; @@ -53,10 +51,10 @@ public interface GitHubCreateOperation { * @param reloading The development reloading option * @param gorm The GORM * @param servlet The Servlet + * @param gspLayout The GSP layout implementation * @param javaVersion The java version * @param code The GitHub code * @param state An unguessable random string. It is used to protect against cross-site request forgery attacks. - * @param userAgent The browser user-agent * @param requestInfo The request info * @return An information about newly created GitHub repository */ @@ -72,7 +70,6 @@ HttpResponse createApp( @Nullable JdkVersion javaVersion, @NonNull String code, @NonNull String state, - @Nullable @Header(HttpHeaders.USER_AGENT) String userAgent, @NonNull RequestInfo requestInfo ); } From db817ebd2dde7cf2fde3de7ef5becfb346263e84 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Wed, 10 Jun 2026 00:17:32 -0600 Subject: [PATCH 12/25] Fix if/else spacing in the SiteMesh test-app build files Use "if (" with a space and place "} else {" on a single line in the SITEMESH2_TESTING_ENABLED toggle across the example and suite build files. --- grails-test-examples/app1/build.gradle | 5 ++--- grails-test-examples/app2/build.gradle | 5 ++--- grails-test-examples/app3/build.gradle | 5 ++--- grails-test-examples/async-events-pubsub-demo/build.gradle | 5 ++--- grails-test-examples/cache/build.gradle | 5 ++--- grails-test-examples/datasources/build.gradle | 5 ++--- grails-test-examples/demo33/build.gradle | 5 ++--- grails-test-examples/external-configuration/build.gradle | 5 ++--- grails-test-examples/geb-context-path/build.gradle | 5 ++--- grails-test-examples/geb-gebconfig/build.gradle | 5 ++--- grails-test-examples/geb/build.gradle | 5 ++--- grails-test-examples/gorm/build.gradle | 5 ++--- .../hibernate5/grails-database-per-tenant/build.gradle | 5 ++--- .../hibernate5/grails-hibernate/build.gradle | 5 ++--- .../hibernate5/grails-partitioned-multi-tenancy/build.gradle | 5 ++--- .../hibernate5/grails-schema-per-tenant/build.gradle | 5 ++--- grails-test-examples/hibernate5/issue450/build.gradle | 5 ++--- grails-test-examples/hyphenated/build.gradle | 5 ++--- grails-test-examples/issue-11102/build.gradle | 5 ++--- grails-test-examples/issue-698-domain-save-npe/build.gradle | 5 ++--- grails-test-examples/issue-views-182/build.gradle | 5 ++--- grails-test-examples/mongodb/base/build.gradle | 5 ++--- .../mongodb/database-per-tenant/build.gradle | 5 ++--- grails-test-examples/mongodb/gson-templates/build.gradle | 5 ++--- grails-test-examples/mongodb/hibernate5/build.gradle | 5 ++--- grails-test-examples/namespaces/build.gradle | 5 ++--- grails-test-examples/plugins/issue11005/build.gradle | 5 ++--- grails-test-examples/plugins/loadafter/build.gradle | 5 ++--- grails-test-examples/plugins/loadfirst/build.gradle | 5 ++--- grails-test-examples/plugins/loadsecond/build.gradle | 5 ++--- grails-test-examples/views-functional-tests/build.gradle | 5 ++--- grails-test-suite-uber/build.gradle | 5 ++--- 32 files changed, 64 insertions(+), 96 deletions(-) diff --git a/grails-test-examples/app1/build.gradle b/grails-test-examples/app1/build.gradle index bef37389f4e..d6e58ff03c0 100644 --- a/grails-test-examples/app1/build.gradle +++ b/grails-test-examples/app1/build.gradle @@ -39,10 +39,9 @@ dependencies { implementation 'org.apache.grails:grails-dependencies-starter-web' implementation 'com.h2database:h2' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } diff --git a/grails-test-examples/app2/build.gradle b/grails-test-examples/app2/build.gradle index 50e881e677f..a5d6f09b44f 100644 --- a/grails-test-examples/app2/build.gradle +++ b/grails-test-examples/app2/build.gradle @@ -39,10 +39,9 @@ dependencies { implementation 'org.apache.grails:grails-dependencies-starter-web' implementation 'com.h2database:h2' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } diff --git a/grails-test-examples/app3/build.gradle b/grails-test-examples/app3/build.gradle index 7ccf0f9b905..5e5d1c30822 100644 --- a/grails-test-examples/app3/build.gradle +++ b/grails-test-examples/app3/build.gradle @@ -38,10 +38,9 @@ dependencies { implementation 'org.apache.grails:grails-dependencies-starter-web' implementation 'com.h2database:h2' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } diff --git a/grails-test-examples/async-events-pubsub-demo/build.gradle b/grails-test-examples/async-events-pubsub-demo/build.gradle index 27d6a126a3d..423e8f6545c 100644 --- a/grails-test-examples/async-events-pubsub-demo/build.gradle +++ b/grails-test-examples/async-events-pubsub-demo/build.gradle @@ -47,10 +47,9 @@ dependencies { implementation 'org.apache.grails:grails-services' implementation 'org.apache.grails:grails-url-mappings' implementation 'org.apache.grails:grails-web-boot' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } diff --git a/grails-test-examples/cache/build.gradle b/grails-test-examples/cache/build.gradle index a51112a1217..c3113d4aac0 100644 --- a/grails-test-examples/cache/build.gradle +++ b/grails-test-examples/cache/build.gradle @@ -49,10 +49,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-logging' implementation 'org.springframework.boot:spring-boot-starter-tomcat' implementation 'org.springframework.boot:spring-boot-starter-validation' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } diff --git a/grails-test-examples/datasources/build.gradle b/grails-test-examples/datasources/build.gradle index de5c5aae51d..2257681fbf3 100644 --- a/grails-test-examples/datasources/build.gradle +++ b/grails-test-examples/datasources/build.gradle @@ -33,10 +33,9 @@ dependencies { implementation 'org.apache.grails:grails-dependencies-starter-web' implementation 'com.h2database:h2' implementation 'org.apache.grails:grails-web-boot' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } diff --git a/grails-test-examples/demo33/build.gradle b/grails-test-examples/demo33/build.gradle index 5635b15151f..72c6b8df714 100644 --- a/grails-test-examples/demo33/build.gradle +++ b/grails-test-examples/demo33/build.gradle @@ -45,10 +45,9 @@ dependencies { implementation 'org.apache.grails:grails-interceptors' implementation 'org.apache.grails:grails-web-boot' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-data-hibernate5' diff --git a/grails-test-examples/external-configuration/build.gradle b/grails-test-examples/external-configuration/build.gradle index dabc068b42f..3452ce2bef3 100644 --- a/grails-test-examples/external-configuration/build.gradle +++ b/grails-test-examples/external-configuration/build.gradle @@ -38,10 +38,9 @@ dependencies { implementation 'org.apache.grails:grails-services' implementation 'org.apache.grails:grails-url-mappings' implementation 'org.apache.grails:grails-web-boot' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-data-hibernate5' diff --git a/grails-test-examples/geb-context-path/build.gradle b/grails-test-examples/geb-context-path/build.gradle index 8be98a7f37d..00ae5e70a8f 100644 --- a/grails-test-examples/geb-context-path/build.gradle +++ b/grails-test-examples/geb-context-path/build.gradle @@ -42,10 +42,9 @@ dependencies { implementation 'org.apache.grails:grails-url-mappings' implementation 'org.apache.grails:grails-web-boot' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-data-hibernate5' diff --git a/grails-test-examples/geb-gebconfig/build.gradle b/grails-test-examples/geb-gebconfig/build.gradle index 9f9c58d1913..cf23c19629b 100644 --- a/grails-test-examples/geb-gebconfig/build.gradle +++ b/grails-test-examples/geb-gebconfig/build.gradle @@ -43,10 +43,9 @@ dependencies { implementation 'org.apache.grails:grails-url-mappings' implementation 'org.apache.grails:grails-web-boot' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-data-hibernate5' diff --git a/grails-test-examples/geb/build.gradle b/grails-test-examples/geb/build.gradle index 5bbf216b708..1ed570e8172 100644 --- a/grails-test-examples/geb/build.gradle +++ b/grails-test-examples/geb/build.gradle @@ -42,10 +42,9 @@ dependencies { implementation 'org.apache.grails:grails-url-mappings' implementation 'org.apache.grails:grails-web-boot' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-data-hibernate5' diff --git a/grails-test-examples/gorm/build.gradle b/grails-test-examples/gorm/build.gradle index bd74d355110..b960240d9d7 100644 --- a/grails-test-examples/gorm/build.gradle +++ b/grails-test-examples/gorm/build.gradle @@ -34,10 +34,9 @@ dependencies { implementation 'org.apache.grails:grails-dependencies-starter-web' implementation 'com.h2database:h2' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } diff --git a/grails-test-examples/hibernate5/grails-database-per-tenant/build.gradle b/grails-test-examples/hibernate5/grails-database-per-tenant/build.gradle index 83849045d1f..152288c6b14 100644 --- a/grails-test-examples/hibernate5/grails-database-per-tenant/build.gradle +++ b/grails-test-examples/hibernate5/grails-database-per-tenant/build.gradle @@ -36,10 +36,9 @@ dependencies { implementation 'org.apache.grails:grails-core' implementation 'org.apache.grails:grails-rest-transforms' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } diff --git a/grails-test-examples/hibernate5/grails-hibernate/build.gradle b/grails-test-examples/hibernate5/grails-hibernate/build.gradle index 0ee797d042e..a68f7c9e60d 100644 --- a/grails-test-examples/hibernate5/grails-hibernate/build.gradle +++ b/grails-test-examples/hibernate5/grails-hibernate/build.gradle @@ -38,10 +38,9 @@ dependencies { implementation 'org.apache.grails:grails-core' implementation 'org.apache.grails:grails-rest-transforms' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } diff --git a/grails-test-examples/hibernate5/grails-partitioned-multi-tenancy/build.gradle b/grails-test-examples/hibernate5/grails-partitioned-multi-tenancy/build.gradle index ebfdf4035d2..690d03c3044 100644 --- a/grails-test-examples/hibernate5/grails-partitioned-multi-tenancy/build.gradle +++ b/grails-test-examples/hibernate5/grails-partitioned-multi-tenancy/build.gradle @@ -36,10 +36,9 @@ dependencies { implementation 'org.apache.grails:grails-core' implementation 'org.apache.grails:grails-rest-transforms' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } diff --git a/grails-test-examples/hibernate5/grails-schema-per-tenant/build.gradle b/grails-test-examples/hibernate5/grails-schema-per-tenant/build.gradle index 3a1040e3f0f..dabbf8a53a6 100644 --- a/grails-test-examples/hibernate5/grails-schema-per-tenant/build.gradle +++ b/grails-test-examples/hibernate5/grails-schema-per-tenant/build.gradle @@ -36,10 +36,9 @@ dependencies { implementation 'org.apache.grails:grails-core' implementation 'org.apache.grails:grails-rest-transforms' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } diff --git a/grails-test-examples/hibernate5/issue450/build.gradle b/grails-test-examples/hibernate5/issue450/build.gradle index c88d7a4e2d6..c28e422fa6d 100644 --- a/grails-test-examples/hibernate5/issue450/build.gradle +++ b/grails-test-examples/hibernate5/issue450/build.gradle @@ -36,10 +36,9 @@ dependencies { implementation 'org.apache.grails:grails-core' implementation 'org.apache.grails:grails-rest-transforms' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } diff --git a/grails-test-examples/hyphenated/build.gradle b/grails-test-examples/hyphenated/build.gradle index 6f03f0672fb..6512f0b685f 100644 --- a/grails-test-examples/hyphenated/build.gradle +++ b/grails-test-examples/hyphenated/build.gradle @@ -38,10 +38,9 @@ dependencies { implementation 'org.apache.grails:grails-dependencies-starter-web' implementation 'com.h2database:h2' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } diff --git a/grails-test-examples/issue-11102/build.gradle b/grails-test-examples/issue-11102/build.gradle index 48dcad6e7d8..e2ac7dd7a9a 100644 --- a/grails-test-examples/issue-11102/build.gradle +++ b/grails-test-examples/issue-11102/build.gradle @@ -42,10 +42,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-tomcat' implementation 'org.apache.grails:grails-web-boot' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-logging' diff --git a/grails-test-examples/issue-698-domain-save-npe/build.gradle b/grails-test-examples/issue-698-domain-save-npe/build.gradle index be445a246be..793272c9371 100644 --- a/grails-test-examples/issue-698-domain-save-npe/build.gradle +++ b/grails-test-examples/issue-698-domain-save-npe/build.gradle @@ -32,10 +32,9 @@ dependencies { implementation 'org.apache.grails:grails-dependencies-starter-web' implementation 'com.h2database:h2' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } diff --git a/grails-test-examples/issue-views-182/build.gradle b/grails-test-examples/issue-views-182/build.gradle index 9f2be833e62..b285ef00752 100644 --- a/grails-test-examples/issue-views-182/build.gradle +++ b/grails-test-examples/issue-views-182/build.gradle @@ -48,10 +48,9 @@ dependencies { implementation 'org.apache.grails:grails-datasource' implementation 'org.apache.grails:grails-databinding' implementation 'org.apache.grails:grails-web-boot' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-logging' diff --git a/grails-test-examples/mongodb/base/build.gradle b/grails-test-examples/mongodb/base/build.gradle index c27d9c15f53..1289e7bf5a4 100644 --- a/grails-test-examples/mongodb/base/build.gradle +++ b/grails-test-examples/mongodb/base/build.gradle @@ -36,10 +36,9 @@ dependencies { implementation 'org.apache.grails:grails-core' implementation 'org.apache.grails:grails-rest-transforms' implementation 'org.apache.grails:grails-web-boot' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-gsp' diff --git a/grails-test-examples/mongodb/database-per-tenant/build.gradle b/grails-test-examples/mongodb/database-per-tenant/build.gradle index e7d0cb11a29..7852e29f5cd 100644 --- a/grails-test-examples/mongodb/database-per-tenant/build.gradle +++ b/grails-test-examples/mongodb/database-per-tenant/build.gradle @@ -37,10 +37,9 @@ dependencies { implementation 'org.apache.grails:grails-rest-transforms' implementation 'org.apache.grails:grails-web-boot' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } diff --git a/grails-test-examples/mongodb/gson-templates/build.gradle b/grails-test-examples/mongodb/gson-templates/build.gradle index 2f49f5d9860..b6576b1950e 100644 --- a/grails-test-examples/mongodb/gson-templates/build.gradle +++ b/grails-test-examples/mongodb/gson-templates/build.gradle @@ -36,10 +36,9 @@ dependencies { implementation 'org.apache.grails:grails-views-gson' implementation 'org.apache.grails:grails-views-markup' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-databinding' diff --git a/grails-test-examples/mongodb/hibernate5/build.gradle b/grails-test-examples/mongodb/hibernate5/build.gradle index e43413ae0c4..6fe5ba9fa49 100644 --- a/grails-test-examples/mongodb/hibernate5/build.gradle +++ b/grails-test-examples/mongodb/hibernate5/build.gradle @@ -37,10 +37,9 @@ dependencies { implementation 'org.apache.grails:grails-rest-transforms' implementation 'org.apache.grails:grails-web-boot' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } implementation 'org.apache.grails:grails-data-hibernate5' diff --git a/grails-test-examples/namespaces/build.gradle b/grails-test-examples/namespaces/build.gradle index 74c78757814..88cc0bb649a 100644 --- a/grails-test-examples/namespaces/build.gradle +++ b/grails-test-examples/namespaces/build.gradle @@ -38,10 +38,9 @@ dependencies { implementation 'org.apache.grails:grails-dependencies-starter-web' implementation 'com.h2database:h2' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } diff --git a/grails-test-examples/plugins/issue11005/build.gradle b/grails-test-examples/plugins/issue11005/build.gradle index 48efe2a3d6e..449b2742178 100644 --- a/grails-test-examples/plugins/issue11005/build.gradle +++ b/grails-test-examples/plugins/issue11005/build.gradle @@ -31,10 +31,9 @@ group = 'com.example.grails.plugins' dependencies { implementation platform(project(':grails-bom')) - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } api 'org.apache.grails:grails-dependencies-starter-web' diff --git a/grails-test-examples/plugins/loadafter/build.gradle b/grails-test-examples/plugins/loadafter/build.gradle index 4435c5235ae..d0bc369daef 100644 --- a/grails-test-examples/plugins/loadafter/build.gradle +++ b/grails-test-examples/plugins/loadafter/build.gradle @@ -31,10 +31,9 @@ apply plugin: 'org.apache.grails.gradle.grails-gsp' dependencies { implementation platform(project(':grails-bom')) - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { api 'org.apache.grails:grails-layout' - } - else { + } else { api 'org.apache.grails:grails-sitemesh3' } api 'org.apache.grails:grails-dependencies-starter-web' diff --git a/grails-test-examples/plugins/loadfirst/build.gradle b/grails-test-examples/plugins/loadfirst/build.gradle index 7b3a23270ee..8330e2381bd 100644 --- a/grails-test-examples/plugins/loadfirst/build.gradle +++ b/grails-test-examples/plugins/loadfirst/build.gradle @@ -31,10 +31,9 @@ apply plugin: 'org.apache.grails.gradle.grails-gsp' dependencies { implementation platform(project(':grails-bom')) - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { api 'org.apache.grails:grails-layout' - } - else { + } else { api 'org.apache.grails:grails-sitemesh3' } api 'org.apache.grails:grails-dependencies-starter-web' diff --git a/grails-test-examples/plugins/loadsecond/build.gradle b/grails-test-examples/plugins/loadsecond/build.gradle index 11d011c74c9..14e3b892e62 100644 --- a/grails-test-examples/plugins/loadsecond/build.gradle +++ b/grails-test-examples/plugins/loadsecond/build.gradle @@ -30,10 +30,9 @@ apply plugin: 'org.apache.grails.gradle.grails-gsp' dependencies { implementation platform(project(':grails-bom')) - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { api 'org.apache.grails:grails-layout' - } - else { + } else { api 'org.apache.grails:grails-sitemesh3' } api 'org.apache.grails:grails-dependencies-starter-web' diff --git a/grails-test-examples/views-functional-tests/build.gradle b/grails-test-examples/views-functional-tests/build.gradle index 70ac6f07b4e..94996c4932c 100644 --- a/grails-test-examples/views-functional-tests/build.gradle +++ b/grails-test-examples/views-functional-tests/build.gradle @@ -39,10 +39,9 @@ dependencies { implementation 'org.apache.grails:grails-core' implementation 'org.apache.grails:grails-logging' implementation 'org.apache.grails:grails-web-boot' - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' - } - else { + } else { implementation 'org.apache.grails:grails-sitemesh3' } diff --git a/grails-test-suite-uber/build.gradle b/grails-test-suite-uber/build.gradle index 4a1fa030a9a..c85f5861672 100644 --- a/grails-test-suite-uber/build.gradle +++ b/grails-test-suite-uber/build.gradle @@ -50,10 +50,9 @@ dependencies { testImplementation 'tools.jackson.core:jackson-databind' testImplementation project(':grails-data-hibernate5-core') testImplementation project(':grails-gsp') - if(System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation project(':grails-layout') - } - else { + } else { implementation project(':grails-sitemesh3') } testImplementation project(':grails-testing-support-datamapping'), { From 51d5e883100e07f012374863d63d3cc55fff709b Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Wed, 10 Jun 2026 09:25:49 -0600 Subject: [PATCH 13/25] Make SiteMesh 3 the silent forge default, SiteMesh 2 a selectable feature Per review, drop the GspLayoutImpl select option and its plumbing through Options, the controllers and the CLI. The layout engine keeps the same shape it has today, just inverted: SiteMesh 3 (grails-sitemesh3) is the invisible default applied to web applications, and the "GSP SiteMesh 2 Layouts" feature (grails-layout) can be selected to override it. Because the default is invisible it never appears as a feature card, which also fixes the UI glitch where both layouts showed after selecting SiteMesh 2. No grails-forge-ui changes are required. --- .../forge/api/ApplicationController.java | 3 +- .../grails/forge/api/GspLayoutImplDTO.java | 100 ------------------ .../grails/forge/api/SelectOptionsDTO.java | 22 +--- .../api/create/AbstractCreateController.java | 4 +- .../create/github/GitHubCreateController.java | 9 +- .../create/github/GitHubCreateOperation.java | 7 +- .../create/github/GitHubCreateService.java | 4 +- .../api/create/zip/ZipCreateController.java | 12 +-- .../api/create/zip/ZipCreateOperation.java | 2 - .../grails/forge/api/diff/DiffController.java | 10 +- .../grails/forge/api/diff/DiffOperations.java | 4 - .../options/GspLayoutImplSelectOptions.java | 42 -------- .../forge/api/preview/PreviewController.java | 6 +- .../forge/api/preview/PreviewOperations.java | 3 - .../SelectOptionsControllerSpec.groovy | 48 --------- .../forge/cli/command/CreateCommand.java | 13 +-- .../cli/command/GspLayoutImplCandidates.java | 31 ------ .../cli/command/GspLayoutImplConverter.java | 41 ------- .../forge/application/ContextFactory.java | 10 +- .../forge/feature/sitemesh3/Sitemesh3.java | 17 ++- .../forge/feature/view/GrailsLayout.java | 15 +-- .../grails/forge/feature/view/GspLayout.java | 12 +-- .../grails/forge/options/FeatureFilter.java | 10 -- .../grails/forge/options/GspLayoutImpl.java | 54 ---------- .../org/grails/forge/options/Options.java | 31 +----- .../org/grails/forge/BuildBuilder.groovy | 8 -- .../forge/feature/view/GspLayoutSpec.groovy | 23 ++-- 27 files changed, 54 insertions(+), 487 deletions(-) delete mode 100644 grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/GspLayoutImplDTO.java delete mode 100644 grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/options/GspLayoutImplSelectOptions.java delete mode 100644 grails-forge/grails-forge-api/src/test/groovy/org/grails/forge/api/options/SelectOptionsControllerSpec.groovy delete mode 100644 grails-forge/grails-forge-cli/src/main/java/org/grails/forge/cli/command/GspLayoutImplCandidates.java delete mode 100644 grails-forge/grails-forge-cli/src/main/java/org/grails/forge/cli/command/GspLayoutImplConverter.java delete mode 100644 grails-forge/grails-forge-core/src/main/java/org/grails/forge/options/GspLayoutImpl.java diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/ApplicationController.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/ApplicationController.java index 64a0319236b..c3182db1e4e 100644 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/ApplicationController.java +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/ApplicationController.java @@ -235,7 +235,6 @@ protected Options getOptions(@Nullable FeatureFilter filter, RequestInfo request filter.getGorm() == null ? GormImpl.DEFAULT_OPTION : filter.getGorm(), filter.getServlet() == null ? ServletImpl.DEFAULT_OPTION : filter.getServlet(), filter.getJavaVersion() == null ? JdkVersion.DEFAULT_OPTION : filter.getJavaVersion(), - getOperatingSystem(requestInfo.getUserAgent())) - .withGspLayoutImpl(filter.getGspLayout() == null ? GspLayoutImpl.DEFAULT_OPTION : filter.getGspLayout()); + getOperatingSystem(requestInfo.getUserAgent())); } } diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/GspLayoutImplDTO.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/GspLayoutImplDTO.java deleted file mode 100644 index c25b70329c3..00000000000 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/GspLayoutImplDTO.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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.api; - -import io.micronaut.context.MessageSource; -import io.micronaut.core.annotation.Creator; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.Introspected; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.naming.Described; -import io.micronaut.core.naming.Named; -import io.swagger.v3.oas.annotations.media.Schema; -import org.grails.forge.options.GspLayoutImpl; - -/** - * DTO objects for {@link GspLayoutImpl}. - * - * @since 8.0.0 - */ -@Schema(name = "GspLayoutImplInfo") -@Introspected -public class GspLayoutImplDTO extends Linkable implements Named, Described, Selectable { - - static final String MESSAGE_PREFIX = GrailsForgeConfiguration.PREFIX + ".gspLayoutImpl."; - - private final String name; - private final String description; - private final GspLayoutImpl value; - - /** - * @param gspLayoutImpl The {@link GspLayoutImpl} - */ - public GspLayoutImplDTO(GspLayoutImpl gspLayoutImpl) { - this.value = gspLayoutImpl; - this.name = gspLayoutImpl.getName(); - this.description = gspLayoutImpl.getName(); - } - - @Creator - @Internal - GspLayoutImplDTO(GspLayoutImpl gspLayoutImpl, - String name, - String description) { - this.value = gspLayoutImpl; - this.name = name; - this.description = description; - } - - @Internal - GspLayoutImplDTO(GspLayoutImpl gspLayoutImpl, - MessageSource messageSource, - MessageSource.MessageContext messageContext) { - this.value = gspLayoutImpl; - String name = gspLayoutImpl.getName(); - this.name = name; - this.description = messageSource.getMessage(MESSAGE_PREFIX + name + ".description", messageContext, name); - } - - @NonNull - @Override - @Schema(description = "A description of the GSP Layout Implementation") - public String getDescription() { - return description; - } - - @Override - @Schema(description = "The name of the GSP Layout Implementation") - @NonNull - public String getName() { - return name; - } - - @Override - @Schema(description = "The value of the GSP Layout Implementation for select options") - public GspLayoutImpl getValue() { - return value; - } - - @Override - @Schema(description = "The label of the GSP Layout Implementation for select options") - public String getLabel() { - return value.getLabel(); - } -} diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/SelectOptionsDTO.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/SelectOptionsDTO.java index b4345bf574a..f25ee04940f 100644 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/SelectOptionsDTO.java +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/SelectOptionsDTO.java @@ -52,8 +52,6 @@ public class SelectOptionsDTO { private ServletImplSelectOptions servlet; - private GspLayoutImplSelectOptions gspLayout; - SelectOptionsDTO() { } @@ -63,15 +61,13 @@ public SelectOptionsDTO(ApplicationTypeSelectOptions type, LanguageSelectOptions lang, DevelopmentReloadingSelectOptions reloading, GormImplSelectOptions gorm, - ServletImplSelectOptions servlet, - GspLayoutImplSelectOptions gspLayout) { + ServletImplSelectOptions servlet) { this.type = type; this.jdkVersion = jdkVersion; this.lang = lang; this.reloading = reloading; this.gorm = gorm; this.servlet = servlet; - this.gspLayout = gspLayout; } @Schema(description = "supported options for application type") @@ -104,11 +100,6 @@ public ServletImplSelectOptions getServlet() { return servlet; } - @Schema(description = "supported options for GSP Layout Implementation") - public GspLayoutImplSelectOptions getGspLayout() { - return gspLayout; - } - /** * Build the options * @@ -172,17 +163,8 @@ public static SelectOptionsDTO make(MessageSource messageSource, MessageSource.M new ServletImplDTO(ServletImpl.DEFAULT_OPTION, messageSource, messageContext) ); - List gspLayoutImpls = Arrays.stream(GspLayoutImpl.values()) - .map(it -> new GspLayoutImplDTO(it, messageSource, messageContext)) - .collect(Collectors.toList()); - - GspLayoutImplSelectOptions gspLayoutImplOpts = new GspLayoutImplSelectOptions( - gspLayoutImpls, - new GspLayoutImplDTO(GspLayoutImpl.DEFAULT_OPTION, messageSource, messageContext) - ); - - return new SelectOptionsDTO(applicationOpts, jdkVersionOpts, languageOpts, developmentReloadingOpts, gormImplOpts, servletImplOpts, gspLayoutImplOpts); + return new SelectOptionsDTO(applicationOpts, jdkVersionOpts, languageOpts, developmentReloadingOpts, gormImplOpts, servletImplOpts); } } diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/AbstractCreateController.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/AbstractCreateController.java index 314aa9c504d..bf9286a94a7 100644 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/AbstractCreateController.java +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/AbstractCreateController.java @@ -73,7 +73,6 @@ public GeneratorContext createProjectGeneratorContext( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, - @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @Nullable @Header(HttpHeaders.USER_AGENT) String userAgent) { Project project; @@ -94,8 +93,7 @@ public GeneratorContext createProjectGeneratorContext( gormImpl == null ? GormImpl.DEFAULT_OPTION : gormImpl, servletImpl == null ? ServletImpl.DEFAULT_OPTION : servletImpl, javaVersion == null ? JdkVersion.DEFAULT_OPTION : javaVersion, - getOperatingSystem(userAgent)) - .withGspLayoutImpl(gspLayout != null ? gspLayout : GspLayoutImpl.DEFAULT_OPTION), + getOperatingSystem(userAgent)), getOperatingSystem(userAgent), features != null ? features : Collections.emptyList(), ConsoleOutput.NOOP diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateController.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateController.java index b1d2113680c..b217f4ccb0d 100644 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateController.java +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateController.java @@ -22,10 +22,12 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpHeaders; import io.micronaut.http.HttpResponse; import io.micronaut.http.MediaType; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Header; import io.micronaut.http.annotation.QueryValue; import io.micronaut.scheduling.TaskExecutors; import io.micronaut.scheduling.annotation.ExecuteOn; @@ -40,7 +42,6 @@ import org.grails.forge.options.BuildTool; import org.grails.forge.options.GormImpl; import org.grails.forge.options.JdkVersion; -import org.grails.forge.options.GspLayoutImpl; import org.grails.forge.options.ServletImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -84,7 +85,7 @@ public GitHubCreateController(GitHubCreateService gitHubCreateService, * @return A json containing the generated application details. */ @Override - @Get(uri = "/github/{type}/{name}{?features,gorm,servlet,gspLayout,build,reloading,javaVersion,code,state}", produces = MediaType.APPLICATION_JSON) + @Get(uri = "/github/{type}/{name}{?features,gorm,servlet,build,reloading,javaVersion,code,state}", produces = MediaType.APPLICATION_JSON) @ApiResponses(value = { @ApiResponse( responseCode = "200", @@ -111,10 +112,10 @@ public HttpResponse createApp( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, - @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @Nullable String code, @Nullable String state, + @Nullable @Header(HttpHeaders.USER_AGENT) String userAgent, @Parameter(hidden = true) @NonNull RequestInfo requestInfo) { URI launcherURI = redirectService.getLauncherURI(); try { @@ -122,7 +123,7 @@ public HttpResponse createApp( return HttpResponse.temporaryRedirect(redirectService.constructOAuthRedirectUrl(requestInfo)); } else { GitHubRepository repository = gitHubCreateService.creatApp( - type, name, features, build, reloading, gorm, servlet, gspLayout, javaVersion, code, state, requestInfo.getUserAgent()); + type, name, features, build, reloading, gorm, servlet, javaVersion, code, state, userAgent); if (launcherURI == null) { return HttpResponse.ok(new GitHubCreateDTO(repository.getUrl(), repository.getCloneUrl(), repository.getHtmlUrl())); diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateOperation.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateOperation.java index 387ddc28613..6800efa71f6 100644 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateOperation.java +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateOperation.java @@ -20,13 +20,14 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.HttpHeaders; import io.micronaut.http.HttpResponse; +import io.micronaut.http.annotation.Header; import org.grails.forge.api.RequestInfo; import org.grails.forge.api.DevelopmentReloading; import org.grails.forge.application.ApplicationType; import org.grails.forge.options.BuildTool; import org.grails.forge.options.GormImpl; -import org.grails.forge.options.GspLayoutImpl; import org.grails.forge.options.JdkVersion; import org.grails.forge.options.ServletImpl; @@ -51,10 +52,10 @@ public interface GitHubCreateOperation { * @param reloading The development reloading option * @param gorm The GORM * @param servlet The Servlet - * @param gspLayout The GSP layout implementation * @param javaVersion The java version * @param code The GitHub code * @param state An unguessable random string. It is used to protect against cross-site request forgery attacks. + * @param userAgent The browser user-agent * @param requestInfo The request info * @return An information about newly created GitHub repository */ @@ -66,10 +67,10 @@ HttpResponse createApp( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, - @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @NonNull String code, @NonNull String state, + @Nullable @Header(HttpHeaders.USER_AGENT) String userAgent, @NonNull RequestInfo requestInfo ); } diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateService.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateService.java index 1a0373e5319..ba40f554a7a 100644 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateService.java +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateService.java @@ -41,7 +41,6 @@ import org.grails.forge.options.BuildTool; import org.grails.forge.options.GormImpl; import org.grails.forge.options.JdkVersion; -import org.grails.forge.options.GspLayoutImpl; import org.grails.forge.options.ServletImpl; import org.grails.forge.util.GitHubUtil; import org.slf4j.Logger; @@ -96,7 +95,6 @@ protected GitHubRepository creatApp( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, - @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @NonNull String code, @NonNull String state, @@ -107,7 +105,7 @@ protected GitHubRepository creatApp( GitHubUser gitHubUser = getGitHubUser(authToken); GeneratorContext generatorContext = createProjectGeneratorContext( - type, name, features, build, reloading, gorm, servlet, gspLayout, javaVersion, userAgent); + type, name, features, build, reloading, gorm, servlet, javaVersion, userAgent); String repoName = generatorContext.getProject().getName(); String repoDescription = String.format("Grails %s Application", generatorContext.getProject().getNaturalName()); diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/zip/ZipCreateController.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/zip/ZipCreateController.java index 9f0637e2af0..f2682c1a199 100644 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/zip/ZipCreateController.java +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/zip/ZipCreateController.java @@ -43,7 +43,6 @@ import org.grails.forge.options.BuildTool; import org.grails.forge.options.GormImpl; import org.grails.forge.options.JdkVersion; -import org.grails.forge.options.GspLayoutImpl; import org.grails.forge.options.ServletImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -111,10 +110,9 @@ public HttpResponse createApp( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, - @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @Nullable @Header(HttpHeaders.USER_AGENT) String userAgent) { - return generateAppIntoZipFile(type, name, features, build, reloading, gorm, servlet, gspLayout, javaVersion, userAgent); + return generateAppIntoZipFile(type, name, features, build, reloading, gorm, servlet, javaVersion, userAgent); } /** @@ -131,7 +129,7 @@ public HttpResponse createApp( * @param userAgent The browser user-agent * @return A Zip file containing the application */ - @Get(uri = "/{name}.zip{?type,features,gorm,servlet,gspLayout,build,reloading}", produces = MEDIA_TYPE_APPLICATION_ZIP) + @Get(uri = "/{name}.zip{?type,features,gorm,servlet,build,reloading}", produces = MEDIA_TYPE_APPLICATION_ZIP) @ApiResponse( description = "A ZIP file containing the generated application.", content = @Content( @@ -146,10 +144,9 @@ public HttpResponse createZip( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, - @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @Nullable @Header("User-Agent") String userAgent) { - return generateAppIntoZipFile(type, name, features, build, reloading, gorm, servlet, gspLayout, javaVersion, userAgent); + return generateAppIntoZipFile(type, name, features, build, reloading, gorm, servlet, javaVersion, userAgent); } public HttpResponse generateAppIntoZipFile( @@ -160,11 +157,10 @@ public HttpResponse generateAppIntoZipFile( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, - @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @Nullable String userAgent) { - GeneratorContext generatorContext = createProjectGeneratorContext(type, name, features, buildTool, reloading, gorm, servlet, gspLayout, javaVersion, userAgent); + GeneratorContext generatorContext = createProjectGeneratorContext(type, name, features, buildTool, reloading, gorm, servlet, javaVersion, userAgent); MutableHttpResponse response = HttpResponse.created(new Writable() { @Override public void writeTo(OutputStream outputStream, @Nullable Charset charset) throws IOException { diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/zip/ZipCreateOperation.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/zip/ZipCreateOperation.java index 9adffad8ba7..3d74bd1470d 100644 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/zip/ZipCreateOperation.java +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/zip/ZipCreateOperation.java @@ -27,7 +27,6 @@ import org.grails.forge.application.ApplicationType; import org.grails.forge.options.BuildTool; import org.grails.forge.options.GormImpl; -import org.grails.forge.options.GspLayoutImpl; import org.grails.forge.options.ServletImpl; import org.grails.forge.options.JdkVersion; @@ -64,7 +63,6 @@ HttpResponse createApp( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, - @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @Nullable @Header(HttpHeaders.USER_AGENT) String userAgent ); diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/diff/DiffController.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/diff/DiffController.java index 50387a00a15..4f71dc63d54 100644 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/diff/DiffController.java +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/diff/DiffController.java @@ -87,7 +87,7 @@ public DiffController(ProjectGenerator projectGenerator, FeatureDiffer featureDi * @param requestInfo The request info * @return A string representing the difference */ - @Get(uri = "/{type}/feature/{feature}{?gorm,servlet,gspLayout,build,test,javaVersion,name}", + @Get(uri = "/{type}/feature/{feature}{?gorm,servlet,build,test,javaVersion,name}", produces = MediaType.TEXT_PLAIN) @Override @ApiResponse(responseCode = "404", description = "If no difference is found") @@ -101,7 +101,6 @@ public Publisher diffFeature( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, - @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @Parameter(hidden = true) RequestInfo requestInfo) { @@ -114,7 +113,7 @@ public Publisher diffFeature( gorm != null ? gorm : GormImpl.DEFAULT_OPTION, servlet != null ? servlet : ServletImpl.DEFAULT_OPTION, javaVersion != null ? javaVersion : JdkVersion.DEFAULT_OPTION - ).withGspLayoutImpl(gspLayout != null ? gspLayout : GspLayoutImpl.DEFAULT_OPTION); + ); projectGenerator = this.projectGenerator; generatorContext = projectGenerator.createGeneratorContext( type, @@ -143,7 +142,7 @@ public Publisher diffFeature( * @param requestInfo The request info * @return An HTTP response that emits a writable */ - @Get(uri = "/{type}/{name}{?features,gorm,servlet,gspLayout,build,test,javaVersion}", produces = MediaType.TEXT_PLAIN) + @Get(uri = "/{type}/{name}{?features,gorm,servlet,build,test,javaVersion}", produces = MediaType.TEXT_PLAIN) @Override @ApiResponse(responseCode = "404", description = "If no difference is found") @ApiResponse(responseCode = "400", description = "If the supplied parameters are invalid") @@ -156,7 +155,6 @@ public Publisher diffApp( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, - @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @Parameter(hidden = true) RequestInfo requestInfo) throws IOException { ProjectGenerator projectGenerator; @@ -168,7 +166,7 @@ public Publisher diffApp( gorm != null ? gorm : GormImpl.DEFAULT_OPTION, servlet != null ? servlet : ServletImpl.DEFAULT_OPTION, javaVersion != null ? javaVersion : JdkVersion.DEFAULT_OPTION - ).withGspLayoutImpl(gspLayout != null ? gspLayout : GspLayoutImpl.DEFAULT_OPTION); + ); projectGenerator = this.projectGenerator; generatorContext = projectGenerator.createGeneratorContext( type, diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/diff/DiffOperations.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/diff/DiffOperations.java index 541d28edbb5..17cafc7fd26 100644 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/diff/DiffOperations.java +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/diff/DiffOperations.java @@ -48,7 +48,6 @@ public interface DiffOperations { * @param reloading The development reloading * @param gorm The GORM implementation * @param servlet The Servlet implementation - * @param gspLayout The GSP layout implementation * @param javaVersion The java version * @param requestInfo The request info * @return An HTTP response that emits a writable @@ -62,7 +61,6 @@ Publisher diffFeature( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, - @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @Parameter(hidden = true) RequestInfo requestInfo) throws IOException; @@ -76,7 +74,6 @@ Publisher diffFeature( * @param reloading The reloading framework * @param gorm The GORM implementation * @param servlet The Servlet implementation - * @param gspLayout The GSP layout implementation * @param javaVersion The java version * @param requestInfo The request info * @return An HTTP response that emits a writable @@ -90,7 +87,6 @@ Publisher diffApp( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, - @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @Parameter(hidden = true) RequestInfo requestInfo) throws IOException; } diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/options/GspLayoutImplSelectOptions.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/options/GspLayoutImplSelectOptions.java deleted file mode 100644 index c7e76e431d5..00000000000 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/options/GspLayoutImplSelectOptions.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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.api.options; - -import io.swagger.v3.oas.annotations.media.Schema; -import org.grails.forge.api.GspLayoutImplDTO; -import org.grails.forge.api.SelectOptionDTO; - -import java.util.List; - -@Schema(name = "GspLayoutImplSelectOptions") -public class GspLayoutImplSelectOptions extends SelectOptionDTO { - public GspLayoutImplSelectOptions(List options, GspLayoutImplDTO defaultOption) { - super(options, defaultOption); - } - - @Override - public List getOptions() { - return super.getOptions(); - } - - @Override - public GspLayoutImplDTO getDefaultOption() { - return super.getDefaultOption(); - } -} diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/preview/PreviewController.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/preview/PreviewController.java index e4bf3b493ba..5f92955ddd0 100644 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/preview/PreviewController.java +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/preview/PreviewController.java @@ -77,7 +77,7 @@ public PreviewController(ProjectGenerator projectGenerator, ApplicationEventPubl * @param servlet The Servlet (optional, defaults to Embedded Tomcat) * @return A preview of the application contents. */ - @Get(uri = "/{type}/{name}{?features,gorm,servlet,gspLayout,build,reloading,javaVersion}", produces = MediaType.APPLICATION_JSON) + @Get(uri = "/{type}/{name}{?features,gorm,servlet,build,reloading,javaVersion}", produces = MediaType.APPLICATION_JSON) @Override public PreviewDTO previewApp( ApplicationType type, @@ -87,7 +87,6 @@ public PreviewDTO previewApp( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, - @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @Parameter(hidden = true) RequestInfo requestInfo) throws IOException { try { @@ -100,8 +99,7 @@ public PreviewDTO previewApp( gorm == null ? GormImpl.DEFAULT_OPTION : gorm, servlet == null ? ServletImpl.DEFAULT_OPTION : servlet, javaVersion == null ? JdkVersion.DEFAULT_OPTION : javaVersion, - getOperatingSystem(requestInfo.getUserAgent())) - .withGspLayoutImpl(gspLayout != null ? gspLayout : GspLayoutImpl.DEFAULT_OPTION), + getOperatingSystem(requestInfo.getUserAgent())), getOperatingSystem(requestInfo.getUserAgent()), features == null ? Collections.emptyList() : features, outputHandler, diff --git a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/preview/PreviewOperations.java b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/preview/PreviewOperations.java index 53467d8543f..e16700b32da 100644 --- a/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/preview/PreviewOperations.java +++ b/grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/preview/PreviewOperations.java @@ -26,7 +26,6 @@ import org.grails.forge.application.ApplicationType; import org.grails.forge.options.BuildTool; import org.grails.forge.options.GormImpl; -import org.grails.forge.options.GspLayoutImpl; import org.grails.forge.options.JdkVersion; import org.grails.forge.options.ServletImpl; @@ -44,7 +43,6 @@ public interface PreviewOperations { * @param reloading The development reloading * @param gorm The GORM * @param servlet The Servlet - * @param gspLayout The GSP layout implementation * @param javaVersion The java version * @param requestInfo The request info * @return An HTTP response that emits a writable @@ -58,7 +56,6 @@ PreviewDTO previewApp( @Nullable DevelopmentReloading reloading, @Nullable GormImpl gorm, @Nullable ServletImpl servlet, - @Nullable GspLayoutImpl gspLayout, @Nullable JdkVersion javaVersion, @Parameter(hidden = true) RequestInfo requestInfo) throws IOException; } diff --git a/grails-forge/grails-forge-api/src/test/groovy/org/grails/forge/api/options/SelectOptionsControllerSpec.groovy b/grails-forge/grails-forge-api/src/test/groovy/org/grails/forge/api/options/SelectOptionsControllerSpec.groovy deleted file mode 100644 index 349c156054f..00000000000 --- a/grails-forge/grails-forge-api/src/test/groovy/org/grails/forge/api/options/SelectOptionsControllerSpec.groovy +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.api.options - -import io.micronaut.http.HttpRequest -import io.micronaut.http.client.HttpClient -import io.micronaut.http.client.annotation.Client -import io.micronaut.test.extensions.spock.annotation.MicronautTest -import jakarta.inject.Inject -import org.grails.forge.api.SelectOptionsDTO -import org.grails.forge.options.GspLayoutImpl -import spock.lang.Specification - -@MicronautTest -class SelectOptionsControllerSpec extends Specification { - - @Inject - @Client("/") - HttpClient httpClient - - void "select options expose the GSP layout implementations with SiteMesh 3 as the default"() { - when: - SelectOptionsDTO selectOptions = httpClient.toBlocking() - .retrieve(HttpRequest.GET('/select-options'), SelectOptionsDTO) - - then: - selectOptions.gspLayout - selectOptions.gspLayout.defaultOption.value == GspLayoutImpl.SITEMESH3 - selectOptions.gspLayout.options*.value as Set == [GspLayoutImpl.SITEMESH3, GspLayoutImpl.GRAILS_LAYOUT] as Set - } -} diff --git a/grails-forge/grails-forge-cli/src/main/java/org/grails/forge/cli/command/CreateCommand.java b/grails-forge/grails-forge-cli/src/main/java/org/grails/forge/cli/command/CreateCommand.java index 0f1272ed53f..aa478ae6749 100644 --- a/grails-forge/grails-forge-cli/src/main/java/org/grails/forge/cli/command/CreateCommand.java +++ b/grails-forge/grails-forge-cli/src/main/java/org/grails/forge/cli/command/CreateCommand.java @@ -57,10 +57,6 @@ public abstract class CreateCommand extends BaseCommand implements Callable getAdditionalOptions() { public Integer call() throws Exception { if (listFeatures) { new ListFeatures(availableFeatures, - new Options(reloading, gormImpl, servletImpl, getJdkVersion(), getOperatingSystem()).withGspLayoutImpl(getGspLayoutImpl()), + new Options(reloading, gormImpl, servletImpl, getJdkVersion(), getOperatingSystem()), applicationType, getOperatingSystem(), contextFactory).output(this); @@ -126,16 +122,11 @@ public void generate(OutputHandler outputHandler) throws Exception { } public void generate(Project project, OutputHandler outputHandler) throws Exception { - Options options = new Options(reloading, gormImpl, servletImpl, getJdkVersion(), getOperatingSystem(), getAdditionalOptions()) - .withGspLayoutImpl(getGspLayoutImpl()); + Options options = new Options(reloading, gormImpl, servletImpl, getJdkVersion(), getOperatingSystem(), getAdditionalOptions()); projectGenerator.generate(applicationType, project, options, getOperatingSystem(), getSelectedFeatures(), outputHandler, this); } - private GspLayoutImpl getGspLayoutImpl() { - return gspLayoutImpl == null ? GspLayoutImpl.DEFAULT_OPTION : gspLayoutImpl; - } - private JdkVersion getJdkVersion() { if (javaVersion == null) { return JdkVersion.DEFAULT_OPTION; diff --git a/grails-forge/grails-forge-cli/src/main/java/org/grails/forge/cli/command/GspLayoutImplCandidates.java b/grails-forge/grails-forge-cli/src/main/java/org/grails/forge/cli/command/GspLayoutImplCandidates.java deleted file mode 100644 index f8bd3d75075..00000000000 --- a/grails-forge/grails-forge-cli/src/main/java/org/grails/forge/cli/command/GspLayoutImplCandidates.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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.cli.command; - -import org.grails.forge.options.GspLayoutImpl; - -import java.util.ArrayList; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class GspLayoutImplCandidates extends ArrayList { - public GspLayoutImplCandidates() { - super(Stream.of(GspLayoutImpl.values()).map(GspLayoutImpl::getName).collect(Collectors.toList())); - } -} diff --git a/grails-forge/grails-forge-cli/src/main/java/org/grails/forge/cli/command/GspLayoutImplConverter.java b/grails-forge/grails-forge-cli/src/main/java/org/grails/forge/cli/command/GspLayoutImplConverter.java deleted file mode 100644 index 8e3efad2727..00000000000 --- a/grails-forge/grails-forge-cli/src/main/java/org/grails/forge/cli/command/GspLayoutImplConverter.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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.cli.command; - -import io.micronaut.core.annotation.Introspected; -import org.grails.forge.options.GspLayoutImpl; -import picocli.CommandLine; - -@Introspected -public class GspLayoutImplConverter implements CommandLine.ITypeConverter { - - @Override - public GspLayoutImpl convert(String value) throws Exception { - if (value == null) { - return GspLayoutImpl.DEFAULT_OPTION; - } else { - for (GspLayoutImpl impl : GspLayoutImpl.values()) { - if (value.equalsIgnoreCase(impl.getName()) || value.equalsIgnoreCase(impl.name())) { - return impl; - } - } - } - throw new CommandLine.TypeConversionException("Invalid GSP layout implementation selection: " + value); - } -} diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/application/ContextFactory.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/application/ContextFactory.java index 27fc48ca266..f499008902b 100644 --- a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/application/ContextFactory.java +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/application/ContextFactory.java @@ -65,8 +65,7 @@ public FeatureContext createFeatureContext(AvailableFeatures availableFeatures, Options newOptions = options .withDevelopmentReloading(determineDevelopmentReloading(options.getDevelopmentReloading())) .withGormImpl(determineGormImpl(options.getGormImpl())) - .withServletImpl(determineServletImpl(options.getServletImpl())) - .withGspLayoutImpl(determineGspLayoutImpl(options.getGspLayoutImpl())); + .withServletImpl(determineServletImpl(options.getServletImpl())); availableFeatures.getAllFeatures() .filter(f -> f instanceof DefaultFeature) @@ -121,13 +120,6 @@ GormImpl determineGormImpl(GormImpl gormImpl) { return gormImpl; } - GspLayoutImpl determineGspLayoutImpl(GspLayoutImpl gspLayoutImpl) { - if (gspLayoutImpl == null) { - gspLayoutImpl = GspLayoutImpl.DEFAULT_OPTION; - } - return gspLayoutImpl; - } - ServletImpl determineServletImpl(ServletImpl servletImpl) { if (servletImpl == null) { servletImpl = ServletImpl.DEFAULT_OPTION; diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/sitemesh3/Sitemesh3.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/sitemesh3/Sitemesh3.java index ab5301439ed..f828e35f0c9 100644 --- a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/sitemesh3/Sitemesh3.java +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/sitemesh3/Sitemesh3.java @@ -22,20 +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.DefaultFeature; import org.grails.forge.feature.Feature; import org.grails.forge.feature.view.GspLayout; -import org.grails.forge.options.GspLayoutImpl; import org.grails.forge.options.Options; import java.util.Set; /** * Default GSP layout decorator, backed by SiteMesh 3 ({@code grails-sitemesh3}). - * Applied automatically to web applications unless the {@link GspLayoutImpl} - * option selects the legacy SiteMesh 2 based {@code grails-layout} decorator. + * 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 { +public class Sitemesh3 extends GspLayout implements DefaultFeature { @Override public String getName() { @@ -52,10 +54,15 @@ public String getDescription() { return "Adds support for SiteMesh 3 based GSP layouts"; } + @Override + public boolean isVisible() { + return false; + } + @Override public boolean shouldApply(ApplicationType applicationType, Options options, Set selectedFeatures) { return supports(applicationType) && - options.getGspLayoutImpl() == GspLayoutImpl.SITEMESH3; + selectedFeatures.stream().noneMatch(GspLayout.class::isInstance); } @Override diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GrailsLayout.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GrailsLayout.java index 52ab66af69d..5bd2b958b9c 100644 --- a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GrailsLayout.java +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GrailsLayout.java @@ -19,20 +19,13 @@ package org.grails.forge.feature.view; import jakarta.inject.Singleton; -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.Feature; -import org.grails.forge.options.GspLayoutImpl; -import org.grails.forge.options.Options; - -import java.util.Set; /** * Opt-in GSP layout decorator backed by the legacy SiteMesh 2 based * {@code grails-layout} plugin. Mutually exclusive with {@code sitemesh3}; - * applied when the {@link GspLayoutImpl} option selects {@code grails-layout} - * instead of the default SiteMesh 3 decorator. + * selecting this feature replaces the default SiteMesh 3 decorator. */ @Singleton public class GrailsLayout extends GspLayout { @@ -52,12 +45,6 @@ public String getDescription() { return "Adds support for legacy SiteMesh 2 based GSP layouts (grails-layout) instead of SiteMesh 3"; } - @Override - public boolean shouldApply(ApplicationType applicationType, Options options, Set selectedFeatures) { - return supports(applicationType) && - options.getGspLayoutImpl() == GspLayoutImpl.GRAILS_LAYOUT; - } - @Override public void apply(GeneratorContext generatorContext) { generatorContext.addDependency(Dependency.builder() diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GspLayout.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GspLayout.java index 1f45ac85511..d818518645f 100644 --- a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GspLayout.java +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GspLayout.java @@ -20,7 +20,6 @@ import org.grails.forge.application.ApplicationType; import org.grails.forge.feature.Category; -import org.grails.forge.feature.DefaultFeature; import org.grails.forge.feature.OneOfFeature; /** @@ -28,12 +27,8 @@ * ({@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). - * - *

The choice is driven by the {@link org.grails.forge.options.GspLayoutImpl} option - * rather than by selecting a feature, so the members are not visible as standalone - * features. Each member applies based on the selected option (see {@code shouldApply}). */ -public abstract class GspLayout implements OneOfFeature, DefaultFeature { +public abstract class GspLayout implements OneOfFeature { @Override public Class getFeatureClass() { @@ -49,9 +44,4 @@ public boolean supports(ApplicationType applicationType) { public String getCategory() { return Category.VIEW; } - - @Override - public boolean isVisible() { - return false; - } } diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/options/FeatureFilter.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/options/FeatureFilter.java index d5191209f6c..a0cc294b4e5 100644 --- a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/options/FeatureFilter.java +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/options/FeatureFilter.java @@ -29,8 +29,6 @@ public class FeatureFilter { private GormImpl gorm; @JsonProperty("servlet") private ServletImpl servlet; - @JsonProperty("gspLayout") - private GspLayoutImpl gspLayout; @JsonProperty("javaVersion") private JdkVersion javaVersion; @@ -58,14 +56,6 @@ public void setServlet(ServletImpl servlet) { this.servlet = servlet; } - public GspLayoutImpl getGspLayout() { - return gspLayout; - } - - public void setGspLayout(GspLayoutImpl gspLayout) { - this.gspLayout = gspLayout; - } - public JdkVersion getJavaVersion() { return javaVersion; } diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/options/GspLayoutImpl.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/options/GspLayoutImpl.java deleted file mode 100644 index 667e0692f42..00000000000 --- a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/options/GspLayoutImpl.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.options; - -import io.micronaut.core.annotation.NonNull; - -/** - * The GSP layout decorator implementation. SiteMesh 3 ({@code grails-sitemesh3}) - * is the default; the legacy SiteMesh 2 based {@code grails-layout} plugin is the - * opt-in alternative. The two are mutually exclusive, so the choice is modelled as - * a single option rather than competing features. - * - * @since 8.0.0 - */ -public enum GspLayoutImpl { - - SITEMESH3("sitemesh3", "SiteMesh 3"), - GRAILS_LAYOUT("grails-layout", "SiteMesh 2"); - - public static final GspLayoutImpl DEFAULT_OPTION = SITEMESH3; - private final String featureName; - private final String label; - - GspLayoutImpl(String featureName, String label) { - this.featureName = featureName; - this.label = label; - } - - @NonNull - public String getName() { - return featureName; - } - - @NonNull - public String getLabel() { - return label; - } -} diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/options/Options.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/options/Options.java index c78c4c8b0bb..b3ab6a01bb3 100644 --- a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/options/Options.java +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/options/Options.java @@ -34,7 +34,6 @@ public class Options implements ConvertibleValues { private final BuildTool buildTool; private final GormImpl gormImpl; private final ServletImpl servletImpl; - private final GspLayoutImpl gspLayoutImpl; private final JdkVersion javaVersion; private final ConvertibleValuesMap additionalOptions; @@ -45,22 +44,10 @@ public Options(DevelopmentReloading reloading, OperatingSystem operatingSystem, Map additionalOptions) { - this(reloading, gormImpl, servletImpl, GspLayoutImpl.DEFAULT_OPTION, javaVersion, operatingSystem, additionalOptions); - } - - public Options(DevelopmentReloading reloading, - GormImpl gormImpl, - ServletImpl servletImpl, - GspLayoutImpl gspLayoutImpl, - JdkVersion javaVersion, - OperatingSystem operatingSystem, - Map additionalOptions) { - this.reloading = reloading; this.buildTool = BuildTool.DEFAULT_OPTION; this.gormImpl = gormImpl; this.servletImpl = servletImpl; - this.gspLayoutImpl = gspLayoutImpl; this.javaVersion = javaVersion; this.operatingSystem = operatingSystem; this.additionalOptions = new ConvertibleValuesMap<>(additionalOptions); @@ -130,10 +117,6 @@ public ServletImpl getServletImpl() { return servletImpl; } - public GspLayoutImpl getGspLayoutImpl() { - return gspLayoutImpl; - } - @Override public Set names() { return additionalOptions.names(); @@ -154,26 +137,22 @@ public JdkVersion getJavaVersion() { } public Options withOperatingSystem(OperatingSystem operatingSystem) { - return new Options(reloading, gormImpl, servletImpl, gspLayoutImpl, javaVersion, operatingSystem, additionalOptions.asMap()); + return new Options(reloading, gormImpl, servletImpl, javaVersion, operatingSystem, additionalOptions.asMap()); } public Options withDevelopmentReloading(DevelopmentReloading reloading) { - return new Options(reloading, gormImpl, servletImpl, gspLayoutImpl, javaVersion, operatingSystem, additionalOptions.asMap()); + return new Options(reloading, gormImpl, servletImpl, javaVersion, operatingSystem, additionalOptions.asMap()); } public Options withGormImpl(GormImpl gormImpl) { - return new Options(reloading, gormImpl, servletImpl, gspLayoutImpl, javaVersion, operatingSystem, additionalOptions.asMap()); + return new Options(reloading, gormImpl, servletImpl, javaVersion, operatingSystem, additionalOptions.asMap()); } public Options withServletImpl(ServletImpl servletImpl) { - return new Options(reloading, gormImpl, servletImpl, gspLayoutImpl, javaVersion, operatingSystem, additionalOptions.asMap()); - } - - public Options withGspLayoutImpl(GspLayoutImpl gspLayoutImpl) { - return new Options(reloading, gormImpl, servletImpl, gspLayoutImpl, javaVersion, operatingSystem, additionalOptions.asMap()); + return new Options(reloading, gormImpl, servletImpl, javaVersion, operatingSystem, additionalOptions.asMap()); } public Options withJavaVersion(JdkVersion javaVersion) { - return new Options(reloading, gormImpl, servletImpl, gspLayoutImpl, javaVersion, operatingSystem, additionalOptions.asMap()); + return new Options(reloading, gormImpl, servletImpl, javaVersion, operatingSystem, additionalOptions.asMap()); } } diff --git a/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/BuildBuilder.groovy b/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/BuildBuilder.groovy index a7838228a32..3a2cfa91fd5 100644 --- a/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/BuildBuilder.groovy +++ b/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/BuildBuilder.groovy @@ -48,7 +48,6 @@ class BuildBuilder implements ProjectFixture, ContextFixture { private JdkVersion jdkVersion private GormImpl gormImpl private ServletImpl servletImpl - private GspLayoutImpl gspLayoutImpl private OperatingSystem operatingSystem private Project project private ApplicationContext ctx @@ -58,7 +57,6 @@ class BuildBuilder implements ProjectFixture, ContextFixture { this.ctx = ctx this.gormImpl = GormImpl.DEFAULT_OPTION this.servletImpl = ServletImpl.DEFAULT_OPTION - this.gspLayoutImpl = GspLayoutImpl.DEFAULT_OPTION this.operatingSystem = OperatingSystem.DEFAULT } @@ -100,11 +98,6 @@ class BuildBuilder implements ProjectFixture, ContextFixture { this } - BuildBuilder gspLayoutImpl(GspLayoutImpl gspLayoutImpl) { - this.gspLayoutImpl = gspLayoutImpl - this - } - BuildBuilder project(Project project) { this.project = project this @@ -118,7 +111,6 @@ class BuildBuilder implements ProjectFixture, ContextFixture { JdkVersion jdkVersion = this.jdkVersion ?: JdkVersion.DEFAULT_OPTION final Options options = new Options(reloading, gormImpl, servletImpl, jdkVersion, operatingSystem) - .withGspLayoutImpl(gspLayoutImpl ?: GspLayoutImpl.DEFAULT_OPTION) Features features = getFeatures(featureNames, options, type) GradleBuild build = gradleBuild(options, features, project, type) diff --git a/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/view/GspLayoutSpec.groovy b/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/view/GspLayoutSpec.groovy index b947f56ddd2..9d6c1e9249d 100644 --- a/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/view/GspLayoutSpec.groovy +++ b/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/view/GspLayoutSpec.groovy @@ -22,7 +22,6 @@ package org.grails.forge.feature.view import org.grails.forge.ApplicationContextSpec import org.grails.forge.BuildBuilder import org.grails.forge.application.ApplicationType -import org.grails.forge.options.GspLayoutImpl import spock.lang.Unroll class GspLayoutSpec extends ApplicationContextSpec { @@ -43,33 +42,27 @@ class GspLayoutSpec extends ApplicationContextSpec { applicationType << [ApplicationType.WEB, ApplicationType.WEB_PLUGIN] } - @Unroll - void "the grails-layout option replaces the default SiteMesh 3 decorator for #applicationType"() { + void "selecting grails-layout replaces the default SiteMesh 3 decorator"() { when: final String build = new BuildBuilder(beanContext) - .features(['gsp']) - .gspLayoutImpl(GspLayoutImpl.GRAILS_LAYOUT) - .applicationType(applicationType) + .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"') - - where: - applicationType << [ApplicationType.WEB, ApplicationType.WEB_PLUGIN] } - void "the sitemesh3 option keeps the default SiteMesh 3 decorator"() { + void "sitemesh3 is the silent default and is not a selectable feature"() { when: - final String build = new BuildBuilder(beanContext) - .features(['gsp']) - .gspLayoutImpl(GspLayoutImpl.SITEMESH3) + new BuildBuilder(beanContext) + .features(['gsp', 'sitemesh3']) .applicationType(ApplicationType.WEB) .render() then: - build.contains('implementation "org.apache.grails:grails-sitemesh3"') - !build.contains('implementation "org.apache.grails:grails-layout"') + IllegalArgumentException e = thrown() + e.message.contains('The requested feature does not exist: sitemesh3') } } From a61691ec379f05964ff299dcdd6a2e1a8cebc5c9 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Wed, 10 Jun 2026 13:37:14 -0600 Subject: [PATCH 14/25] Re-enable the gsp-sitemesh3 test example It was disabled when SiteMesh 3 lacked Spring Boot 4 support, which the filterless integration has since restored. This is the dedicated SiteMesh 3 layout regression app, mirroring the gsp-layout SiteMesh 2 anchor. --- settings.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/settings.gradle b/settings.gradle index 08b68a7feb2..5fe8c308ed0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -437,7 +437,7 @@ include( 'grails-test-examples-geb-gebconfig', 'grails-test-examples-gorm', 'grails-test-examples-gsp-layout', - // TODO: 'grails-test-examples-gsp-sitemesh3', + 'grails-test-examples-gsp-sitemesh3', // TODO: 'grails-test-examples-gsp-spring-boot', 'grails-test-examples-hyphenated', 'grails-test-examples-issue-11102', @@ -475,7 +475,7 @@ project(':grails-test-examples-geb-gebconfig').projectDir = file('grails-test-ex project(':grails-test-examples-namespaces').projectDir = file('grails-test-examples/namespaces') project(':grails-test-examples-gorm').projectDir = file('grails-test-examples/gorm') project(':grails-test-examples-gsp-layout').projectDir = file('grails-test-examples/gsp-layout') -//TODO: project(':grails-test-examples-gsp-sitemesh3').projectDir = file('grails-test-examples/gsp-sitemesh3') +project(':grails-test-examples-gsp-sitemesh3').projectDir = file('grails-test-examples/gsp-sitemesh3') //TODO: project(':grails-test-examples-gsp-spring-boot').projectDir = file('grails-test-examples/gsp-spring-boot/app') project(':grails-test-examples-issue-698-domain-save-npe').projectDir = file('grails-test-examples/issue-698-domain-save-npe') project(':grails-test-examples-hyphenated').projectDir = file('grails-test-examples/hyphenated') From 6e83b250f1c43a14787dd4d9f5edea757a52b394 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Wed, 10 Jun 2026 14:26:41 -0600 Subject: [PATCH 15/25] Fix SiteMesh 3 layout chaining, error-page decoration and JSP rendering Three gaps surfaced by re-enabling the gsp-sitemesh3 example, which tests the layout engine itself rather than just using it: - Nested chains lost the inner layout's title, head and body wrappers: applyLayout overwrote the captured body with the full rendered document and passed the inner chain's decorated output on as raw text. Never overwrite a captured body buffer, and parse uncaptured full-document bodies through the content processor so chained decoration keeps head/title/body, as SiteMesh 2 does. - Error-dispatched views (404/500 pages) rendered undecorated because Sitemesh3LayoutFinder guarded on the composite isRenderView(), which is false for any response status >= 300. Replace the guard with the SiteMesh 2 parity mechanism: a Sitemesh3RenderViewMutator registered as grailsRenderViewMutator that unwraps the SiteMesh view only for "render template:" partials, so error views decorate via their meta layout while partials stay undecorated. - JSP views rendered empty on Tomcat 11 because JstlView forwards and ApplicationDispatcher suspends the wrapped response after the forward, discarding everything SiteMesh writes. Render InternalResourceView inner views via include instead, and honor the JSP's meta layout. Verified: gsp-sitemesh3 integration suite 15/15, grails-sitemesh3 unit tests 32/32 (new mutator/finder/resolver specs), no regressions in the app1 layout/include/template specs or scaffolding-fields embedded fields. --- .../sitemesh3/Sitemesh3GrailsPlugin.groovy | 5 ++ .../web/taglib/RenderSitemeshTagLib.groovy | 52 +++++++++++-- .../sitemesh3/GrailsSiteMeshViewResolver.java | 10 +++ .../sitemesh3/Sitemesh3LayoutFinder.java | 13 ---- .../sitemesh3/Sitemesh3RenderViewMutator.java | 49 +++++++++++++ .../GrailsSiteMeshViewResolverSpec.groovy | 19 +++++ .../Sitemesh3LayoutFinderSpec.groovy | 28 +++++++ .../Sitemesh3RenderViewMutatorSpec.groovy | 73 +++++++++++++++++++ 8 files changed, 228 insertions(+), 21 deletions(-) create mode 100644 grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3RenderViewMutator.java create mode 100644 grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/Sitemesh3RenderViewMutatorSpec.groovy diff --git a/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/sitemesh3/Sitemesh3GrailsPlugin.groovy b/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/sitemesh3/Sitemesh3GrailsPlugin.groovy index 0e2f820bf3c..4e5cb61fb76 100644 --- a/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/sitemesh3/Sitemesh3GrailsPlugin.groovy +++ b/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/sitemesh3/Sitemesh3GrailsPlugin.groovy @@ -103,6 +103,11 @@ class Sitemesh3GrailsPlugin extends Plugin { layoutCacheExpirationMillis = config.getProperty('grails.sitemesh.layout.cache.interval', Long, 5000L) } + // 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) + // Replace the filter registration from // org.sitemesh.autoconfigure.SiteMeshAutoConfiguration with a no-op // filter bean under the same name. SiteMeshAutoConfiguration is diff --git a/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/web/taglib/RenderSitemeshTagLib.groovy b/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/web/taglib/RenderSitemeshTagLib.groovy index e5e6d452b93..0fdfa4d53f0 100644 --- a/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/web/taglib/RenderSitemeshTagLib.groovy +++ b/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/web/taglib/RenderSitemeshTagLib.groovy @@ -18,6 +18,8 @@ */ package org.grails.plugins.web.taglib +import java.nio.CharBuffer + import org.sitemesh.DecoratorSelector import org.sitemesh.SiteMeshContext import org.sitemesh.content.Content @@ -79,11 +81,13 @@ class RenderSitemeshTagLib implements TagLibrary { String savedAttribute = request.getAttribute(WebUtils.LAYOUT_ATTRIBUTE) // Save the request-scoped captured page (the one being decorated by the // outer SiteMesh render) and push a fresh one for the duration of the - // body() render. The body of is plain markup (e.g. the - // grails-fields embedded fieldset content) with no taglibs, - // so its content must be wired into the page explicitly rather than - // relying on capture. Restoring the outer page afterwards prevents the - // body fragment from clobbering the outer page's body/title/properties. + // body() render. The body of may be a full GSP document + // (whose / the compile-time capture taglibs record into the + // fresh page) or plain markup with no capture taglibs at all (e.g. the + // grails-fields embedded fieldset content, or the already-decorated + // output of a nested ). Restoring the outer page + // afterwards prevents the body fragment from clobbering the outer + // page's body/title/properties. Object savedCapturedPage = request.getAttribute(Sitemesh3CapturedPage.REQUEST_ATTRIBUTE) GrailsSiteMeshViewContext context = new GrailsSiteMeshViewContext( 'text/html', request, response, servletContext, @@ -103,7 +107,28 @@ class RenderSitemeshTagLib implements TagLibrary { bodyBuffer = stringWriter.buffer } bodyBuffer.setPreferSubChunkWhenWritingToOtherBuffer(true) - bodyPage.setBodyBuffer(bodyBuffer) + + Content content + if (bodyPage.isUsed()) { + // The body was a full GSP document: the compile-time capture + // taglibs have already populated bodyPage with its + // //<body>. Only fall back to the whole markup + // when no <body> tag was present; never overwrite a captured + // body with the full document (that would nest the document + // inside the layout's <g:layoutBody> output). + if (bodyPage.getBodyBuffer() == null) { + bodyPage.setBodyBuffer(bodyBuffer) + } + content = bodyPage + } else { + // No capture taglib ran while rendering the body: it is raw + // markup — plain text, or the already-decorated output of a + // nested <g:applyLayout>. Parse it so a full HTML document + // contributes its head/title/body to the decoration chain + // (SiteMesh 2 parses in the same situation). Markup without a + // <body> tag falls back to the whole fragment as the body. + content = contentProcessor.build(CharBuffer.wrap(bodyBuffer.toString()), context) + } // Expose <g:applyLayout params="[...]"> entries as page properties so // the layout can read them via <g:pageProperty name="..."/>. This @@ -113,12 +138,11 @@ class RenderSitemeshTagLib implements TagLibrary { if (params) { params.each { k, v -> if (k != null && v != null) { - bodyPage.addProperty(k.toString(), v.toString()) + addContentProperty(content, k.toString(), v.toString()) } } } - Content content = bodyPage if (attrs.name) { request.setAttribute(WebUtils.LAYOUT_ATTRIBUTE, attrs.name) } @@ -154,6 +178,18 @@ class RenderSitemeshTagLib implements TagLibrary { } } + private void addContentProperty(Content content, String name, String value) { + if (content instanceof Sitemesh3CapturedPage) { + ((Sitemesh3CapturedPage) content).addProperty(name, value) + return + } + ContentProperty property = content.getExtractedProperties() + for (String part : name.split('\\.')) { + property = property.getChild(part) + } + property.setValue(value) + } + private ContentProperty getContentProperty(String name) { if (!name) { return null diff --git a/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolver.java b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolver.java index 0c210032e34..b88a016c974 100644 --- a/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolver.java +++ b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolver.java @@ -28,6 +28,7 @@ import org.springframework.web.servlet.View; import org.springframework.web.servlet.ViewResolver; +import org.springframework.web.servlet.view.InternalResourceView; /** * Grails-flavoured {@link SiteMeshViewResolver} that wraps each inner view @@ -51,6 +52,15 @@ public GrailsSiteMeshViewResolver(ViewResolver innerViewResolver, @Override protected SiteMeshView createSiteMeshView(View innerView) { + if (innerView instanceof InternalResourceView) { + // JSP views render through RequestDispatcher.forward() by default. + // Recent Tomcat versions (suspendWrappedResponseAfterForward, on by + // default) suspend the underlying response once a forward returns, + // even when the view was handed SiteMesh's buffering wrapper — so + // the decorated page written afterwards would be discarded. + // Rendering the JSP via include leaves the response usable. + ((InternalResourceView) innerView).setAlwaysInclude(true); + } return new GrailsSiteMeshView(innerView, contentProcessor, decoratorSelector, servletContext, getInnerViewResolver()); } diff --git a/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3LayoutFinder.java b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3LayoutFinder.java index 3e07fbbca75..0c043a51961 100644 --- a/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3LayoutFinder.java +++ b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3LayoutFinder.java @@ -106,19 +106,6 @@ public String[] selectDecoratorPaths(Content content, SiteMeshContext context) { } HttpServletRequest request = ((WebAppContext) context).getRequest(); - // A controller's "render template:" (or "render view:" with renderView - // disabled) renders the GSP directly and sets renderView=false. Such - // partial/template renders must not be decorated with a layout, matching - // SiteMesh 2 behaviour (see GrailsLayoutSelector). When an explicit - // layout was requested (layout: 'x') the LAYOUT_ATTRIBUTE is set and the - // checks below still apply it. - if (request.getAttribute(WebUtils.LAYOUT_ATTRIBUTE) == null) { - GrailsWebRequest webRequest = GrailsWebRequest.lookup(request); - if (webRequest != null && !webRequest.isRenderView()) { - return new String[0]; - } - } - Object layoutAttribute = request.getAttribute(WebUtils.LAYOUT_ATTRIBUTE); String layoutName = layoutAttribute == null ? null : layoutAttribute.toString(); diff --git a/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3RenderViewMutator.java b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3RenderViewMutator.java new file mode 100644 index 00000000000..1e307064ef8 --- /dev/null +++ b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3RenderViewMutator.java @@ -0,0 +1,49 @@ +/* + * 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.plugins.sitemesh3; + +import java.util.Locale; + +import org.sitemesh.webmvc.SiteMeshView; + +import org.springframework.web.servlet.View; + +import grails.web.pages.GrailsRenderViewMutator; + +/** + * Unwraps the SiteMesh decorating view for partial renders. A controller's + * {@code render template:} resolves its view through the same view-resolver + * chain as full views, so the resolved view arrives wrapped in a + * {@link SiteMeshView}. Partials must not be decorated with a layout (matching + * the SiteMesh 2 plugin's {@code GrailsLayoutRenderViewMutator}, which unwraps + * its {@code EmbeddedGrailsLayoutView} in the same situation), so the inner + * view is rendered directly. When an explicit layout was requested + * ({@code render template: 'x', layout: 'y'}) the wrapping is kept and the + * layout is applied as usual. + */ +public class Sitemesh3RenderViewMutator implements GrailsRenderViewMutator { + + @Override + public View mutateView(boolean renderWithLayout, String templateUri, Locale locale, View existingView) { + if (!renderWithLayout && existingView instanceof SiteMeshView) { + return ((SiteMeshView) existingView).getInnerView(); + } + return existingView; + } +} diff --git a/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolverSpec.groovy b/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolverSpec.groovy index b4f347d05b6..e564833e42f 100644 --- a/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolverSpec.groovy +++ b/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolverSpec.groovy @@ -26,8 +26,11 @@ import org.sitemesh.DecoratorSelector import org.sitemesh.SiteMeshContext import org.sitemesh.content.ContentProcessor +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse import org.springframework.web.servlet.View import org.springframework.web.servlet.ViewResolver +import org.springframework.web.servlet.view.InternalResourceView import org.springframework.web.servlet.view.RedirectView import spock.lang.Specification @@ -79,6 +82,22 @@ class GrailsSiteMeshViewResolverSpec extends Specification { result.is(redirect) } + void "JSP inner views are rendered via include so the response survives decoration"() { + given: 'a JSP view resolved through the SiteMesh resolver' + InternalResourceView jspView = new InternalResourceView('/WEB-INF/grails-app/views/demo/hello.jsp') + inner.resolveViewName('/demo/hello', Locale.ENGLISH) >> jspView + MockHttpServletRequest request = new MockHttpServletRequest() + MockHttpServletResponse response = new MockHttpServletResponse() + + when: + View result = resolver().resolveViewName('/demo/hello', Locale.ENGLISH) + result.render([:], request, response) + + then: 'the JSP is dispatched with include semantics, not a forward' + response.includedUrl == '/WEB-INF/grails-app/views/demo/hello.jsp' + response.forwardedUrl == null + } + void "null inner view yields null"() { given: inner.resolveViewName('/missing', Locale.ENGLISH) >> null diff --git a/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/Sitemesh3LayoutFinderSpec.groovy b/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/Sitemesh3LayoutFinderSpec.groovy index 5f443ca1b3d..8c58d2ed323 100644 --- a/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/Sitemesh3LayoutFinderSpec.groovy +++ b/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/Sitemesh3LayoutFinderSpec.groovy @@ -169,6 +169,34 @@ class Sitemesh3LayoutFinderSpec extends Specification { paths.length == 0 } + void 'NONE layout suppresses decoration'() { + given: + request.setAttribute(WebUtils.LAYOUT_ATTRIBUTE, WebUtils.NONE_LAYOUT) + Content content = contentWithMetaLayout('ignored') + + when: + String[] paths = finder.selectDecoratorPaths(content, context) + + then: + 0 * locator.findViewByPath(_) + paths.length == 0 + } + + void 'meta layout still applies when the response carries an error status'() { + given: 'an error-dispatched render (e.g. a custom 404 page) bound to the request' + GrailsWebRequest webRequest = new GrailsWebRequest(request, response, servletContext) + RequestContextHolder.setRequestAttributes(webRequest) + response.setStatus(404) + Content content = contentWithMetaLayout('main') + + when: + String[] paths = finder.selectDecoratorPaths(content, context) + + then: + 1 * locator.findViewByPath('/layouts/main') >> Mock(GroovyPageScriptSource) + paths == ['/layouts/main'] as String[] + } + private Content contentWithMetaLayout(String layout) { Content content = new InMemoryContent() ContentProperty root = content.getExtractedProperties() diff --git a/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/Sitemesh3RenderViewMutatorSpec.groovy b/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/Sitemesh3RenderViewMutatorSpec.groovy new file mode 100644 index 00000000000..e8751917d5d --- /dev/null +++ b/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/Sitemesh3RenderViewMutatorSpec.groovy @@ -0,0 +1,73 @@ +/* + * 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.plugins.sitemesh3 + +import java.util.Locale + +import jakarta.servlet.ServletContext + +import org.sitemesh.DecoratorSelector +import org.sitemesh.SiteMeshContext +import org.sitemesh.content.ContentProcessor + +import org.springframework.web.servlet.View +import org.springframework.web.servlet.ViewResolver + +import spock.lang.Specification + +class Sitemesh3RenderViewMutatorSpec extends Specification { + + Sitemesh3RenderViewMutator mutator = new Sitemesh3RenderViewMutator() + View innerView = Mock(View) + + GrailsSiteMeshView siteMeshView() { + new GrailsSiteMeshView(innerView, Mock(ContentProcessor), + Mock(DecoratorSelector<SiteMeshContext>), Mock(ServletContext), Mock(ViewResolver)) + } + + void 'unwraps the SiteMesh view for partial renders without an explicit layout'() { + when: + View result = mutator.mutateView(false, '/book/_details', Locale.ENGLISH, siteMeshView()) + + then: 'render template: partials are not decorated' + result.is(innerView) + } + + void 'keeps the SiteMesh view when an explicit layout was requested'() { + given: + GrailsSiteMeshView wrapped = siteMeshView() + + when: + View result = mutator.mutateView(true, '/book/_details', Locale.ENGLISH, wrapped) + + then: 'render template: x, layout: y is still decorated' + result.is(wrapped) + } + + void 'passes through views that are not SiteMesh-wrapped'() { + given: + View plain = Mock(View) + + when: + View result = mutator.mutateView(false, '/book/_details', Locale.ENGLISH, plain) + + then: + result.is(plain) + } +} From e7390ac489cc629ddf71ae3deeb7117e2831573d Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg <scott@alwaysvip.com> Date: Wed, 10 Jun 2026 19:27:13 -0600 Subject: [PATCH 16/25] Drop SiteMesh 3 plugin workarounds superseded by the updated starter The refreshed org.sitemesh 3.3.0-SNAPSHOT defaults the Boot starter to the view-resolver integration (the servlet filter is now opt-in via sitemesh.integration=filter, and the starter registers its own disabled "sitemesh" guard bean), and SiteMeshViewResolver now switches forward-based JSP inner views to include dispatch itself, keyed on DispatchMode's container detection. Remove the plugin pieces that existed to compensate: - the NoopSitemeshFilter and its "sitemesh" FilterRegistrationBean, which suppressed the upstream filter auto-configuration - Sitemesh3EnvironmentPostProcessor and its spring.factories/imports registrations, which forced sitemesh.integration and wrap-mode defaults; Sitemesh3AutoConfiguration now uses matchIfMissing instead - the InternalResourceView alwaysInclude special case in GrailsSiteMeshViewResolver, now handled upstream before the createSiteMeshView hook; the resolver spec stubs Tomcat 11 server info to verify the inherited behavior Verified against the updated snapshot: grails-sitemesh3 unit tests and codeStyle, gsp-sitemesh3 integration suite 15/15 (including the JSP demo on Tomcat 11), app1 layout/include/template specs and scaffolding-fields embedded fields all green. --- .../sitemesh3/Sitemesh3GrailsPlugin.groovy | 31 -------- .../sitemesh3/GrailsSiteMeshViewResolver.java | 13 +--- .../sitemesh3/Sitemesh3AutoConfiguration.java | 2 +- .../Sitemesh3EnvironmentPostProcessor.java | 70 ------------------ .../main/resources/META-INF/spring.factories | 2 - ....boot.env.EnvironmentPostProcessor.imports | 1 - .../GrailsSiteMeshViewResolverSpec.groovy | 9 ++- ...temesh3EnvironmentPostProcessorSpec.groovy | 71 ------------------- 8 files changed, 10 insertions(+), 189 deletions(-) delete mode 100644 grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3EnvironmentPostProcessor.java delete mode 100644 grails-gsp/grails-sitemesh3/src/main/resources/META-INF/spring.factories delete mode 100644 grails-gsp/grails-sitemesh3/src/main/resources/META-INF/spring/org.springframework.boot.env.EnvironmentPostProcessor.imports delete mode 100644 grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/Sitemesh3EnvironmentPostProcessorSpec.groovy diff --git a/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/sitemesh3/Sitemesh3GrailsPlugin.groovy b/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/sitemesh3/Sitemesh3GrailsPlugin.groovy index 4e5cb61fb76..b8d09a7b708 100644 --- a/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/sitemesh3/Sitemesh3GrailsPlugin.groovy +++ b/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/sitemesh3/Sitemesh3GrailsPlugin.groovy @@ -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 @@ -107,30 +100,6 @@ class Sitemesh3GrailsPlugin extends Plugin { // they are never decorated with a layout (the SiteMesh 2 plugin // does the same with its GrailsLayoutRenderViewMutator). grailsRenderViewMutator(Sitemesh3RenderViewMutator) - - // 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) } } } diff --git a/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolver.java b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolver.java index b88a016c974..a537f3df971 100644 --- a/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolver.java +++ b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolver.java @@ -28,7 +28,6 @@ import org.springframework.web.servlet.View; import org.springframework.web.servlet.ViewResolver; -import org.springframework.web.servlet.view.InternalResourceView; /** * Grails-flavoured {@link SiteMeshViewResolver} that wraps each inner view @@ -52,15 +51,9 @@ public GrailsSiteMeshViewResolver(ViewResolver innerViewResolver, @Override protected SiteMeshView createSiteMeshView(View innerView) { - if (innerView instanceof InternalResourceView) { - // JSP views render through RequestDispatcher.forward() by default. - // Recent Tomcat versions (suspendWrappedResponseAfterForward, on by - // default) suspend the underlying response once a forward returns, - // even when the view was handed SiteMesh's buffering wrapper — so - // the decorated page written afterwards would be discarded. - // Rendering the JSP via include leaves the response usable. - ((InternalResourceView) innerView).setAlwaysInclude(true); - } + // Forward-based JSP inner views are switched to include dispatch by + // SiteMeshViewResolver.prepareForBufferedRender (keyed on + // DispatchMode) before this hook runs. return new GrailsSiteMeshView(innerView, contentProcessor, decoratorSelector, servletContext, getInnerViewResolver()); } diff --git a/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3AutoConfiguration.java b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3AutoConfiguration.java index bdef7c450c8..fb0f87e558e 100644 --- a/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3AutoConfiguration.java +++ b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3AutoConfiguration.java @@ -38,7 +38,7 @@ @AutoConfiguration @AutoConfigureBefore(name = "org.sitemesh.autoconfigure.SiteMeshViewResolverAutoConfiguration") @ConditionalOnClass(SiteMeshViewResolverBeanPostProcessor.class) -@ConditionalOnProperty(name = "sitemesh.integration", havingValue = "view-resolver") +@ConditionalOnProperty(name = "sitemesh.integration", havingValue = "view-resolver", matchIfMissing = true) public class Sitemesh3AutoConfiguration { @Bean diff --git a/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3EnvironmentPostProcessor.java b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3EnvironmentPostProcessor.java deleted file mode 100644 index b43dfa88dcb..00000000000 --- a/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3EnvironmentPostProcessor.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.plugins.sitemesh3; - -import java.util.HashMap; -import java.util.Map; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.env.EnvironmentPostProcessor; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.MapPropertySource; - -/** - * Seeds default properties consumed by the upstream - * {@code SiteMeshViewResolverAutoConfiguration}: - * - * <ul> - * <li>{@code sitemesh.integration=view-resolver} — activates the Spring - * MVC {@code ViewResolver} integration instead of the servlet-filter - * integration.</li> - * <li>{@code sitemesh.viewResolver.wrapMode=bean-instance} — selects - * the live-bean {@code SiteMeshViewResolverBeanPostProcessor}. The - * default {@code bean-definition} variant cannot find - * {@code gspViewResolver} because its bean definition is registered - * after {@code BeanDefinitionRegistryPostProcessors} fire.</li> - * <li>{@code sitemesh.viewResolver.targetBeanName=gspViewResolver} — - * tells the post-processor which view resolver to wrap (the default is - * {@code jspViewResolver}).</li> - * </ul> - * - * <p>Each value is only set when absent so an application can opt-out by - * explicitly setting any of these properties.</p> - */ -public class Sitemesh3EnvironmentPostProcessor implements EnvironmentPostProcessor { - - public static final String PROPERTY_SOURCE_NAME = "grailsSitemesh3Defaults"; - - @Override - public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { - Map<String, Object> props = new HashMap<>(); - if (environment.getProperty("sitemesh.integration") == null) { - props.put("sitemesh.integration", "view-resolver"); - } - if (environment.getProperty("sitemesh.viewResolver.wrapMode") == null) { - props.put("sitemesh.viewResolver.wrapMode", "bean-instance"); - } - if (environment.getProperty("sitemesh.viewResolver.targetBeanName") == null) { - props.put("sitemesh.viewResolver.targetBeanName", "jspViewResolver"); - } - if (!props.isEmpty()) { - environment.getPropertySources().addLast(new MapPropertySource(PROPERTY_SOURCE_NAME, props)); - } - } -} diff --git a/grails-gsp/grails-sitemesh3/src/main/resources/META-INF/spring.factories b/grails-gsp/grails-sitemesh3/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 6446d21ad76..00000000000 --- a/grails-gsp/grails-sitemesh3/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,2 +0,0 @@ -org.springframework.boot.env.EnvironmentPostProcessor=\ -org.grails.plugins.sitemesh3.Sitemesh3EnvironmentPostProcessor diff --git a/grails-gsp/grails-sitemesh3/src/main/resources/META-INF/spring/org.springframework.boot.env.EnvironmentPostProcessor.imports b/grails-gsp/grails-sitemesh3/src/main/resources/META-INF/spring/org.springframework.boot.env.EnvironmentPostProcessor.imports deleted file mode 100644 index e4fe91c2486..00000000000 --- a/grails-gsp/grails-sitemesh3/src/main/resources/META-INF/spring/org.springframework.boot.env.EnvironmentPostProcessor.imports +++ /dev/null @@ -1 +0,0 @@ -org.grails.plugins.sitemesh3.Sitemesh3EnvironmentPostProcessor diff --git a/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolverSpec.groovy b/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolverSpec.groovy index e564833e42f..7d5903662ae 100644 --- a/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolverSpec.groovy +++ b/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolverSpec.groovy @@ -82,14 +82,17 @@ class GrailsSiteMeshViewResolverSpec extends Specification { result.is(redirect) } - void "JSP inner views are rendered via include so the response survives decoration"() { - given: 'a JSP view resolved through the SiteMesh resolver' + void "JSP inner views are rendered via include on containers where forward is unsafe"() { + given: 'a container where forward dispatch suspends the buffered response' + servletContext.getServerInfo() >> 'Apache Tomcat/11.0.5' + + and: 'a JSP view resolved through the SiteMesh resolver' InternalResourceView jspView = new InternalResourceView('/WEB-INF/grails-app/views/demo/hello.jsp') inner.resolveViewName('/demo/hello', Locale.ENGLISH) >> jspView MockHttpServletRequest request = new MockHttpServletRequest() MockHttpServletResponse response = new MockHttpServletResponse() - when: + when: 'the view is resolved (upstream prepareForBufferedRender runs) and rendered' View result = resolver().resolveViewName('/demo/hello', Locale.ENGLISH) result.render([:], request, response) diff --git a/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/Sitemesh3EnvironmentPostProcessorSpec.groovy b/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/Sitemesh3EnvironmentPostProcessorSpec.groovy deleted file mode 100644 index a0d3ac6fe59..00000000000 --- a/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/Sitemesh3EnvironmentPostProcessorSpec.groovy +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.plugins.sitemesh3 - -import org.springframework.boot.SpringApplication -import org.springframework.core.env.MapPropertySource -import org.springframework.core.env.MutablePropertySources -import org.springframework.mock.env.MockEnvironment - -import spock.lang.Specification - -class Sitemesh3EnvironmentPostProcessorSpec extends Specification { - - Sitemesh3EnvironmentPostProcessor pp = new Sitemesh3EnvironmentPostProcessor() - - void "seeds sitemesh defaults when nothing is configured"() { - given: - MockEnvironment env = new MockEnvironment() - - when: - pp.postProcessEnvironment(env, new SpringApplication()) - - then: - env.getProperty('sitemesh.integration') == 'view-resolver' - env.getProperty('sitemesh.viewResolver.wrapMode') == 'bean-instance' - env.getProperty('sitemesh.viewResolver.targetBeanName') == 'jspViewResolver' - } - - void "respects existing user values"() { - given: - MockEnvironment env = new MockEnvironment() - env.setProperty('sitemesh.integration', 'filter') - env.setProperty('sitemesh.viewResolver.wrapMode', 'bean-definition') - env.setProperty('sitemesh.viewResolver.targetBeanName', 'myResolver') - - when: - pp.postProcessEnvironment(env, new SpringApplication()) - - then: 'user values win' - env.getProperty('sitemesh.integration') == 'filter' - env.getProperty('sitemesh.viewResolver.wrapMode') == 'bean-definition' - env.getProperty('sitemesh.viewResolver.targetBeanName') == 'myResolver' - } - - void "registered property source has the expected name"() { - given: - MockEnvironment env = new MockEnvironment() - - when: - pp.postProcessEnvironment(env, new SpringApplication()) - - then: - env.getPropertySources().contains(Sitemesh3EnvironmentPostProcessor.PROPERTY_SOURCE_NAME) - } -} From 53ee6ca3539af1d350054968d247017e0cf04054 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg <scott@alwaysvip.com> Date: Wed, 10 Jun 2026 23:09:47 -0700 Subject: [PATCH 17/25] Fix stale SiteMesh toggle in the hibernate7 test examples The hibernate7 examples were cloned from the hibernate5 tree before the SiteMesh toggle was inverted, so they still gated on the removed SITEMESH3_TESTING_ENABLED variable and silently defaulted to SiteMesh 2. Bring them in line with the other examples: grails-sitemesh3 by default, grails-layout via SITEMESH2_TESTING_ENABLED. --- .../hibernate7/grails-database-per-tenant/build.gradle | 7 +++---- .../hibernate7/grails-hibernate/build.gradle | 7 +++---- .../grails-partitioned-multi-tenancy/build.gradle | 7 +++---- .../hibernate7/grails-schema-per-tenant/build.gradle | 7 +++---- grails-test-examples/hibernate7/issue450/build.gradle | 7 +++---- 5 files changed, 15 insertions(+), 20 deletions(-) diff --git a/grails-test-examples/hibernate7/grails-database-per-tenant/build.gradle b/grails-test-examples/hibernate7/grails-database-per-tenant/build.gradle index 0c0a2631021..19dfd2f926a 100644 --- a/grails-test-examples/hibernate7/grails-database-per-tenant/build.gradle +++ b/grails-test-examples/hibernate7/grails-database-per-tenant/build.gradle @@ -36,11 +36,10 @@ dependencies { implementation 'org.apache.grails:grails-core' implementation 'org.apache.grails:grails-rest-transforms' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' - } - else { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' + } else { + implementation 'org.apache.grails:grails-sitemesh3' } testAndDevelopmentOnly platform(project(':grails-hibernate7-bom')) diff --git a/grails-test-examples/hibernate7/grails-hibernate/build.gradle b/grails-test-examples/hibernate7/grails-hibernate/build.gradle index 4062d966805..cb532715160 100644 --- a/grails-test-examples/hibernate7/grails-hibernate/build.gradle +++ b/grails-test-examples/hibernate7/grails-hibernate/build.gradle @@ -38,11 +38,10 @@ dependencies { implementation 'org.apache.grails:grails-core' implementation 'org.apache.grails:grails-rest-transforms' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' - } - else { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' + } else { + implementation 'org.apache.grails:grails-sitemesh3' } testAndDevelopmentOnly platform(project(':grails-hibernate7-bom')) diff --git a/grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/build.gradle b/grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/build.gradle index 30b26e46369..d206efe0103 100644 --- a/grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/build.gradle +++ b/grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/build.gradle @@ -36,11 +36,10 @@ dependencies { implementation 'org.apache.grails:grails-core' implementation 'org.apache.grails:grails-rest-transforms' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' - } - else { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' + } else { + implementation 'org.apache.grails:grails-sitemesh3' } testAndDevelopmentOnly platform(project(':grails-hibernate7-bom')) diff --git a/grails-test-examples/hibernate7/grails-schema-per-tenant/build.gradle b/grails-test-examples/hibernate7/grails-schema-per-tenant/build.gradle index 7be36bd6c05..a7c6b8c0cc3 100644 --- a/grails-test-examples/hibernate7/grails-schema-per-tenant/build.gradle +++ b/grails-test-examples/hibernate7/grails-schema-per-tenant/build.gradle @@ -36,11 +36,10 @@ dependencies { implementation 'org.apache.grails:grails-core' implementation 'org.apache.grails:grails-rest-transforms' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' - } - else { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' + } else { + implementation 'org.apache.grails:grails-sitemesh3' } testAndDevelopmentOnly platform(project(':grails-hibernate7-bom')) diff --git a/grails-test-examples/hibernate7/issue450/build.gradle b/grails-test-examples/hibernate7/issue450/build.gradle index ba153ea3bbc..1a2f4295008 100644 --- a/grails-test-examples/hibernate7/issue450/build.gradle +++ b/grails-test-examples/hibernate7/issue450/build.gradle @@ -36,11 +36,10 @@ dependencies { implementation 'org.apache.grails:grails-core' implementation 'org.apache.grails:grails-rest-transforms' implementation 'org.apache.grails:grails-gsp' - if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { - implementation 'org.apache.grails:grails-sitemesh3' - } - else { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' + } else { + implementation 'org.apache.grails:grails-sitemesh3' } testAndDevelopmentOnly platform(project(':grails-hibernate7-bom')) From 3589ba6d0f032b09da9e586971a4d2228a4d03b6 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg <scott@alwaysvip.com> Date: Wed, 10 Jun 2026 23:09:47 -0700 Subject: [PATCH 18/25] Don't mis-slice head content around title-prefixed elements Sitemesh3CapturedPage.extractHead matched "<title" as a bare prefix, so an element such as <titlebar> or <title-x> appearing before the real title made the strip start at the wrong tag and delete everything up to the real , silently dropping head content. Require the character after the prefix to terminate the tag name ('>' or whitespace) and keep scanning otherwise. Covered by a new Sitemesh3CapturedPageSpec. --- .../sitemesh3/Sitemesh3CapturedPage.java | 26 ++++++- .../Sitemesh3CapturedPageSpec.groovy | 77 +++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/Sitemesh3CapturedPageSpec.groovy diff --git a/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3CapturedPage.java b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3CapturedPage.java index e05736dea5b..47bb18bf923 100644 --- a/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3CapturedPage.java +++ b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3CapturedPage.java @@ -229,7 +229,7 @@ private CharSequence extractHead() { if (!titleCaptured) { return head; } - int titleStart = indexOfIgnoreCase(head, " open tag. A bare " or and mis-slice the head, so the character + // following the prefix must terminate the tag name ('>' or + // whitespace before attributes). + private static int indexOfTitleOpenTag(CharSequence head) { + int len = head.length(); + int from = 0; + while (true) { + int i = indexOfIgnoreCase(head, "' || Character.isWhitespace(c)) { + return i; + } + } + from = i + 1; + } + } + private static int indexOfIgnoreCase(CharSequence seq, String needle, int fromIndex) { int needleLen = needle.length(); int max = seq.length() - needleLen; diff --git a/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/Sitemesh3CapturedPageSpec.groovy b/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/Sitemesh3CapturedPageSpec.groovy new file mode 100644 index 00000000000..352be20b6ce --- /dev/null +++ b/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/Sitemesh3CapturedPageSpec.groovy @@ -0,0 +1,77 @@ +/* + * 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.plugins.sitemesh3 + +import org.grails.buffer.FastStringWriter +import org.grails.buffer.StreamCharBuffer + +import spock.lang.Specification + +class Sitemesh3CapturedPageSpec extends Specification { + + void "strips the captured title element from the head"() { + given: + Sitemesh3CapturedPage page = pageWithHead('My Page') + + expect: + headOf(page) == '' + } + + void "strips a title element that carries attributes"() { + given: + Sitemesh3CapturedPage page = pageWithHead('My Page') + + expect: + headOf(page) == '' + } + + void "elements whose names merely start with title are not treated as the title"() { + given: 'a custom element before the real title' + Sitemesh3CapturedPage page = pageWithHead('toolsMy Page') + + expect: 'only the real title is removed' + headOf(page) == 'tools' + } + + void "a head with only title-prefixed custom elements is left untouched"() { + given: + Sitemesh3CapturedPage page = pageWithHead('tools') + + expect: + headOf(page) == 'tools' + } + + private Sitemesh3CapturedPage pageWithHead(String head) { + Sitemesh3CapturedPage page = new Sitemesh3CapturedPage() + page.setHeadBuffer(bufferOf(head)) + page.setTitleBuffer(bufferOf('My Page')) + page.setTitleCaptured(true) + page + } + + private String headOf(Sitemesh3CapturedPage page) { + page.getExtractedProperties().getChild('head').value + } + + private StreamCharBuffer bufferOf(String value) { + FastStringWriter writer = new FastStringWriter() + writer.print(value) + writer.buffer + } +} From 751a9371537854f32caf26863004273730c9076d Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Wed, 10 Jun 2026 23:36:23 -0700 Subject: [PATCH 19/25] Match SiteMesh 2's default layout config and application fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sitemesh3LayoutFinder only consulted the configured default layout and returned nothing when unset, while SiteMesh 2's GroovyPageLayoutFinder falls back to the implicit "application" layout — so apps relying on layouts/application.gsp silently lost decoration when switching. The finder now mirrors SiteMesh 2 exactly: the configured default is used as-is, and only when none is configured is the implicit application layout tried (a configured-but-missing default does not additionally fall back). The plugin also honors SiteMesh 2's grails.views.layout.default config key when the SiteMesh 3 specific grails.sitemesh.default.layout is unset. --- .../sitemesh3/Sitemesh3GrailsPlugin.groovy | 6 +++- .../sitemesh3/Sitemesh3LayoutFinder.java | 20 ++++++++--- .../Sitemesh3LayoutFinderSpec.groovy | 35 ++++++++++++++++++- 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/sitemesh3/Sitemesh3GrailsPlugin.groovy b/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/sitemesh3/Sitemesh3GrailsPlugin.groovy index b8d09a7b708..9e7fcfba771 100644 --- a/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/sitemesh3/Sitemesh3GrailsPlugin.groovy +++ b/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/sitemesh3/Sitemesh3GrailsPlugin.groovy @@ -73,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) diff --git a/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3LayoutFinder.java b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3LayoutFinder.java index 0c043a51961..72482f52b01 100644 --- a/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3LayoutFinder.java +++ b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/Sitemesh3LayoutFinder.java @@ -55,7 +55,9 @@ *
  • Controller's {@code static layout = 'x'} property
  • *
  • {@code /layouts//.gsp}
  • *
  • {@code /layouts/.gsp}
  • - *
  • Configured default (e.g. {@code grails.sitemesh.default.layout})
  • + *
  • The configured default, or — when none is configured — the + * implicit {@code application} layout, matching the SiteMesh 2 + * plugin's GroovyPageLayoutFinder
  • * * *

    Results are cached by (controllerName, actionUri) outside of the @@ -102,7 +104,7 @@ public void setLayoutCacheExpirationMillis(long layoutCacheExpirationMillis) { @Override public String[] selectDecoratorPaths(Content content, SiteMeshContext context) { if (!(context instanceof WebAppContext)) { - return toArray(resolveByName(defaultDecoratorName)); + return toArray(resolveDefaultDecorator()); } HttpServletRequest request = ((WebAppContext) context).getRequest(); @@ -119,7 +121,7 @@ public String[] selectDecoratorPaths(Content content, SiteMeshContext context) { GroovyObject controller = (GroovyObject) request.getAttribute(GrailsApplicationAttributes.CONTROLLER); if (controller == null) { - return toArray(resolveByName(defaultDecoratorName)); + return toArray(resolveDefaultDecorator()); } GrailsWebRequest webRequest = GrailsWebRequest.lookup(request); @@ -130,7 +132,7 @@ public String[] selectDecoratorPaths(Content content, SiteMeshContext context) { String actionUri = webRequest != null ? webRequest.getAttributes().getControllerActionUri(request) : null; if (controllerName == null || actionUri == null) { - return toArray(resolveByName(defaultDecoratorName)); + return toArray(resolveDefaultDecorator()); } LayoutCacheKey cacheKey = null; @@ -176,7 +178,15 @@ private String resolveByConvention(GroovyObject controller, String controllerNam if (resolved != null) { return resolved; } - return resolveByName(defaultDecoratorName); + return resolveDefaultDecorator(); + } + + // Mirrors SiteMesh 2's GroovyPageLayoutFinder: when no default layout is + // configured the implicit "application" layout is tried; a configured + // default is used as-is (and does NOT additionally fall back to + // "application" when missing). + private String resolveDefaultDecorator() { + return resolveByName(defaultDecoratorName != null ? defaultDecoratorName : "application"); } private String resolveByName(String name) { diff --git a/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/Sitemesh3LayoutFinderSpec.groovy b/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/Sitemesh3LayoutFinderSpec.groovy index 8c58d2ed323..b8c7ab6dfaa 100644 --- a/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/Sitemesh3LayoutFinderSpec.groovy +++ b/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/Sitemesh3LayoutFinderSpec.groovy @@ -155,7 +155,7 @@ class Sitemesh3LayoutFinderSpec extends Specification { paths == ['/layouts/application'] as String[] } - void 'returns empty when nothing matches and no default'() { + void 'returns empty when nothing matches, no default and no application layout'() { given: bindController(new ConventionController(), 'sample', '/sample/edit') Content content = emptyContent() @@ -166,6 +166,39 @@ class Sitemesh3LayoutFinderSpec extends Specification { then: 1 * locator.findViewByPath('/layouts/sample/edit') >> null 1 * locator.findViewByPath('/layouts/sample') >> null + 1 * locator.findViewByPath('/layouts/application') >> null + paths.length == 0 + } + + void 'falls back to the implicit application layout when no default is configured'() { + given: + bindController(new ConventionController(), 'sample', '/sample/edit') + Content content = emptyContent() + + when: + String[] paths = finder.selectDecoratorPaths(content, context) + + then: + 1 * locator.findViewByPath('/layouts/sample/edit') >> null + 1 * locator.findViewByPath('/layouts/sample') >> null + 1 * locator.findViewByPath('/layouts/application') >> Mock(GroovyPageScriptSource) + paths == ['/layouts/application'] as String[] + } + + void 'a configured default that is missing does not fall back to the application layout'() { + given: 'SiteMesh 2 parity: the explicit default replaces the implicit fallback' + finder.defaultDecoratorName = 'custom' + bindController(new ConventionController(), 'sample', '/sample/edit') + Content content = emptyContent() + + when: + String[] paths = finder.selectDecoratorPaths(content, context) + + then: + 1 * locator.findViewByPath('/layouts/sample/edit') >> null + 1 * locator.findViewByPath('/layouts/sample') >> null + 1 * locator.findViewByPath('/layouts/custom') >> null + 0 * locator.findViewByPath('/layouts/application') paths.length == 0 } From 159f8125972097323ade87777c0033d32c145935 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Wed, 10 Jun 2026 23:36:23 -0700 Subject: [PATCH 20/25] Run the jetty and scaffolding examples against SiteMesh 3 by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both were hardcoded to grails-layout as SiteMesh 2 anchors. With the gsp-layout example serving as the dedicated SiteMesh 2 regression anchor, switch them to the standard SITEMESH2_TESTING_ENABLED toggle so the default build exercises SiteMesh 3 — notably adding the only Jetty-with-SiteMesh-3 integration coverage in the repository. --- grails-test-examples/jetty/build.gradle | 6 +++++- grails-test-examples/scaffolding/build.gradle | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/grails-test-examples/jetty/build.gradle b/grails-test-examples/jetty/build.gradle index 036352bf585..dd36e268aa8 100644 --- a/grails-test-examples/jetty/build.gradle +++ b/grails-test-examples/jetty/build.gradle @@ -38,7 +38,11 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-jetty' implementation 'org.apache.grails:grails-web-boot' - implementation 'org.apache.grails:grails-layout' + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' + } else { + implementation 'org.apache.grails:grails-sitemesh3' + } implementation 'org.apache.grails:grails-logging' implementation 'org.apache.grails:grails-rest-transforms' implementation 'org.apache.grails:grails-databinding' diff --git a/grails-test-examples/scaffolding/build.gradle b/grails-test-examples/scaffolding/build.gradle index a1b19ee82c0..a01ca8edf8c 100644 --- a/grails-test-examples/scaffolding/build.gradle +++ b/grails-test-examples/scaffolding/build.gradle @@ -46,7 +46,11 @@ dependencies { implementation "org.apache.grails:grails-databinding" implementation "org.apache.grails:grails-services" implementation "org.apache.grails:grails-url-mappings" - implementation "org.apache.grails:grails-layout" + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-layout' + } else { + implementation 'org.apache.grails:grails-sitemesh3' + } implementation "org.apache.grails:grails-interceptors" implementation "org.apache.grails:grails-scaffolding" implementation "org.apache.grails:grails-data-hibernate5" From 8e6a989fce9c6c86a0d547baa49f49f8a54aa0b2 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Wed, 10 Jun 2026 23:47:35 -0700 Subject: [PATCH 21/25] Publish grails-gsp-spring-boot and re-enable its example The module exists to let plain Spring Boot applications use GSP (with SiteMesh 3 layouts and JSP taglib support), so it must ship: add it to the published projects list. Re-enable the gsp-spring-boot test example so the standalone Boot + GSP combination is at least compiled and GSP-precompiled by the default build; the example currently carries no tests, so functional coverage remains a follow-up. Verified: grails-gsp-spring-boot publishToMavenLocal produces the artifact, and the re-enabled example builds cleanly. --- gradle/publish-root-config.gradle | 1 + settings.gradle | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/gradle/publish-root-config.gradle b/gradle/publish-root-config.gradle index 83d3a767784..123bafc419f 100644 --- a/gradle/publish-root-config.gradle +++ b/gradle/publish-root-config.gradle @@ -64,6 +64,7 @@ def publishedProjects = [ 'grails-geb', 'grails-gsp', 'grails-gsp-core', + 'grails-gsp-spring-boot', 'grails-i18n', 'grails-interceptors', 'grails-layout', diff --git a/settings.gradle b/settings.gradle index 5fe8c308ed0..866415b493c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -438,7 +438,7 @@ include( 'grails-test-examples-gorm', 'grails-test-examples-gsp-layout', 'grails-test-examples-gsp-sitemesh3', - // TODO: 'grails-test-examples-gsp-spring-boot', + 'grails-test-examples-gsp-spring-boot', 'grails-test-examples-hyphenated', 'grails-test-examples-issue-11102', 'grails-test-examples-issue-15228', @@ -476,7 +476,7 @@ project(':grails-test-examples-namespaces').projectDir = file('grails-test-examp project(':grails-test-examples-gorm').projectDir = file('grails-test-examples/gorm') project(':grails-test-examples-gsp-layout').projectDir = file('grails-test-examples/gsp-layout') project(':grails-test-examples-gsp-sitemesh3').projectDir = file('grails-test-examples/gsp-sitemesh3') -//TODO: project(':grails-test-examples-gsp-spring-boot').projectDir = file('grails-test-examples/gsp-spring-boot/app') +project(':grails-test-examples-gsp-spring-boot').projectDir = file('grails-test-examples/gsp-spring-boot/app') project(':grails-test-examples-issue-698-domain-save-npe').projectDir = file('grails-test-examples/issue-698-domain-save-npe') project(':grails-test-examples-hyphenated').projectDir = file('grails-test-examples/hyphenated') project(':grails-test-examples-issue-views-182').projectDir = file('grails-test-examples/issue-views-182') From 7040b864f8d42ba00b8fdf954f86d49f1af00194 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Thu, 11 Jun 2026 00:19:52 -0700 Subject: [PATCH 22/25] Support the full applyLayout attribute surface under SiteMesh 3 SiteMesh 3's g:applyLayout only handled name, params and the tag body; the SiteMesh 2 implementation also selects the content to decorate via template (with model and the other g:render attributes), url, and action/controller (delegating to g:include with params), plus parse to force a fresh head/title/body parse and contentType for the SiteMesh context. Mirror those semantics: each content source produces a buffer that flows through the existing captured-page pipeline (fresh page push and restore, capture reuse or content-processor parse, params as page properties, raw emission when no decorator resolves), and the model now reaches the layout render through GrailsSiteMeshViewContext. Deliberate differences from the SiteMesh 2 code, not its docs: encoding is accepted but unused (SiteMesh 2 never read it either), url content is always parsed (as in SiteMesh 2, where no captured page exists for that path), and contentType cannot switch parsers since SiteMesh 3 uses a single configured ContentProcessor. The fresh captured page is pushed for url/include renders too, preserving the nested-render isolation. Adds six gsp-sitemesh3 end-to-end cases (template, model, url, action/controller, parse, no-decorator), GrailsSiteMeshViewContext model unit cases, and documents the previously missing action, controller and parse attributes in the applyLayout tag reference. --- .../src/en/ref/Tags - GSP/applyLayout.adoc | 5 +- .../web/taglib/RenderSitemeshTagLib.groovy | 96 +++++++++++++++---- .../sitemesh3/GrailsSiteMeshViewContext.java | 14 ++- .../GrailsSiteMeshViewContextSpec.groovy | 40 ++++++++ .../grails/layout/EndToEndController.groovy | 29 ++++++ .../grails-app/views/endToEnd/_content.gsp | 19 ++++ .../grails-app/views/endToEnd/_document.gsp | 22 +++++ .../views/endToEnd/actionContent.gsp | 19 ++++ .../views/endToEnd/modelContent.gsp | 19 ++++ .../views/endToEnd/parseContent.gsp | 22 +++++ .../views/endToEnd/templateContent.gsp | 19 ++++ .../views/endToEnd/templateDocument.gsp | 19 ++++ .../grails-app/views/endToEnd/urlContent.gsp | 19 ++++ .../grails-app/views/layouts/model.gsp | 22 +++++ .../groovy/EndToEndSpec.groovy | 61 ++++++++++++ 15 files changed, 407 insertions(+), 18 deletions(-) create mode 100644 grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/_content.gsp create mode 100644 grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/_document.gsp create mode 100644 grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/actionContent.gsp create mode 100644 grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/modelContent.gsp create mode 100644 grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/parseContent.gsp create mode 100644 grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/templateContent.gsp create mode 100644 grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/templateDocument.gsp create mode 100644 grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/urlContent.gsp create mode 100644 grails-test-examples/gsp-sitemesh3/grails-app/views/layouts/model.gsp diff --git a/grails-doc/src/en/ref/Tags - GSP/applyLayout.adoc b/grails-doc/src/en/ref/Tags - GSP/applyLayout.adoc index 3634c1873e7..d9e20353dc7 100644 --- a/grails-doc/src/en/ref/Tags - GSP/applyLayout.adoc +++ b/grails-doc/src/en/ref/Tags - GSP/applyLayout.adoc @@ -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 diff --git a/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/web/taglib/RenderSitemeshTagLib.groovy b/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/web/taglib/RenderSitemeshTagLib.groovy index 0fdfa4d53f0..e83a3bc2828 100644 --- a/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/web/taglib/RenderSitemeshTagLib.groovy +++ b/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/web/taglib/RenderSitemeshTagLib.groovy @@ -19,6 +19,7 @@ package org.grails.plugins.web.taglib import java.nio.CharBuffer +import java.nio.charset.StandardCharsets import org.sitemesh.DecoratorSelector import org.sitemesh.SiteMeshContext @@ -35,12 +36,16 @@ import org.springframework.web.servlet.ViewResolver import grails.artefact.TagLibrary import grails.gsp.TagLib +import grails.util.TypeConvertingMap import org.grails.buffer.FastStringWriter import org.grails.buffer.StreamCharBuffer import org.grails.encoder.CodecLookup import org.grails.encoder.Encoder import org.grails.plugins.sitemesh3.GrailsSiteMeshViewContext import org.grails.plugins.sitemesh3.Sitemesh3CapturedPage +import org.grails.taglib.TagLibraryLookup +import org.grails.taglib.TagOutput +import org.grails.taglib.encoder.OutputContextLookupHelper import org.grails.web.util.WebUtils /** @@ -70,6 +75,32 @@ class RenderSitemeshTagLib implements TagLibrary { @Qualifier('jspViewResolver') ViewResolver viewResolver + // Used to invoke and for the url/action/template + // content sources, exactly as SiteMesh 2's RenderGrailsLayoutTagLib does. + // @Lazy for the same circular-dependency reason as the ViewResolver above: + // gspTagLibraryLookup depends on every taglib bean, including this one. + @Autowired + @Lazy + TagLibraryLookup gspTagLibraryLookup + + /** + * Apply a layout to a particular block of text or to the given view or template.
    + * + * <g:applyLayout name="myLayout">some text</g:applyLayout>
    + * <g:applyLayout name="myLayout" template="mytemplate" />
    + * <g:applyLayout name="myLayout" url="https://www.google.com" />
    + * <g:applyLayout name="myLayout" action="myAction" controller="myController">
    + * + * @attr name The name of the layout + * @attr template Optional. The template to apply the layout to + * @attr url Optional. The URL to retrieve the content from and apply a layout to + * @attr action Optional. The action to be called to generate the content to apply the layout to + * @attr controller Optional. The controller that contains the action that will generate the content to apply the layout to + * @attr contentType Optional. The content type to use, default is 'text/html' + * @attr params Optional. The params to pass onto the page object + * @attr model Optional. The model to pass to the template, include and layout renders as a java.util.Map + * @attr parse Optional. If true, the content is always parsed by the SiteMesh content processor instead of reusing the GSP-captured page + */ // Dispatches via GrailsSiteMeshViewContext so the layout is rendered // through Spring's View API rather than RequestDispatcher.forward(). // Using the default WebAppContext here would re-enter the servlet @@ -81,23 +112,55 @@ class RenderSitemeshTagLib implements TagLibrary { String savedAttribute = request.getAttribute(WebUtils.LAYOUT_ATTRIBUTE) // Save the request-scoped captured page (the one being decorated by the // outer SiteMesh render) and push a fresh one for the duration of the - // body() render. The body of may be a full GSP document - // (whose / the compile-time capture taglibs record into the - // fresh page) or plain markup with no capture taglibs at all (e.g. the - // grails-fields embedded fieldset content, or the already-decorated - // output of a nested ). Restoring the outer page - // afterwards prevents the body fragment from clobbering the outer - // page's body/title/properties. + // content render. The content of may be a full GSP + // document (whose / the compile-time capture taglibs record + // into the fresh page) or plain markup with no capture taglibs at all + // (e.g. the grails-fields embedded fieldset content, or the + // already-decorated output of a nested ). Restoring the + // outer page afterwards prevents the content fragment from clobbering + // the outer page's body/title/properties. Object savedCapturedPage = request.getAttribute(Sitemesh3CapturedPage.REQUEST_ATTRIBUTE) + String contentType = attrs.contentType ? attrs.contentType as String : 'text/html' + Map pageParams = attrs.params instanceof Map ? (Map) attrs.params : [:] + Map viewModel = attrs.model instanceof Map ? (Map) attrs.model : [:] GrailsSiteMeshViewContext context = new GrailsSiteMeshViewContext( - 'text/html', request, response, servletContext, + contentType, request, response, servletContext, contentProcessor, new ResponseMetaData(), false, viewResolver, request.getLocale()) + // SiteMesh 2 renders the decorator template with the supplied model + // (template.make(viewModel) in RenderGrailsLayoutTagLib); mirror that + // by handing the model to the ViewResolver dispatch that renders the + // layout view. + context.setViewModel(viewModel) try { Sitemesh3CapturedPage bodyPage = new Sitemesh3CapturedPage() request.setAttribute(Sitemesh3CapturedPage.REQUEST_ATTRIBUTE, bodyPage) - Object renderedBody = body() + // Select the content to decorate, mirroring SiteMesh 2's + // RenderGrailsLayoutTagLib: a remote URL, another controller + // action's output (via ), a template or view (via + // ) or the tag body. URL and include content never + // flows through the GSP capture taglibs in SiteMesh 2 (no page + // is pushed for those paths), so it is always parsed below — + // the fresh bodyPage is still pushed to keep any capture taglibs + // that run during the include from clobbering the outer page. + boolean externalContent = false + Object renderedBody + if (attrs.url) { + externalContent = true + renderedBody = new URL(attrs.url as String).getText(StandardCharsets.UTF_8.name()) + } else if (attrs.action && attrs.controller) { + externalContent = true + Map includeAttrs = [action: attrs.action, controller: attrs.controller, + params: pageParams, model: viewModel] + renderedBody = TagOutput.captureTagOutput(gspTagLibraryLookup, 'g', 'include', + includeAttrs, null, OutputContextLookupHelper.lookupOutputContext()) + } else if (attrs.view || attrs.template) { + renderedBody = TagOutput.captureTagOutput(gspTagLibraryLookup, 'g', 'render', + attrs, null, OutputContextLookupHelper.lookupOutputContext()) + } else { + renderedBody = body() + } StreamCharBuffer bodyBuffer if (renderedBody instanceof StreamCharBuffer) { bodyBuffer = (StreamCharBuffer) renderedBody @@ -109,7 +172,11 @@ class RenderSitemeshTagLib implements TagLibrary { bodyBuffer.setPreferSubChunkWhenWritingToOtherBuffer(true) Content content - if (bodyPage.isUsed()) { + // parse="true" forces a SiteMesh parse of the rendered markup even + // when the GSP capture taglibs populated the page — the same + // override SiteMesh 2 honors before reusing its GSP-captured page. + boolean forceParse = externalContent || ((TypeConvertingMap) attrs).boolean('parse') + if (!forceParse && bodyPage.isUsed()) { // The body was a full GSP document: the compile-time capture // taglibs have already populated bodyPage with its // //<body>. Only fall back to the whole markup @@ -134,12 +201,9 @@ class RenderSitemeshTagLib implements TagLibrary { // the layout can read them via <g:pageProperty name="..."/>. This // mirrors SiteMesh 2's GrailsLayoutTagLib, which calls // page.addProperty(k, v) for each params entry. - Map params = attrs.params instanceof Map ? (Map) attrs.params : null - if (params) { - params.each { k, v -> - if (k != null && v != null) { - addContentProperty(content, k.toString(), v.toString()) - } + pageParams.each { k, v -> + if (k != null && v != null) { + addContentProperty(content, k.toString(), v.toString()) } } diff --git a/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/GrailsSiteMeshViewContext.java b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/GrailsSiteMeshViewContext.java index 52651c3b9c2..c4a4c6ddb86 100644 --- a/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/GrailsSiteMeshViewContext.java +++ b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/GrailsSiteMeshViewContext.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.util.Collections; import java.util.Locale; +import java.util.Map; import jakarta.servlet.ServletContext; import jakarta.servlet.ServletException; @@ -54,6 +55,13 @@ */ public class GrailsSiteMeshViewContext extends SiteMeshViewContext { + // Model handed to the layout view render. SiteMesh 2's <g:applyLayout> + // renders the decorator template with the supplied model + // (template.make(viewModel)); callers that need the same behavior set + // this before decorating. Defaults to an empty map so the standard + // decoration path is unchanged. + private Map<String, ?> viewModel = Collections.emptyMap(); + public GrailsSiteMeshViewContext(String contentType, HttpServletRequest request, HttpServletResponse response, @@ -67,6 +75,10 @@ public GrailsSiteMeshViewContext(String contentType, includeErrorPages, viewResolver, locale); } + public void setViewModel(Map<String, ?> viewModel) { + this.viewModel = viewModel != null ? viewModel : Collections.emptyMap(); + } + @Override public void dispatch(HttpServletRequest request, HttpServletResponse response, String path) throws ServletException, IOException { @@ -89,7 +101,7 @@ public void dispatch(HttpServletRequest request, HttpServletResponse response, S try { View view = getViewResolver().resolveViewName(path, getLocale()); if (view != null) { - view.render(Collections.emptyMap(), request, response); + view.render(viewModel, request, response); return; } } catch (IOException | ServletException e) { diff --git a/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/GrailsSiteMeshViewContextSpec.groovy b/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/GrailsSiteMeshViewContextSpec.groovy index b346648a151..e17ff0b9ccb 100644 --- a/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/GrailsSiteMeshViewContextSpec.groovy +++ b/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/GrailsSiteMeshViewContextSpec.groovy @@ -88,4 +88,44 @@ class GrailsSiteMeshViewContextSpec extends Specification { then: 1 * view.render(_, request, response) } + + void "dispatch renders the layout view with an empty model by default"() { + given: + View view = Mock(View) + viewResolver.resolveViewName('/layouts/custom', Locale.ENGLISH) >> view + + when: + newContext().dispatch(request, response, '/layouts/custom') + + then: + 1 * view.render([:], request, response) + } + + void "dispatch renders the layout view with the configured view model"() { + given: + View view = Mock(View) + viewResolver.resolveViewName('/layouts/custom', Locale.ENGLISH) >> view + GrailsSiteMeshViewContext context = newContext() + context.setViewModel([greeting: 'hello']) + + when: + context.dispatch(request, response, '/layouts/custom') + + then: + 1 * view.render([greeting: 'hello'], request, response) + } + + void "a null view model falls back to an empty model"() { + given: + View view = Mock(View) + viewResolver.resolveViewName('/layouts/custom', Locale.ENGLISH) >> view + GrailsSiteMeshViewContext context = newContext() + context.setViewModel(null) + + when: + context.dispatch(request, response, '/layouts/custom') + + then: + 1 * view.render([:], request, response) + } } diff --git a/grails-test-examples/gsp-sitemesh3/grails-app/controllers/org/example/grails/layout/EndToEndController.groovy b/grails-test-examples/gsp-sitemesh3/grails-app/controllers/org/example/grails/layout/EndToEndController.groovy index d6a8161ee93..0f25824a9ca 100644 --- a/grails-test-examples/gsp-sitemesh3/grails-app/controllers/org/example/grails/layout/EndToEndController.groovy +++ b/grails-test-examples/gsp-sitemesh3/grails-app/controllers/org/example/grails/layout/EndToEndController.groovy @@ -59,4 +59,33 @@ class EndToEndController { def multilineTitle() { render view: 'multilineTitle', layout: 'simple' } + + def templateContent() { + render view: 'templateContent' + } + + def templateDocument() { + render view: 'templateDocument' + } + + def actionContent() { + render view: 'actionContent' + } + + def urlContent() { + render view: 'urlContent', model: [port: request.localPort] + } + + def parseContent() { + render view: 'parseContent' + } + + def modelContent() { + render view: 'modelContent' + } + + def contentFragment() { + render text: "<html><head><title>Included titleincluded body foo=${params.foo ?: 'none'}", + contentType: 'text/html' + } } diff --git a/grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/_content.gsp b/grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/_content.gsp new file mode 100644 index 00000000000..b0f9924deec --- /dev/null +++ b/grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/_content.gsp @@ -0,0 +1,19 @@ +<%-- + ~ 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. + --%> +

    template content with ${message}

    diff --git a/grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/_document.gsp b/grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/_document.gsp new file mode 100644 index 00000000000..1d76a6ec206 --- /dev/null +++ b/grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/_document.gsp @@ -0,0 +1,22 @@ +<%-- + ~ 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. + --%> + +Document title +document body + diff --git a/grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/actionContent.gsp b/grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/actionContent.gsp new file mode 100644 index 00000000000..8bf0e7d159c --- /dev/null +++ b/grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/actionContent.gsp @@ -0,0 +1,19 @@ +<%-- + ~ 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. + --%> + diff --git a/grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/modelContent.gsp b/grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/modelContent.gsp new file mode 100644 index 00000000000..b209a27e9a1 --- /dev/null +++ b/grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/modelContent.gsp @@ -0,0 +1,19 @@ +<%-- + ~ 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. + --%> +plain body diff --git a/grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/parseContent.gsp b/grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/parseContent.gsp new file mode 100644 index 00000000000..b20547b70c9 --- /dev/null +++ b/grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/parseContent.gsp @@ -0,0 +1,22 @@ +<%-- + ~ 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. + --%> + +Parsed title +parsed body + diff --git a/grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/templateContent.gsp b/grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/templateContent.gsp new file mode 100644 index 00000000000..29e8fbcb01f --- /dev/null +++ b/grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/templateContent.gsp @@ -0,0 +1,19 @@ +<%-- + ~ 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. + --%> + diff --git a/grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/templateDocument.gsp b/grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/templateDocument.gsp new file mode 100644 index 00000000000..d227385d695 --- /dev/null +++ b/grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/templateDocument.gsp @@ -0,0 +1,19 @@ +<%-- + ~ 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. + --%> + diff --git a/grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/urlContent.gsp b/grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/urlContent.gsp new file mode 100644 index 00000000000..c7e87b2afd1 --- /dev/null +++ b/grails-test-examples/gsp-sitemesh3/grails-app/views/endToEnd/urlContent.gsp @@ -0,0 +1,19 @@ +<%-- + ~ 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. + --%> + diff --git a/grails-test-examples/gsp-sitemesh3/grails-app/views/layouts/model.gsp b/grails-test-examples/gsp-sitemesh3/grails-app/views/layouts/model.gsp new file mode 100644 index 00000000000..2a9cc2069ef --- /dev/null +++ b/grails-test-examples/gsp-sitemesh3/grails-app/views/layouts/model.gsp @@ -0,0 +1,22 @@ +<%-- + ~ 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. + --%> + +Model <g:layoutTitle/> +${greeting} + diff --git a/grails-test-examples/gsp-sitemesh3/src/integration-test/groovy/EndToEndSpec.groovy b/grails-test-examples/gsp-sitemesh3/src/integration-test/groovy/EndToEndSpec.groovy index f15caa6bf74..da3da5afb78 100644 --- a/grails-test-examples/gsp-sitemesh3/src/integration-test/groovy/EndToEndSpec.groovy +++ b/grails-test-examples/gsp-sitemesh3/src/integration-test/groovy/EndToEndSpec.groovy @@ -108,4 +108,65 @@ class EndToEndSpec extends ContainerGebSpec {

    Hello

    body text
    """ } + + // The template/url/action/model/parse attribute forms mirror SiteMesh 2's + // (RenderGrailsLayoutTagLib); the SiteMesh 2 twin app has + // no coverage for them, so these cases encode the SiteMesh 2 semantics. + def 'apply layout to a template'() { + when: + go('endToEnd/templateContent') + + then: + pageSource.contains('

    Hello

    ') + pageSource.contains('template content with from the model') + } + + def 'apply layout to a template that is a full document'() { + when: + go('endToEnd/templateDocument') + + then: + title == 'Decorated Document title' + pageSource.contains('

    Hello

    ') + pageSource.contains('document body') + } + + def 'apply layout to the output of another controller action'() { + when: + go('endToEnd/actionContent') + + then: + title == 'Decorated Included title' + pageSource.contains('

    Hello

    ') + pageSource.contains('included body foo=bar') + } + + def 'apply layout to content fetched from a url'() { + when: + go('endToEnd/urlContent') + + then: + title == 'Decorated Included title' + pageSource.contains('

    Hello

    ') + pageSource.contains('included body foo=none') + } + + def 'parse attribute forces a SiteMesh parse of the tag body'() { + when: + go('endToEnd/parseContent') + + then: + title == 'Decorated Parsed title' + pageSource.contains('

    Hello

    ') + pageSource.contains('parsed body') + } + + def 'model is available to the layout being applied'() { + when: + go('endToEnd/modelContent') + + then: + pageSource.contains('hi from the model') + pageSource.contains('plain body') + } } From 14da8e4b112a72895ea7e39f5417445b798709be Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Thu, 11 Jun 2026 11:54:04 -0700 Subject: [PATCH 23/25] Apply the published-module build conventions to grails-gsp-spring-boot --- grails-gsp/spring-boot/build.gradle | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/grails-gsp/spring-boot/build.gradle b/grails-gsp/spring-boot/build.gradle index 1d2c0e06df9..0f81f68282f 100644 --- a/grails-gsp/spring-boot/build.gradle +++ b/grails-gsp/spring-boot/build.gradle @@ -18,10 +18,15 @@ */ plugins { + id 'project-report' id 'org.apache.grails.buildsrc.properties' id 'org.apache.grails.buildsrc.dependency-validator' - id 'org.apache.grails.buildsrc.compile' id 'org.apache.grails.gradle.grails-plugin' + id 'org.apache.grails.buildsrc.compile' + id 'org.apache.grails.buildsrc.publish' + id 'org.apache.grails.buildsrc.sbom' + id 'org.apache.grails.gradle.grails-code-style' + id 'org.apache.grails.gradle.grails-jacoco' } version = projectVersion @@ -41,8 +46,7 @@ dependencies { } apply { - //TODO: no docs until this is a publish project (its docs likely will need to be handled differently) - // from rootProject.layout.projectDirectory.file('gradle/docs-config.gradle') + from rootProject.layout.projectDirectory.file('gradle/docs-config.gradle') from rootProject.layout.projectDirectory.file('gradle/test-config.gradle') from rootProject.layout.projectDirectory.file('gradle/grails-extension-gradle-config.gradle') } From 5e636fdcd5070df69f507942f605f0e4b9412455 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Thu, 11 Jun 2026 11:54:04 -0700 Subject: [PATCH 24/25] Leave the view resolver unwrapped when the SiteMesh 3 beans are absent --- ...SiteMeshViewResolverBeanPostProcessor.java | 21 +++++++++++++++++++ ...shViewResolverBeanPostProcessorSpec.groovy | 21 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolverBeanPostProcessor.java b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolverBeanPostProcessor.java index 77479991bc9..1c0dbf3fd62 100644 --- a/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolverBeanPostProcessor.java +++ b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolverBeanPostProcessor.java @@ -20,6 +20,9 @@ import org.sitemesh.webmvc.SiteMeshViewResolverBeanPostProcessor; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; + /** * {@link SiteMeshViewResolverBeanPostProcessor} preconfigured to wrap * Grails' {@code gspViewResolver} bean with a @@ -42,4 +45,22 @@ public GrailsSiteMeshViewResolverBeanPostProcessor() { setTargetViewResolverBeanName(TARGET_VIEW_RESOLVER_BEAN_NAME); setSiteMeshViewResolverClass(GrailsSiteMeshViewResolver.class); } + + /** + * Contexts can include this post-processor (via {@code Sitemesh3AutoConfiguration}) + * without the SiteMesh plugin beans being registered — the unit-test context built + * by grails-testing-support registers all Grails auto-configurations but never runs + * the plugin's {@code doWithSpring}. Decoration is impossible without those beans, + * so leave the view resolver unwrapped instead of failing the context. + */ + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + BeanFactory beanFactory = getBeanFactory(); + if (beanFactory == null || + !beanFactory.containsBean(getContentProcessorBeanName()) || + !beanFactory.containsBean(getDecoratorSelectorBeanName())) { + return bean; + } + return super.postProcessAfterInitialization(bean, beanName); + } } diff --git a/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolverBeanPostProcessorSpec.groovy b/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolverBeanPostProcessorSpec.groovy index 550c9faa11c..2cd440da744 100644 --- a/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolverBeanPostProcessorSpec.groovy +++ b/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolverBeanPostProcessorSpec.groovy @@ -45,6 +45,8 @@ class GrailsSiteMeshViewResolverBeanPostProcessorSpec extends Specification { ContentProcessor cp = Mock(ContentProcessor) DecoratorSelector ds = Mock(DecoratorSelector) ServletContext sc = Mock(ServletContext) + beanFactory.containsBean('contentProcessor') >> true + beanFactory.containsBean('decoratorSelector') >> true beanFactory.getBean('contentProcessor', ContentProcessor) >> cp beanFactory.getBean('decoratorSelector', DecoratorSelector) >> ds beanFactory.getBean('servletContext', ServletContext) >> sc @@ -61,6 +63,7 @@ class GrailsSiteMeshViewResolverBeanPostProcessorSpec extends Specification { void "beans with a non-matching name are returned untouched"() { given: + beanFactory.containsBean(_ as String) >> true GrailsSiteMeshViewResolverBeanPostProcessor pp = new GrailsSiteMeshViewResolverBeanPostProcessor() pp.setBeanFactory(beanFactory) ViewResolver other = new InternalResourceViewResolver() @@ -68,4 +71,22 @@ class GrailsSiteMeshViewResolverBeanPostProcessorSpec extends Specification { expect: pp.postProcessAfterInitialization(other, 'someOther').is(other) } + + void "the view resolver is left unwrapped when the SiteMesh beans are not in the context"() { + given: "a context without the plugin's contentProcessor/decoratorSelector beans, like a unit-test context" + beanFactory.containsBean('contentProcessor') >> hasContentProcessor + beanFactory.containsBean('decoratorSelector') >> hasDecoratorSelector + GrailsSiteMeshViewResolverBeanPostProcessor pp = new GrailsSiteMeshViewResolverBeanPostProcessor() + pp.setBeanFactory(beanFactory) + ViewResolver resolver = new InternalResourceViewResolver() + + expect: + pp.postProcessAfterInitialization(resolver, 'jspViewResolver').is(resolver) + + where: + hasContentProcessor | hasDecoratorSelector + false | false + true | false + false | true + } } From 536da4d477c47126c328a9eca644fbf783d22115 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Thu, 11 Jun 2026 12:31:56 -0700 Subject: [PATCH 25/25] Defer to SiteMesh 2's layout resolver instead of double-wrapping it --- ...sSiteMeshViewResolverBeanPostProcessor.java | 11 ++++++++++- ...eshViewResolverBeanPostProcessorSpec.groovy | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolverBeanPostProcessor.java b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolverBeanPostProcessor.java index 1c0dbf3fd62..39e15e9b04f 100644 --- a/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolverBeanPostProcessor.java +++ b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolverBeanPostProcessor.java @@ -22,6 +22,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; +import org.springframework.context.ApplicationListener; /** * {@link SiteMeshViewResolverBeanPostProcessor} preconfigured to wrap @@ -52,13 +53,21 @@ public GrailsSiteMeshViewResolverBeanPostProcessor() { * by grails-testing-support registers all Grails auto-configurations but never runs * the plugin's {@code doWithSpring}. Decoration is impossible without those beans, * so leave the view resolver unwrapped instead of failing the context. + * + *

    Resolvers implementing {@link ApplicationListener} are also left unwrapped. + * When the legacy grails-layout module is on the classpath its post-processor + * installs the SiteMesh 2 {@code GrailsLayoutViewResolver} (an + * {@code ApplicationListener}) as {@code jspViewResolver}; that resolver already + * decorates, and wrapping it would additionally break Spring's event listener + * retrieval, which expects the instance to match the definition's listener type. */ @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { BeanFactory beanFactory = getBeanFactory(); if (beanFactory == null || !beanFactory.containsBean(getContentProcessorBeanName()) || - !beanFactory.containsBean(getDecoratorSelectorBeanName())) { + !beanFactory.containsBean(getDecoratorSelectorBeanName()) || + bean instanceof ApplicationListener) { return bean; } return super.postProcessAfterInitialization(bean, beanName); diff --git a/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolverBeanPostProcessorSpec.groovy b/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolverBeanPostProcessorSpec.groovy index 2cd440da744..d188232cd25 100644 --- a/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolverBeanPostProcessorSpec.groovy +++ b/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolverBeanPostProcessorSpec.groovy @@ -25,6 +25,8 @@ import org.sitemesh.SiteMeshContext import org.sitemesh.content.ContentProcessor import org.springframework.beans.factory.BeanFactory +import org.springframework.context.ApplicationEvent +import org.springframework.context.ApplicationListener import org.springframework.web.servlet.ViewResolver import org.springframework.web.servlet.view.InternalResourceViewResolver @@ -34,6 +36,11 @@ class GrailsSiteMeshViewResolverBeanPostProcessorSpec extends Specification { BeanFactory beanFactory = Mock(BeanFactory) + static class ListenerViewResolver extends InternalResourceViewResolver implements ApplicationListener { + @Override + void onApplicationEvent(ApplicationEvent event) { } + } + void "target bean name defaults to jspViewResolver and wrapper class is GrailsSiteMeshViewResolver"() { expect: new GrailsSiteMeshViewResolverBeanPostProcessor().targetViewResolverBeanName == 'jspViewResolver' @@ -72,6 +79,17 @@ class GrailsSiteMeshViewResolverBeanPostProcessorSpec extends Specification { pp.postProcessAfterInitialization(other, 'someOther').is(other) } + void "a resolver that is an ApplicationListener, like SiteMesh 2's layout resolver, is left unwrapped"() { + given: + beanFactory.containsBean(_ as String) >> true + GrailsSiteMeshViewResolverBeanPostProcessor pp = new GrailsSiteMeshViewResolverBeanPostProcessor() + pp.setBeanFactory(beanFactory) + ViewResolver listenerResolver = new ListenerViewResolver() + + expect: + pp.postProcessAfterInitialization(listenerResolver, 'jspViewResolver').is(listenerResolver) + } + void "the view resolver is left unwrapped when the SiteMesh beans are not in the context"() { given: "a context without the plugin's contentProcessor/decoratorSelector beans, like a unit-test context" beanFactory.containsBean('contentProcessor') >> hasContentProcessor