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/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-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-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..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,14 +22,22 @@ import org.grails.forge.application.ApplicationType; import org.grails.forge.application.generator.GeneratorContext; import org.grails.forge.build.dependencies.Dependency; -import org.grails.forge.feature.Category; +import org.grails.forge.feature.DefaultFeature; import org.grails.forge.feature.Feature; +import org.grails.forge.feature.view.GspLayout; +import org.grails.forge.options.Options; -@Singleton -public class Sitemesh3 implements Feature { +import java.util.Set; - public Sitemesh3() { - } +/** + * Default GSP layout decorator, backed by SiteMesh 3 ({@code grails-sitemesh3}). + * Applied automatically to web applications unless another {@link GspLayout} + * (e.g. {@code grails-layout}) is explicitly selected. Not visible: SiteMesh 3 + * is silently the default, mirroring how the legacy {@code grails-layout} was + * silently the default before it. + */ +@Singleton +public class Sitemesh3 extends GspLayout implements DefaultFeature { @Override public String getName() { @@ -38,29 +46,30 @@ public String getName() { @Override public String getTitle() { - return "Sitemesh 3"; + return "GSP SiteMesh 3 Layouts"; } @Override public String getDescription() { - return "Adds support for Sitemesh3 based layouts instead of Sitemesh 2"; + return "Adds support for SiteMesh 3 based GSP layouts"; } @Override - public void apply(GeneratorContext generatorContext) { - generatorContext.addDependency(Dependency.builder() - .groupId("org.apache.grails") - .artifactId("grails-sitemesh3") - .implementation()); + public boolean isVisible() { + return false; } @Override - public boolean supports(ApplicationType applicationType) { - return true; + public boolean shouldApply(ApplicationType applicationType, Options options, Set selectedFeatures) { + return supports(applicationType) && + selectedFeatures.stream().noneMatch(GspLayout.class::isInstance); } @Override - public String getCategory() { - return Category.VIEW; + public void apply(GeneratorContext generatorContext) { + generatorContext.addDependency(Dependency.builder() + .groupId("org.apache.grails") + .artifactId("grails-sitemesh3") + .implementation()); } } 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..9d6c1e9249d --- /dev/null +++ b/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/view/GspLayoutSpec.groovy @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.forge.feature.view + +import org.grails.forge.ApplicationContextSpec +import org.grails.forge.BuildBuilder +import org.grails.forge.application.ApplicationType +import spock.lang.Unroll + +class GspLayoutSpec extends ApplicationContextSpec { + + @Unroll + void "web #applicationType apps use SiteMesh 3 (grails-sitemesh3) by default"() { + when: + final String build = new BuildBuilder(beanContext) + .features(['gsp']) + .applicationType(applicationType) + .render() + + then: + build.contains('implementation "org.apache.grails:grails-sitemesh3"') + !build.contains('implementation "org.apache.grails:grails-layout"') + + where: + applicationType << [ApplicationType.WEB, ApplicationType.WEB_PLUGIN] + } + + void "selecting grails-layout replaces the default SiteMesh 3 decorator"() { + when: + final String build = new BuildBuilder(beanContext) + .features(['gsp', 'grails-layout']) + .applicationType(ApplicationType.WEB) + .render() + + then: + build.contains('implementation "org.apache.grails:grails-layout"') + !build.contains('implementation "org.apache.grails:grails-sitemesh3"') + } + + void "sitemesh3 is the silent default and is not a selectable feature"() { + when: + new BuildBuilder(beanContext) + .features(['gsp', 'sitemesh3']) + .applicationType(ApplicationType.WEB) + .render() + + then: + IllegalArgumentException e = thrown() + e.message.contains('The requested feature does not exist: sitemesh3') + } +} 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..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 @@ -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 @@ -80,7 +73,11 @@ class Sitemesh3GrailsPlugin extends Plugin { { -> ConfigurableEnvironment configurableEnvironment = grailsApplication.mainContext.environment as ConfigurableEnvironment def propertySources = configurableEnvironment.getPropertySources() - String defaultLayout = grailsApplication.getConfig().getProperty('grails.sitemesh.default.layout') + // The SiteMesh 3 specific key wins; fall back to the SiteMesh 2 + // plugin's grails.views.layout.default so existing apps keep + // their configured default layout when switching. + String defaultLayout = grailsApplication.getConfig().getProperty('grails.sitemesh.default.layout') ?: + grailsApplication.getConfig().getProperty('grails.views.layout.default') propertySources.addFirst(getDefaultPropertySource(configurableEnvironment, defaultLayout)) (grailsApplication as DefaultGrailsApplication).config = new PropertySourcesConfig(propertySources) @@ -103,29 +100,10 @@ class Sitemesh3GrailsPlugin extends Plugin { layoutCacheExpirationMillis = config.getProperty('grails.sitemesh.layout.cache.interval', Long, 5000L) } - // Replace the filter registration from - // org.sitemesh.autoconfigure.SiteMeshAutoConfiguration with a no-op - // filter bean under the same name. SiteMeshAutoConfiguration is - // @ConditionalOnMissingBean(name = "sitemesh") so registering this - // bean disables the upstream filter-based integration entirely. - // Decoration is done by the Spring MVC view resolver chain. - sitemesh(FilterRegistrationBean) { bean -> - bean.autowire = false - filter = new NoopSitemeshFilter() - enabled = false - dispatcherTypes = EnumSet.of(DispatcherType.REQUEST) - } - } - } - - // This class is never invoked (the FilterRegistrationBean has enabled = false). - // It exists solely so we can register a bean named "sitemesh" and satisfy - // SiteMeshAutoConfiguration's @ConditionalOnMissingBean(name = "sitemesh") - // guard — preventing the upstream filter-based integration from activating. - static class NoopSitemeshFilter implements Filter { - @Override - void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { - chain.doFilter(request, response) + // Unwraps the SiteMesh view for "render template:" partials so + // they are never decorated with a layout (the SiteMesh 2 plugin + // does the same with its GrailsLayoutRenderViewMutator). + grailsRenderViewMutator(Sitemesh3RenderViewMutator) } } } 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..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,9 +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 /** @@ -67,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 @@ -76,15 +110,107 @@ 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 + // 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 { - Content content = contentProcessor.build(CharBuffer.wrap(body()), context) + Sitemesh3CapturedPage bodyPage = new Sitemesh3CapturedPage() + request.setAttribute(Sitemesh3CapturedPage.REQUEST_ATTRIBUTE, bodyPage) + + // 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 + } else { + FastStringWriter stringWriter = new FastStringWriter() + stringWriter.print(renderedBody) + bodyBuffer = stringWriter.buffer + } + bodyBuffer.setPreferSubChunkWhenWritingToOtherBuffer(true) + + Content content + // 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 + // 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 + // mirrors SiteMesh 2's GrailsLayoutTagLib, which calls + // page.addProperty(k, v) for each params entry. + pageParams.each { k, v -> + if (k != null && v != null) { + addContentProperty(content, k.toString(), v.toString()) + } + } + 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 +218,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 { @@ -105,6 +242,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/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 <head>/<body> 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); 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/main/java/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolver.java b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolver.java index 0c210032e34..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 @@ -51,6 +51,9 @@ public GrailsSiteMeshViewResolver(ViewResolver innerViewResolver, @Override protected SiteMeshView createSiteMeshView(View innerView) { + // 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/GrailsSiteMeshViewResolverBeanPostProcessor.java b/grails-gsp/grails-sitemesh3/src/main/java/org/grails/plugins/sitemesh3/GrailsSiteMeshViewResolverBeanPostProcessor.java index 77479991bc9..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 @@ -20,6 +20,10 @@ import org.sitemesh.webmvc.SiteMeshViewResolverBeanPostProcessor; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.context.ApplicationListener; + /** * {@link SiteMeshViewResolverBeanPostProcessor} preconfigured to wrap * Grails' {@code gspViewResolver} bean with a @@ -42,4 +46,30 @@ 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. + * + * <p>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()) || + bean instanceof ApplicationListener) { + return bean; + } + return super.postProcessAfterInitialization(bean, beanName); + } } 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/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, "<title", 0); + int titleStart = indexOfTitleOpenTag(head); if (titleStart < 0) { return head; } @@ -249,6 +249,30 @@ private CharSequence extractHead() { return sb; } + // Finds the start of a real <title> open tag. A bare "<title" prefix + // match is not enough: it would also match elements such as + // <titlebar> or <title-x> 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, "<title", from); + if (i < 0) { + return -1; + } + int boundary = i + 6; + if (boundary < len) { + char c = head.charAt(boundary); + if (c == '>' || 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/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/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 @@ * <li>Controller's {@code static layout = 'x'} property</li> * <li>{@code /layouts/<controllerName>/<actionUri>.gsp}</li> * <li>{@code /layouts/<controllerName>.gsp}</li> - * <li>Configured default (e.g. {@code grails.sitemesh.default.layout})</li> + * <li>The configured default, or — when none is configured — the + * implicit {@code application} layout, matching the SiteMesh 2 + * plugin's GroovyPageLayoutFinder</li> * </ol> * * <p>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/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/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/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-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..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<ApplicationEvent> { + @Override + void onApplicationEvent(ApplicationEvent event) { } + } + void "target bean name defaults to jspViewResolver and wrapper class is GrailsSiteMeshViewResolver"() { expect: new GrailsSiteMeshViewResolverBeanPostProcessor().targetViewResolverBeanName == 'jspViewResolver' @@ -45,6 +52,8 @@ class GrailsSiteMeshViewResolverBeanPostProcessorSpec extends Specification { ContentProcessor cp = Mock(ContentProcessor) DecoratorSelector<SiteMeshContext> 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 +70,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 +78,33 @@ class GrailsSiteMeshViewResolverBeanPostProcessorSpec extends Specification { expect: 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 + 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 + } } 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..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 @@ -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,25 @@ class GrailsSiteMeshViewResolverSpec extends Specification { result.is(redirect) } + 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: 'the view is resolved (upstream prepareForBufferedRender runs) and rendered' + 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/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('<meta charset="UTF-8"><title>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 + } +} 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) - } -} 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..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,9 +166,70 @@ 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 + } + + 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), 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) + } +} 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') } 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 diff --git a/grails-test-examples/app1/build.gradle b/grails-test-examples/app1/build.gradle index 7876d7281a6..d6e58ff03c0 100644 --- a/grails-test-examples/app1/build.gradle +++ b/grails-test-examples/app1/build.gradle @@ -39,11 +39,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/app2/build.gradle b/grails-test-examples/app2/build.gradle index d0cfa927b37..a5d6f09b44f 100644 --- a/grails-test-examples/app2/build.gradle +++ b/grails-test-examples/app2/build.gradle @@ -39,11 +39,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/app3/build.gradle b/grails-test-examples/app3/build.gradle index 7a9bd1ac473..5e5d1c30822 100644 --- a/grails-test-examples/app3/build.gradle +++ b/grails-test-examples/app3/build.gradle @@ -38,11 +38,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/async-events-pubsub-demo/build.gradle b/grails-test-examples/async-events-pubsub-demo/build.gradle index 81db50e646e..423e8f6545c 100644 --- a/grails-test-examples/async-events-pubsub-demo/build.gradle +++ b/grails-test-examples/async-events-pubsub-demo/build.gradle @@ -47,11 +47,10 @@ 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' - } - 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-views-gson' diff --git a/grails-test-examples/cache/build.gradle b/grails-test-examples/cache/build.gradle index 1a6c9a63cf8..c3113d4aac0 100644 --- a/grails-test-examples/cache/build.gradle +++ b/grails-test-examples/cache/build.gradle @@ -49,11 +49,10 @@ 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' - } - else { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation 'org.apache.grails:grails-layout' + } else { + 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/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/datasources/build.gradle b/grails-test-examples/datasources/build.gradle index b8c928826ed..2257681fbf3 100644 --- a/grails-test-examples/datasources/build.gradle +++ b/grails-test-examples/datasources/build.gradle @@ -33,11 +33,10 @@ 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' - } - 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/demo33/build.gradle b/grails-test-examples/demo33/build.gradle index a762ffe5064..72c6b8df714 100644 --- a/grails-test-examples/demo33/build.gradle +++ b/grails-test-examples/demo33/build.gradle @@ -45,11 +45,10 @@ 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' - } - 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' 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..3452ce2bef3 100644 --- a/grails-test-examples/external-configuration/build.gradle +++ b/grails-test-examples/external-configuration/build.gradle @@ -38,11 +38,10 @@ 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' - } - 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' 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..00ae5e70a8f 100644 --- a/grails-test-examples/geb-context-path/build.gradle +++ b/grails-test-examples/geb-context-path/build.gradle @@ -42,11 +42,10 @@ 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' - } - 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' 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..cf23c19629b 100644 --- a/grails-test-examples/geb-gebconfig/build.gradle +++ b/grails-test-examples/geb-gebconfig/build.gradle @@ -43,11 +43,10 @@ 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' - } - 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' implementation 'org.apache.grails:grails-scaffolding' diff --git a/grails-test-examples/geb/build.gradle b/grails-test-examples/geb/build.gradle index 9c87bb95993..1ed570e8172 100644 --- a/grails-test-examples/geb/build.gradle +++ b/grails-test-examples/geb/build.gradle @@ -42,11 +42,10 @@ 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' - } - 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' implementation 'org.apache.grails:grails-scaffolding' diff --git a/grails-test-examples/gorm/build.gradle b/grails-test-examples/gorm/build.gradle index 811c2ba9145..b960240d9d7 100644 --- a/grails-test-examples/gorm/build.gradle +++ b/grails-test-examples/gorm/build.gradle @@ -34,11 +34,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/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: "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') + } } 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..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,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-bom')) diff --git a/grails-test-examples/hibernate5/grails-hibernate/build.gradle b/grails-test-examples/hibernate5/grails-hibernate/build.gradle index 9233fc1f524..a68f7c9e60d 100644 --- a/grails-test-examples/hibernate5/grails-hibernate/build.gradle +++ b/grails-test-examples/hibernate5/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-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..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,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-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..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,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-bom')) diff --git a/grails-test-examples/hibernate5/issue450/build.gradle b/grails-test-examples/hibernate5/issue450/build.gradle index 27187b057c6..c28e422fa6d 100644 --- a/grails-test-examples/hibernate5/issue450/build.gradle +++ b/grails-test-examples/hibernate5/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-bom')) 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')) diff --git a/grails-test-examples/hyphenated/build.gradle b/grails-test-examples/hyphenated/build.gradle index 87589bf3c32..6512f0b685f 100644 --- a/grails-test-examples/hyphenated/build.gradle +++ b/grails-test-examples/hyphenated/build.gradle @@ -38,11 +38,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/issue-11102/build.gradle b/grails-test-examples/issue-11102/build.gradle index e79162954e1..e2ac7dd7a9a 100644 --- a/grails-test-examples/issue-11102/build.gradle +++ b/grails-test-examples/issue-11102/build.gradle @@ -42,11 +42,10 @@ 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' - } - 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-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..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,11 +32,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/issue-views-182/build.gradle b/grails-test-examples/issue-views-182/build.gradle index 07122b9739a..b285ef00752 100644 --- a/grails-test-examples/issue-views-182/build.gradle +++ b/grails-test-examples/issue-views-182/build.gradle @@ -48,11 +48,10 @@ 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' - } - 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-logging' implementation 'org.apache.grails:grails-cache' 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/mongodb/base/build.gradle b/grails-test-examples/mongodb/base/build.gradle index 207878339dd..1289e7bf5a4 100644 --- a/grails-test-examples/mongodb/base/build.gradle +++ b/grails-test-examples/mongodb/base/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-web-boot' - 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-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..7852e29f5cd 100644 --- a/grails-test-examples/mongodb/database-per-tenant/build.gradle +++ b/grails-test-examples/mongodb/database-per-tenant/build.gradle @@ -37,11 +37,10 @@ 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' - } - 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-bom')) diff --git a/grails-test-examples/mongodb/gson-templates/build.gradle b/grails-test-examples/mongodb/gson-templates/build.gradle index 6c568154894..b6576b1950e 100644 --- a/grails-test-examples/mongodb/gson-templates/build.gradle +++ b/grails-test-examples/mongodb/gson-templates/build.gradle @@ -36,11 +36,10 @@ 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' - } - 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-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..6fe5ba9fa49 100644 --- a/grails-test-examples/mongodb/hibernate5/build.gradle +++ b/grails-test-examples/mongodb/hibernate5/build.gradle @@ -37,11 +37,10 @@ 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' - } - 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/namespaces/build.gradle b/grails-test-examples/namespaces/build.gradle index 99da2a0e3b6..88cc0bb649a 100644 --- a/grails-test-examples/namespaces/build.gradle +++ b/grails-test-examples/namespaces/build.gradle @@ -38,11 +38,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/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..449b2742178 100644 --- a/grails-test-examples/plugins/issue11005/build.gradle +++ b/grails-test-examples/plugins/issue11005/build.gradle @@ -31,11 +31,10 @@ group = 'com.example.grails.plugins' dependencies { implementation platform(project(':grails-bom')) - 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' } 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..d0bc369daef 100644 --- a/grails-test-examples/plugins/loadafter/build.gradle +++ b/grails-test-examples/plugins/loadafter/build.gradle @@ -31,11 +31,10 @@ 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' - } - 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/loadfirst/build.gradle b/grails-test-examples/plugins/loadfirst/build.gradle index ef3ddc7dd2d..8330e2381bd 100644 --- a/grails-test-examples/plugins/loadfirst/build.gradle +++ b/grails-test-examples/plugins/loadfirst/build.gradle @@ -31,11 +31,10 @@ 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' - } - 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/loadsecond/build.gradle b/grails-test-examples/plugins/loadsecond/build.gradle index 52239603e69..14e3b892e62 100644 --- a/grails-test-examples/plugins/loadsecond/build.gradle +++ b/grails-test-examples/plugins/loadsecond/build.gradle @@ -30,11 +30,10 @@ 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' - } - 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/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/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" 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' diff --git a/grails-test-examples/views-functional-tests/build.gradle b/grails-test-examples/views-functional-tests/build.gradle index 43e92420d06..94996c4932c 100644 --- a/grails-test-examples/views-functional-tests/build.gradle +++ b/grails-test-examples/views-functional-tests/build.gradle @@ -39,11 +39,10 @@ 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' - } - 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-databinding' diff --git a/grails-test-suite-uber/build.gradle b/grails-test-suite-uber/build.gradle index dabc5391c55..b2185b8af78 100644 --- a/grails-test-suite-uber/build.gradle +++ b/grails-test-suite-uber/build.gradle @@ -50,11 +50,10 @@ 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') - } - else { + if (System.getenv('SITEMESH2_TESTING_ENABLED') == 'true') { implementation project(':grails-layout') + } else { + implementation project(':grails-sitemesh3') } testImplementation project(':grails-testing-support-datamapping'), { // This is a local project dependency diff --git a/settings.gradle b/settings.gradle index 8f49d68c519..7a0f111fb2a 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', 'grails-test-examples-gsp-spring-boot', 'grails-test-examples-hyphenated', 'grails-test-examples-issue-11102', @@ -476,7 +476,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') 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')