You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by jd...@apache.org on 2025/07/06 13:37:23 UTC
(grails-core) branch issue14193 updated (20aa03f7f5 -> 3f14449d86)
This is an automated email from the ASF dual-hosted git repository.
jdaugherty pushed a change to branch issue14193
in repository https://gitbox.apache.org/repos/asf/grails-core.git
omit 20aa03f7f5 revert sitemesh3 upgrade & rename sitemesh -> layout to ensure license compliance
new 3f14449d86 revert sitemesh3 upgrade & rename sitemesh -> layout to ensure license compliance
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (20aa03f7f5)
\
N -- N -- N refs/heads/issue14193 (3f14449d86)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
grails-gsp/grails-web-gsp/build.gradle | 2 +-
.../main/groovy/org/grails/plugins/web/GroovyPagesGrailsPlugin.groovy | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
(grails-core) 01/01: revert sitemesh3 upgrade & rename sitemesh -> layout to ensure license compliance
Posted by jd...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
jdaugherty pushed a commit to branch issue14193
in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 3f14449d86aa963289f7344fbb17080f49d58e79
Author: James Daugherty <jd...@jdresources.net>
AuthorDate: Fri Jul 4 07:35:37 2025 -0400
revert sitemesh3 upgrade & rename sitemesh -> layout to ensure license compliance
---
LICENSE | 7 +
NOTICE | 5 +-
RENAME.md | 2 +-
UPGRADE7.md | 3 +-
dependencies.gradle | 2 +
etc/bin/rename_gradle_artifacts.sh | 8 +-
gradle.properties | 1 -
gradle/publish-root-config.gradle | 3 +-
grails-controllers/build.gradle | 1 +
.../src/en/guide/conf/config/builtInOptions.adoc | 2 +-
.../src/en/guide/theWebLayer/gsp/layouts.adoc | 9 +-
.../src/en/guide/upgrading/upgrading60x.adoc | 21 ++
grails-doc/src/en/ref/Tags - GSP/layoutHead.adoc | 2 +-
grails-doc/src/en/ref/Tags - GSP/layoutTitle.adoc | 2 +-
grails-doc/src/en/ref/Tags - GSP/pageProperty.adoc | 2 +-
.../src/test/resources/docs/guide/single.html | 4 +-
.../test/resources/docs/ref/Tags/layoutHead.html | 2 +-
.../test/resources/docs/ref/Tags/layoutTitle.html | 2 +-
.../test/resources/docs/ref/Tags/pageProperty.html | 2 +-
.../gsp/compiler/GrailsLayoutPreprocessor.java | 95 +++++
.../org/grails/gsp/compiler/GroovyPageParser.java | 26 +-
.../{spring-boot => grails-layout}/build.gradle | 31 +-
.../plugins/web/GrailsLayoutGrailsPlugin.groovy | 37 +-
.../plugins/web/taglib/GrailsLayoutTagLib.groovy | 235 +++++++++++++
.../web/taglib/RenderGrailsLayoutTagLib.groovy | 346 ++++++++++++++++++
grails-gsp/grails-sitemesh3/README.md | 18 -
grails-gsp/grails-sitemesh3/build.gradle | 89 -----
.../sitemesh3/GrailsLayoutHandlerMapping.java | 67 ----
.../plugins/sitemesh3/Sitemesh3GrailsPlugin.groovy | 98 ------
.../plugins/web/taglib/RenderSitemeshTagLib.groovy | 181 ----------
.../plugins/web/taglib/SitemeshTagLib.groovy | 146 --------
grails-gsp/grails-web-gsp/build.gradle | 1 +
.../org/grails/web/pages/GSPResponseWriter.java | 48 ++-
.../view/EmbeddedGrailsLayoutViewResolver.java | 112 ++++++
.../web/servlet/view/GrailsLayoutViewResolver.java | 147 ++++++++
.../build.gradle | 32 +-
.../web/layout/EmbeddedGrailsLayoutView.java | 172 +++++++++
.../apache/grails/web/layout/FactoryHolder.java | 39 +-
.../grails/web/layout/GSPGrailsLayoutPage.java | 237 +++++++++++++
.../web/layout/GrailsContentBufferingResponse.java | 141 ++++++++
.../grails/web/layout/GrailsHTMLPageParser.java | 86 +++++
.../web/layout/GrailsLayoutDecoratorMapper.java | 91 +++++
.../layout/GrailsLayoutRenderViewMutator.groovy | 51 +++
.../apache/grails/web/layout/GrailsLayoutView.java | 47 +++
.../grails/web/layout/GrailsNoDecorator.java | 46 ++-
.../web/layout/GrailsPageResponseWrapper.java | 391 +++++++++++++++++++++
.../web/layout}/GrailsRoutablePrintWriter.java | 7 +-
.../web/layout/GrailsViewBufferingResponse.java | 59 ++++
.../grails/web/layout/GroovyPageLayoutFinder.java | 310 ++++++++++++++++
.../apache/grails/web/layout/LayoutSelector.groovy | 30 +-
.../grails/web/layout/SpringMVCViewDecorator.java | 114 ++++++
.../web/layout/TokenizedHTMLPage2Content.java | 78 ++++
.../grails/web/layout/grails-layout-default.xml | 33 ++
.../web/layout/GroovyPageLayoutFinderSpec.groovy | 55 +++
grails-gsp/plugin/build.gradle | 5 +-
.../GrailsLayoutViewResolverPostProcessor.groovy | 82 +++++
.../plugins/web/GroovyPagesGrailsPlugin.groovy | 28 +-
.../views/gsp/layout/FactoryHolderTests.groovy | 71 ++++
.../layout/FullGrailsLayoutLifeCycleTests.groovy | 270 ++++++++++++++
...sLayoutLifeCycleWithNoPreprocessingTests.groovy | 47 +++
.../gsp/layout/GSPGrailsLayoutPageTests.groovy | 164 +++++++++
.../web/pages/GrailsLayoutPreprocessorTests.groovy | 165 +++++++++
.../groovy/org/grails/web/pages/ParseSpec.groovy | 3 -
.../web/taglib/AbstractGrailsTagTests.groovy | 37 ++
.../org/grails/web/taglib/RenderTagLibTests.groovy | 77 ++++
grails-gsp/spring-boot/build.gradle | 7 +-
.../java/grails/gsp/boot/GspAutoConfiguration.java | 59 +---
.../functionaltests/AsyncFunctionalSpec.groovy | 3 -
.../groovy/functionaltests/ForwardingSpec.groovy | 2 -
.../layout/LayoutFunctionalSpec.groovy | 4 -
.../groovy/pubsub/demo/TaskControllerSpec.groovy | 5 -
grails-test-examples/gsp-layout/README.md | 7 +-
grails-test-examples/gsp-layout/build.gradle | 1 -
.../gsp-layout/grails-app/conf/logback-spring.xml | 1 -
.../gsp-layout/grails-app/views/index.gsp | 3 +-
.../integration-test/groovy/EndToEndSpec.groovy | 3 +-
.../app/src/main/resources/application.properties | 2 +-
.../gsp-spring-boot/script/application.properties | 3 +-
grails-test-suite-uber/build.gradle | 1 +
.../org/grails/web/config/http/GrailsFilters.java | 1 -
.../main/groovy/org/grails/web/util/WebUtils.java | 1 +
licenses/LICENSE-opensymphony.txt | 49 +++
settings.gradle | 6 +-
83 files changed, 4067 insertions(+), 816 deletions(-)
diff --git a/LICENSE b/LICENSE
index 7a2940944f..eba0111938 100644
--- a/LICENSE
+++ b/LICENSE
@@ -263,4 +263,11 @@ This product includes the following files from Spring Framework 5.3, developed b
See the above Apache License 2.0 for the terms governing these files.
+--------------------------------------------------------------------------
+
+This product includes software developed by the OpenSymphony Group (http://www.opensymphony.com/). It uses Sitemesh2,
+licensed under the OpenSymphony Software License, Version 1.1.
+
+See licenses/LICENSE-opensymphony.txt for the full license terms.
+
--------------------------------------------------------------------------
\ No newline at end of file
diff --git a/NOTICE b/NOTICE
index 05b97acc04..b78f70d549 100644
--- a/NOTICE
+++ b/NOTICE
@@ -25,4 +25,7 @@ http://www.oracle.com/webfolder/technetwork/jsc/xml/ns/javaee/index.html
This product includes software from Spring Framework 5.3, developed by Pivotal Software, Inc.
Copyright 2002-2020 the original author or authors.
-See the LICENSE file for the Apache License 2.0 terms.
\ No newline at end of file
+See the LICENSE file for the Apache License 2.0 terms.
+
+This product includes software developed by the OpenSymphony Group (http://www.opensymphony.com/). It uses Sitemesh2,
+licensed under the OpenSymphony Software License, Version 1.1. See licenses/LICENSE-opensymphony.txt for the full license terms.
\ No newline at end of file
diff --git a/RENAME.md b/RENAME.md
index 3feb3fc278..824c7cc2b2 100644
--- a/RENAME.md
+++ b/RENAME.md
@@ -121,7 +121,7 @@ Below is a reference of all migrated artifacts - both their old and new name.
| org.grails.plugins | scaffolding | org.apache.grails | grails-scaffolding | | | grails-views |
| org.grails | grails-plugin-services | org.apache.grails | grails-services | | | grails-core |
| org.grails | grails-shell | org.apache.grails | grails-shell-cli | | | grails-core |
-| org.grails | grails-plugin-sitemesh3 | org.apache.grails | grails-sitemesh3 | | | grails-views |
+| org.grails.plugins | sitemesh2 | org.apache.grails | grails-layout | | | grails-gsp |
| org.grails | grails-spring | org.apache.grails | grails-spring | | | grails-core |
| org.grails.plugins | spring-security-acl | org.apache.grails | grails-spring-security-acl | | | grails-spring-security |
| org.grails.plugins | spring-security-cas | org.apache.grails | grails-spring-security-cas | | | grails-spring-security |
diff --git a/UPGRADE7.md b/UPGRADE7.md
index 05141c23aa..c628675b43 100644
--- a/UPGRADE7.md
+++ b/UPGRADE7.md
@@ -48,6 +48,5 @@ Experienced while upgrading modules for Grails 7
### Cool New Features
- Hello Exterminator, Good by bugs! Lot's of things started working... and working well! For instance, use of controller namespaces now work seemlessly.
-- Massive decoupling of dependencies and cleanup between modules. SiteMesh dependencies are no longer compiled into controllers fused between numerous modules. SiteMesh isn't even required to use Grails!
-- GSP can now be used OUTSIDE of Grails! see grails-boot
+- Massive decoupling of dependencies and cleanup between modules.
- Works with Spring Security 6 out of the box. No plugin needed!
diff --git a/dependencies.gradle b/dependencies.gradle
index 4aa4a75120..d17c1df51f 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -93,6 +93,7 @@ ext {
'rxjava3.version' : '3.1.10',
'selenium.version' : '4.25.0',
'spock.version' : '2.3-groovy-4.0',
+ 'sitemesh.version' : '2.6.0',
]
// Note: the name of the dependency must be the prefix of the property name so properties in the pom are resolved correctly
@@ -123,6 +124,7 @@ ext {
'rxjava' : "io.reactivex:rxjava:${bomDependencyVersions['rxjava.version']}",
'rxjava2' : "io.reactivex.rxjava2:rxjava:${bomDependencyVersions['rxjava2.version']}",
'rxjava3' : "io.reactivex.rxjava3:rxjava:${bomDependencyVersions['rxjava3.version']}",
+ 'sitemesh' : "opensymphony:sitemesh:${bomDependencyVersions['sitemesh.version']}",
]
// Because pom exclusions aren't properly supported by gradle, we can't inherit the grails-gradle-bom
diff --git a/etc/bin/rename_gradle_artifacts.sh b/etc/bin/rename_gradle_artifacts.sh
index 87187d8133..471fdc2689 100755
--- a/etc/bin/rename_gradle_artifacts.sh
+++ b/etc/bin/rename_gradle_artifacts.sh
@@ -160,7 +160,9 @@ declare -a views_mappings=(
"org[.]grails:grails-web-gsp-taglib|org.apache.grails.views:grails-web-gsp-taglib"
"org[.]grails:grails-web-gsp|org.apache.grails.views:grails-web-gsp"
"org[.]grails:grails-taglib|org.apache.grails.views:grails-taglib"
- "org[.]grails:grails-plugin-sitemesh3|org.apache.grails:grails-sitemesh3"
+ #"org[.]grails:grails-plugin-sitemesh3|org.apache.grails:grails-sitemesh3"
+ "org[.]grails[.].plugins:sitemesh2|org.apache.grails:grails-layout"
+ "org[.]grails:grails-web-sitemesh|org.apache.grails:grails-web-layout"
"org[.]grails:grails-gsp|org.apache.grails.views:grails-gsp-core"
)
declare -a excluded_gsp_mappings=(
@@ -171,7 +173,9 @@ declare -a views_mappings=(
"exclude[[:space:]]+module[[:space:]]*:[[:space:]]*['\"]scaffolding['\"]|exclude module:'grails-scaffolding'"
"exclude[[:space:]]+module[[:space:]]*:[[:space:]]*['\"]gsp['\"]|exclude module:'grails-gsp'"
"exclude[[:space:]]+module[[:space:]]*:[[:space:]]*['\"]grails-web-testing-support['\"]|exclude module:'grails-testing-support-web'"
- "exclude[[:space:]]+module[[:space:]]*:[[:space:]]*['\"]grails-plugin-sitemesh3['\"]|exclude module:'grails-sitemesh3'"
+ #"exclude[[:space:]]+module[[:space:]]*:[[:space:]]*['\"]grails-plugin-sitemesh3['\"]|exclude module:'grails-sitemesh3'"
+ "exclude[[:space:]]+module[[:space:]]*:[[:space:]]*['\"]sitemesh2['\"]|exclude module:'grails-layout'"
+ "exclude[[:space:]]+module[[:space:]]*:[[:space:]]*['\"]grails-web-sitemesh['\"]|exclude module:'grails-web-layout'"
"exclude[[:space:]]+module[[:space:]]*:[[:space:]]*['\"]grails-gsp['\"]|exclude module:'grails-gsp-core'"
"exclude[[:space:]]+module[[:space:]]*:[[:space:]]*['\"]fields['\"]|exclude module:'grails-fields'"
)
diff --git a/gradle.properties b/gradle.properties
index 28b28db723..84a4d121d3 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -35,7 +35,6 @@ joddWotVersion=3.3.8
elApiVersion=5.0.1
defaultElImplementationVersion=5.0.0
jspApiVersion=4.0.0
-sitemeshVersion=3.2.2
javassistVersion=3.30.2-GA
yakworksHibernateGroovyProxyVersion=1.1
picocliVersion=4.7.6
diff --git a/gradle/publish-root-config.gradle b/gradle/publish-root-config.gradle
index 9a5b7a548c..d665560586 100644
--- a/gradle/publish-root-config.gradle
+++ b/gradle/publish-root-config.gradle
@@ -67,7 +67,7 @@ def publishedProjects = [
'grails-scaffolding',
'grails-services',
'grails-shell-cli',
- 'grails-sitemesh3',
+ 'grails-layout',
'grails-spring',
'grails-taglib',
'grails-test-core',
@@ -87,6 +87,7 @@ def publishedProjects = [
'grails-web-gsp',
'grails-web-gsp-taglib',
'grails-web-jsp',
+ 'grails-web-layout',
'grails-web-mvc',
'grails-web-taglib',
'grails-web-url-mappings',
diff --git a/grails-controllers/build.gradle b/grails-controllers/build.gradle
index 457a8f9748..3da7ede842 100644
--- a/grails-controllers/build.gradle
+++ b/grails-controllers/build.gradle
@@ -35,6 +35,7 @@ dependencies {
api project(':grails-mimetypes')
api project(':grails-validation')
api project(':grails-domain-class')
+ api project(':grails-web-layout')
api 'org.apache.groovy:groovy'
api 'org.springframework.boot:spring-boot-autoconfigure'
diff --git a/grails-doc/src/en/guide/conf/config/builtInOptions.adoc b/grails-doc/src/en/guide/conf/config/builtInOptions.adoc
index 4961711b69..5cbc2b7799 100644
--- a/grails-doc/src/en/guide/conf/config/builtInOptions.adoc
+++ b/grails-doc/src/en/guide/conf/config/builtInOptions.adoc
@@ -32,6 +32,6 @@ On the runtime front, i.e. `grails-app/conf/application.yml`, there are quite a
* `grails.mime.file.extensions` - Whether to use the file extension to dictate the mime type in link:theWebLayer.html#contentNegotiation[Content Negotiation] (default: true).
* `grails.mime.types` - A map of supported mime types used for link:theWebLayer.html#contentNegotiation[Content Negotiation].
* `grails.serverURL` - A string specifying the server URL portion of absolute links, including server name e.g. grails.serverURL="http://my.yourportal.com". See link:{gspdocs}/ref/Tags/createLink.html[createLink]. Also used by redirects.
-* `grails.views.gsp.sitemesh.preprocess` - Determines whether SiteMesh preprocessing happens. Disabling this slows down page rendering, but if you need SiteMesh to parse the generated HTML from a GSP view then disabling it is the right option. Don't worry if you don't understand this advanced property: leave it set to true.
+* `grails.views.gsp.layout.preprocess` - Determines whether layout preprocessing happens. Disabling this slows down page rendering, but if you need Grails' layout implementation to parse the generated HTML from a GSP view, then disabling it is the right option. Don't worry if you don't understand this advanced property: leave it set to true.
* `grails.reload.excludes` and `grails.reload.includes` - Configuring these directives determines the reload behavior for project specific source files. Each directive takes a list of strings that are the class names for project source files that should be excluded from reloading behavior or included accordingly when running the application in development with the `bootRun` task. If the `grails.reload.includes` directive is configured, then only the classes in that list will be reloaded.
diff --git a/grails-doc/src/en/guide/theWebLayer/gsp/layouts.adoc b/grails-doc/src/en/guide/theWebLayer/gsp/layouts.adoc
index 208af504ef..54d9858148 100644
--- a/grails-doc/src/en/guide/theWebLayer/gsp/layouts.adoc
+++ b/grails-doc/src/en/guide/theWebLayer/gsp/layouts.adoc
@@ -20,8 +20,7 @@ under the License.
==== Creating Layouts
-
-Grails leverages https://github.com/sitemesh[Sitemesh], a decorator engine, to support view layouts. Layouts are located in the `grails-app/views/layouts` directory. A typical layout can be seen below:
+This product includes software developed by the OpenSymphony Group (http://www.opensymphony.com/). The Sitemesh framework is used to provide a decorator engine, to support view layouts. Layouts are located in the `grails-app/views/layouts` directory. A typical layout can be seen below:
[source,xml]
----
@@ -129,13 +128,13 @@ Alternatively, you can create a layout called `grails-app/views/layouts/book/lis
If you have both the above-mentioned layouts in place the layout specific to the action will take precedence when the list action is executed.
If a layout is not located using any of those conventions, the convention of last resort is to look for the application default layout which
-is `grails-app/views/layouts/application.gsp`. The name of the application default layout may be changed by defining the property `grails.sitemesh.default.layout`
+is `grails-app/views/layouts/application.gsp`. The name of the application default layout may be changed by defining the property `grails.views.layout.default`
in the application configuration as follows:
[source,yaml]
.grails-app/conf/application.yml
----
-grails.sitemesh.default.layout: myLayoutName
+grails.views.layout.default: myLayoutName
----
With that property in place, the application default layout will be `grails-app/views/layouts/myLayoutName.gsp`.
@@ -144,7 +143,7 @@ With that property in place, the application default layout will be `grails-app/
==== Inline Layouts
-Grails' also supports Sitemesh's concept of inline layouts with the xref:../../../ref/Tags%20-%20GSP/applyLayout.adoc[applyLayout] tag. This can be used to apply a layout to a template, URL or arbitrary section of content. This lets you even further modularize your view structure by "decorating" your template includes.
+Grails also supports inline layouts with the xref:../../../ref/Tags%20-%20GSP/applyLayout.adoc[applyLayout] tag. This can be used to apply a layout to a template, URL or arbitrary section of content. This lets you even further modularize your view structure by "decorating" your template includes.
Some examples of usage can be seen below:
diff --git a/grails-doc/src/en/guide/upgrading/upgrading60x.adoc b/grails-doc/src/en/guide/upgrading/upgrading60x.adoc
index 7759cbd3d2..27f3d0410a 100644
--- a/grails-doc/src/en/guide/upgrading/upgrading60x.adoc
+++ b/grails-doc/src/en/guide/upgrading/upgrading60x.adoc
@@ -294,6 +294,27 @@ The previous method using WebDriver binaries remains supported for backward comp
The asset pipeline has a new https://github.com/wondrify/asset-pipeline[home]. Version 5.0.12 and forward is compatible with Grails 7.x.
+====== 12.12 Layout Configuration
+
+There are several changes related to layout related configuration:
+
+Package Changes:
+* grails.web.sitemesh -> org.apache.grails.web.layout
+* org.grails.web.sitemesh -> org.apache.grails.views.gsp.layout
+
+Notable Class Moves:
+* org.grails.web.servlet.view.GrailsLayoutViewResolver -> org.grails.web.servlet.view.EmbeddedGrailsLayoutViewResolver
+* org.grails.web.servlet.view.SitemeshLayoutViewResolver -> org.grails.web.servlet.view.GrailsLayoutViewResolver
+* org.grails.web.sitemesh.GrailsLayoutView -> org.apache.grails.web.layout.EmbeddedGrailsLayoutView
+* org.grails.web.sitemesh.SitemeshLayoutView -> org.apache.grails.web.layout.GrailsLayoutView
+
+Property Changes:
+* grails.sitemesh.default.layout -> grails.views.layout.default
+* grails.sitemesh.enable.nongsp -> grails.views.layout.enable.nongsp
+
+Tag namespace changes:
+* sitemesh -> grailsLayout
+
====== Key Features
By extending your test classes with `ContainerGebSpec`, your tests will automatically leverage a containerized browser provided by https://www.testcontainers.org[Testcontainers]. This setup eliminates the need for managing browser versions and ensures consistent test environments.
diff --git a/grails-doc/src/en/ref/Tags - GSP/layoutHead.adoc b/grails-doc/src/en/ref/Tags - GSP/layoutHead.adoc
index 8d8c789dad..55142f05b0 100644
--- a/grails-doc/src/en/ref/Tags - GSP/layoutHead.adoc
+++ b/grails-doc/src/en/ref/Tags - GSP/layoutHead.adoc
@@ -24,7 +24,7 @@ under the License.
=== Purpose
-Used in layouts to render the contents of the head tag of the decorated page. Equivalent to the SiteMesh `<decorator:head />` tag.
+Used in layouts to render the contents of the head tag of the decorated page.
=== Examples
diff --git a/grails-doc/src/en/ref/Tags - GSP/layoutTitle.adoc b/grails-doc/src/en/ref/Tags - GSP/layoutTitle.adoc
index e689f19916..39035e6607 100644
--- a/grails-doc/src/en/ref/Tags - GSP/layoutTitle.adoc
+++ b/grails-doc/src/en/ref/Tags - GSP/layoutTitle.adoc
@@ -25,7 +25,7 @@ under the License.
=== Purpose
-Used in layouts to render the contents of the title tag of the decorated page. Equivalent to the SiteMesh `<decorator:title />` tag.
+Used in layouts to render the contents of the title tag of the decorated page.
=== Examples
diff --git a/grails-doc/src/en/ref/Tags - GSP/pageProperty.adoc b/grails-doc/src/en/ref/Tags - GSP/pageProperty.adoc
index ed4441ce39..eb99bb9fd4 100644
--- a/grails-doc/src/en/ref/Tags - GSP/pageProperty.adoc
+++ b/grails-doc/src/en/ref/Tags - GSP/pageProperty.adoc
@@ -25,7 +25,7 @@ under the License.
=== Purpose
-Used in layouts to output the contents a property of the decorated page. Equivalent to the SiteMesh `<decorator:getProperty/>` tag.
+Used in layouts to output the contents a property of the decorated page.
=== Examples
diff --git a/grails-gradle/docs-core/src/test/resources/docs/guide/single.html b/grails-gradle/docs-core/src/test/resources/docs/guide/single.html
index d3b4282875..8959896c66 100644
--- a/grails-gradle/docs-core/src/test/resources/docs/guide/single.html
+++ b/grails-gradle/docs-core/src/test/resources/docs/guide/single.html
@@ -3679,7 +3679,7 @@ In Groovy code you must use single quotes around the string for the value of the
<p><code>grails.serverURL</code> - A string specifying the server URL portion of absolute links, including server name e.g. grails.serverURL="http://my.yourportal.com". See <a href="../ref/Tags/createLink.html">createLink</a>. Also used by redirects.</p>
</li>
<li>
-<p><code>grails.views.gsp.sitemesh.preprocess</code> - Determines whether SiteMesh preprocessing happens. Disabling this slows down page rendering, but if you need SiteMesh to parse the generated HTML from a GSP view then disabling it is the right option. Don’t worry if you don’t understand this advanced property: leave it set to true.</p>
+<p><code>grails.views.gsp.layout.preprocess</code> - Determines whether Layout preprocessing happens. Disabling this slows down page rendering, but if you need layouts to parse the generated HTML from a GSP view then disabling it is the right option. Don’t worry if you don’t understand this advanced property: leave it set to true.</p>
</li>
<li>
<p><code>grails.reload.excludes</code> and <code>grails.reload.includes</code> - Configuring these directives determines the reload behavior for project specific source files. Each directive takes a list of strings that are the class names for project source files that should be excluded from reloading behavior or included accordingly when running the application in development with the <code>run-app</code> command. If the <code>grails.reload.includes</code> directive is configured, then [...]
@@ -10374,7 +10374,7 @@ in <code>grails-app/conf/application.groovy</code> as follows:</p>
</div>
<div class="listingblock">
<div class="content">
-<pre class="CodeRay highlight"><code data-lang="groovy">grails.sitemesh.default.layout = <span class="string"><span class="delimiter">'</span><span class="content">myLayoutName</span><span class="delimiter">'</span></span></code></pre>
+<pre class="CodeRay highlight"><code data-lang="groovy">grails.views.layout.default = <span class="string"><span class="delimiter">'</span><span class="content">myLayoutName</span><span class="delimiter">'</span></span></code></pre>
</div>
</div>
<div class="paragraph">
diff --git a/grails-gradle/docs-core/src/test/resources/docs/ref/Tags/layoutHead.html b/grails-gradle/docs-core/src/test/resources/docs/ref/Tags/layoutHead.html
index f26f5c0b51..67ed102f18 100644
--- a/grails-gradle/docs-core/src/test/resources/docs/ref/Tags/layoutHead.html
+++ b/grails-gradle/docs-core/src/test/resources/docs/ref/Tags/layoutHead.html
@@ -136,7 +136,7 @@ function addJsClass(el) {
<div class="sect2">
<h3 id="_purpose">Purpose</h3>
<div class="paragraph">
-<p>Used in layouts to render the contents of the head tag of the decorated page. Equivalent to the SiteMesh <code><decorator:head /></code> tag.</p>
+<p>Used in layouts to render the contents of the head tag of the decorated page.
</div>
</div>
<div class="sect2">
diff --git a/grails-gradle/docs-core/src/test/resources/docs/ref/Tags/layoutTitle.html b/grails-gradle/docs-core/src/test/resources/docs/ref/Tags/layoutTitle.html
index 4d7dca8c7d..7207b1823a 100644
--- a/grails-gradle/docs-core/src/test/resources/docs/ref/Tags/layoutTitle.html
+++ b/grails-gradle/docs-core/src/test/resources/docs/ref/Tags/layoutTitle.html
@@ -136,7 +136,7 @@ function addJsClass(el) {
<div class="sect2">
<h3 id="_purpose">Purpose</h3>
<div class="paragraph">
-<p>Used in layouts to render the contents of the title tag of the decorated page. Equivalent to the SiteMesh <code><decorator:title /></code> tag.</p>
+<p>Used in layouts to render the contents of the title tag of the decorated page.
</div>
</div>
<div class="sect2">
diff --git a/grails-gradle/docs-core/src/test/resources/docs/ref/Tags/pageProperty.html b/grails-gradle/docs-core/src/test/resources/docs/ref/Tags/pageProperty.html
index 32c5ee9260..a515cd34ae 100644
--- a/grails-gradle/docs-core/src/test/resources/docs/ref/Tags/pageProperty.html
+++ b/grails-gradle/docs-core/src/test/resources/docs/ref/Tags/pageProperty.html
@@ -136,7 +136,7 @@ function addJsClass(el) {
<div class="sect2">
<h3 id="_purpose">Purpose</h3>
<div class="paragraph">
-<p>Used in layouts to output the contents of a property of the decorated page. Equivalent to the SiteMesh <code><decorator:getProperty/></code> tag.</p>
+<p>Used in layouts to output the contents of a property of the decorated page.
</div>
</div>
<div class="sect2">
diff --git a/grails-gsp/core/src/main/groovy/org/grails/gsp/compiler/GrailsLayoutPreprocessor.java b/grails-gsp/core/src/main/groovy/org/grails/gsp/compiler/GrailsLayoutPreprocessor.java
new file mode 100644
index 0000000000..0592aa1ec4
--- /dev/null
+++ b/grails-gsp/core/src/main/groovy/org/grails/gsp/compiler/GrailsLayoutPreprocessor.java
@@ -0,0 +1,95 @@
+/*
+ * 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.gsp.compiler;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Adds GSP Grails Layout integration directly to compiled GSP.
+ * <p>
+ * head, meta, title, body and content tags are replaced with <grailsLayout:capture*>...</grailsLayout:capture*> taglibs
+ * <p>
+ * The taglib is used to capture the content of each tag. This prevents the need to parse the content output like Sitemesh normally does.
+ *
+ * @author <a href="mailto:lari.hotari@sagire.fi">Lari Hotari, Sagire Software Oy</a>
+ */
+public class GrailsLayoutPreprocessor {
+
+ Pattern parameterPattern = Pattern.compile("<parameter(\\s+name[^>]+?)(/*?)>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
+ Pattern metaPattern = Pattern.compile("<meta(\\s[^>]+?)(/*?)>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
+ Pattern titlePattern = Pattern.compile("<title(\\s[^>]*)?>(.*?)</title>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
+ Pattern headPattern = Pattern.compile("<head(\\s[^>]*)?>(.*?)</head>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
+ Pattern bodyPattern = Pattern.compile("<body(\\s[^>]*)?>(.*?)</body>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
+ Pattern contentPattern = Pattern.compile("<content(\\s+tag[^>]+)>(.*?)</content>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
+ public static final String XML_CLOSING_FOR_EMPTY_TAG_ATTRIBUTE_NAME = "gsp_sm_xmlClosingForEmptyTag";
+
+ public String addGspGrailsLayoutCapturing(String gspSource) {
+ StringBuffer sb = addHeadCapturing(gspSource);
+ sb = addBodyCapturing(sb);
+ sb = addContentCapturing(sb);
+ return sb.toString();
+ }
+
+ StringBuffer addHeadCapturing(String gspSource) {
+ StringBuffer sb = new StringBuffer((int) (gspSource.length() * 1.2));
+ Matcher m = headPattern.matcher(gspSource);
+ if (m.find()) {
+ m.appendReplacement(sb, "");
+ sb.append("<grailsLayout:captureHead");
+ if (m.group(1) != null) {
+ sb.append(m.group(1));
+ }
+ sb.append(">");
+ sb.append(addMetaCapturing(addTitleCapturing(m.group(2))));
+ sb.append("</grailsLayout:captureHead>");
+ m.appendTail(sb);
+ } else if (!bodyPattern.matcher(gspSource).find()) {
+ // no body either, so replace meta & title in the entire gsp source
+ // fix title in sub-template -problem
+ sb.append(addMetaCapturing(addTitleCapturing(gspSource)));
+ } else {
+ sb.append(gspSource);
+ }
+ return sb;
+ }
+
+ String addMetaCapturing(String headContent) {
+ Matcher m = metaPattern.matcher(headContent);
+ final String result = parameterPattern.matcher(
+ m.replaceAll("<grailsLayout:captureMeta " + XML_CLOSING_FOR_EMPTY_TAG_ATTRIBUTE_NAME + "=\"$2\"$1/>")
+ ).replaceAll("<grailsLayout:parameter$1/>");
+ return result;
+ }
+
+ String addTitleCapturing(String headContent) {
+ Matcher m = titlePattern.matcher(headContent);
+ return m.replaceAll("<grailsLayout:wrapTitleTag><grailsLayout:captureTitle$1>$2</grailsLayout:captureTitle></grailsLayout:wrapTitleTag>");
+ }
+
+ StringBuffer addBodyCapturing(StringBuffer sb) {
+ Matcher m = bodyPattern.matcher(sb);
+ return new StringBuffer(m.replaceAll("<grailsLayout:captureBody$1>$2</grailsLayout:captureBody>"));
+ }
+
+ StringBuffer addContentCapturing(StringBuffer sb) {
+ Matcher m = contentPattern.matcher(sb);
+ return new StringBuffer(m.replaceAll("<grailsLayout:captureContent$1>$2</grailsLayout:captureContent>"));
+ }
+}
diff --git a/grails-gsp/core/src/main/groovy/org/grails/gsp/compiler/GroovyPageParser.java b/grails-gsp/core/src/main/groovy/org/grails/gsp/compiler/GroovyPageParser.java
index 29253d612e..20c29cdb51 100644
--- a/grails-gsp/core/src/main/groovy/org/grails/gsp/compiler/GroovyPageParser.java
+++ b/grails-gsp/core/src/main/groovy/org/grails/gsp/compiler/GroovyPageParser.java
@@ -102,6 +102,7 @@ public class GroovyPageParser implements Tokens {
private GrailsTagRegistry tagRegistry = GrailsTagRegistry.getInstance();
private Environment environment;
private List<String> htmlParts = new ArrayList<String>();
+ private static GrailsLayoutPreprocessor grailsLayoutPreprocessor = new GrailsLayoutPreprocessor();
Set<Integer> bodyVarsDefined = new HashSet<Integer>();
Map<Integer, String> attrsVarsMapDefinition = new HashMap<Integer, String>();
@@ -142,7 +143,7 @@ public class GroovyPageParser implements Tokens {
public static final String CONFIG_PROPERTY_DEFAULT_CODEC = "grails.views.default.codec";
public static final String CONFIG_PROPERTY_GSP_ENCODING = "grails.views.gsp.encoding";
public static final String CONFIG_PROPERTY_GSP_KEEPGENERATED_DIR = "grails.views.gsp.keepgenerateddir";
-
+ public static final String CONFIG_PROPERTY_GSP_GRAILS_LAYOUT_PREPROCESS = "grails.views.gsp.layout.preprocess";
public static final String CONFIG_PROPERTY_GSP_COMPILESTATIC = "grails.views.gsp.compileStatic";
public static final String CONFIG_PROPERTY_GSP_ALLOWED_TAGLIB_NAMESPACES = "grails.views.gsp.compileStaticConfig.taglibs";
public static final String CONFIG_PROPERTY_GSP_CODECS = "grails.views.gsp.codecs";
@@ -155,6 +156,7 @@ public class GroovyPageParser implements Tokens {
private static final String STATIC_CODEC_DIRECTIVE = OutputEncodingSettings.STATIC_CODEC_NAME + CODEC_DIRECTIVE_POSTFIX;
private static final String OUT_CODEC_DIRECTIVE = OutputEncodingSettings.OUT_CODEC_NAME + CODEC_DIRECTIVE_POSTFIX;
private static final String TAGLIB_CODEC_DIRECTIVE = OutputEncodingSettings.TAGLIB_CODEC_NAME + CODEC_DIRECTIVE_POSTFIX;
+ private static final String GRAILS_LAYOUT_PREPROCESS_DIRECTIVE = "grailsLayoutPreprocess";
private String pluginAnnotation;
public static final String GROOVY_SOURCE_CHAR_ENCODING = "UTF-8";
@@ -163,12 +165,14 @@ public class GroovyPageParser implements Tokens {
private boolean precompileMode;
private Boolean compileStaticMode;
private boolean modelFieldsMode;
+ private boolean grailsLayoutPreprocessMode = false;
private String expressionCodecDirectiveValue = OutputEncodingSettings.getDefaultValue(OutputEncodingSettings.EXPRESSION_CODEC_NAME);
private String outCodecDirectiveValue = OutputEncodingSettings.getDefaultValue(OutputEncodingSettings.OUT_CODEC_NAME);
private String staticCodecDirectiveValue = OutputEncodingSettings.getDefaultValue(OutputEncodingSettings.STATIC_CODEC_NAME);
private String taglibCodecDirectiveValue = OutputEncodingSettings.getDefaultValue(OutputEncodingSettings.TAGLIB_CODEC_NAME);
private String modelDirectiveValue;
+ private boolean enableGrailsLayoutProcessing = true;
private File keepGeneratedDirectory;
private Set<String> allowedTaglibNamespaces = new LinkedHashSet<>(DEFAULT_TAGLIB_NAMESPACES);
@@ -188,6 +192,9 @@ public class GroovyPageParser implements Tokens {
this.keepGeneratedDirectory = keepGeneratedDirectory;
}
+ public void setEnableGrailsLayoutProcessing(boolean enableGrailsLayoutProcessing) {
+ this.enableGrailsLayoutProcessing = enableGrailsLayoutProcessing;
+ }
class TagMeta {
String name;
@@ -229,6 +236,12 @@ public class GroovyPageParser implements Tokens {
configure(configMap);
}
+ Map<String, String> directives = parseDirectives(gspSource);
+ if (isGrailsLayoutPreprocessingEnabled(directives.get(GRAILS_LAYOUT_PREPROCESS_DIRECTIVE))) {
+ // GSP preprocessing for direct grails layout integration: replace head -> g:captureHead, title -> g:captureTitle, meta -> g:captureMeta, body -> g:captureBody
+ gspSource = grailsLayoutPreprocessor.addGspGrailsLayoutCapturing(gspSource);
+ grailsLayoutPreprocessMode = true;
+ }
scan = new GroovyPageScanner(gspSource, uri);
pageName = uri;
environment = Environment.getCurrent();
@@ -253,6 +266,10 @@ public class GroovyPageParser implements Tokens {
allowedTaglibNamespaces.addAll(Arrays.asList(allowedTagLibsConfigValue.toString().split("\\s*,\\s*")));
}
+ setEnableGrailsLayoutProcessing(
+ config.getProperty(GroovyPageParser.CONFIG_PROPERTY_GSP_GRAILS_LAYOUT_PREPROCESS, Boolean.class, true)
+ );
+
setExpressionCodecDirectiveValue(
config.getProperty(OutputEncodingSettings.CONFIG_PROPERTY_GSP_CODECS + '.' + OutputEncodingSettings.EXPRESSION_CODEC_NAME, String.class,
config.getProperty(OutputEncodingSettings.CONFIG_PROPERTY_DEFAULT_CODEC, String.class, OutputEncodingSettings.getDefaultValue(OutputEncodingSettings.EXPRESSION_CODEC_NAME)))
@@ -296,6 +313,13 @@ public class GroovyPageParser implements Tokens {
return result;
}
+ private boolean isGrailsLayoutPreprocessingEnabled(String gspFilePreprocessDirective) {
+ if (gspFilePreprocessDirective != null) {
+ return GrailsStringUtils.toBoolean(gspFilePreprocessDirective.trim());
+ }
+ return enableGrailsLayoutProcessing;
+ }
+
public int[] getLineNumberMatrix() {
return out.getLineNumbers();
}
diff --git a/grails-gsp/spring-boot/build.gradle b/grails-gsp/grails-layout/build.gradle
similarity index 55%
copy from grails-gsp/spring-boot/build.gradle
copy to grails-gsp/grails-layout/build.gradle
index b73dc4330e..26faafebcc 100644
--- a/grails-gsp/spring-boot/build.gradle
+++ b/grails-gsp/grails-layout/build.gradle
@@ -16,32 +16,43 @@
* specific language governing permissions and limitations
* under the License.
*/
-
plugins {
+ id 'groovy'
id 'java-library'
+ id 'project-report'
+ id 'org.apache.grails.gradle.grails-plugin'
}
version = projectVersion
-group = 'org.apache.grails'
-
-apply plugin: 'org.apache.grails.gradle.grails-plugin'
+group = 'org.apache.grails.views'
ext {
- pomTitle = 'GSP Spring Boot Plugin'
- pomDescription = 'Provides GSP for Spring Boot Applications'
- pomDevelopers = ['graemerocher': 'Graeme Rocher', lhotari: 'Lari Hotari', codeconsole: 'Scott Murphy Heiberg']
+ pomTitle = 'Layout Grails Plugin'
+ pomDescription = 'Provides layout & decoration features for the Grails Framework.'
}
dependencies {
+ astImplementation platform(project(':grails-bom'))
+ //astImplementation project(':grails-web-core') // TODO: What ast transforms are needed on the tag lib?
+
implementation platform(project(':grails-bom'))
- api project(':grails-sitemesh3')
+ implementation 'org.apache.groovy:groovy'
+ implementation project(':grails-core') // for plugin
+ implementation project(':grails-gsp-core') // for grails layout preprocessor
+ api project(':grails-web-taglib')
+ runtimeOnly project(':grails-web-jsp')
+
+ api project(':grails-web-layout')
+
+ testImplementation 'org.spockframework:spock-core'
+ testRuntimeOnly 'org.slf4j:slf4j-nop' // Get rid of warning about missing slf4j implementation during test task
}
apply {
// java-configuration must be applied first since tasks are now lazy registered
from rootProject.layout.projectDirectory.file('gradle/java-config.gradle')
- //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/publish-config.gradle')
from rootProject.layout.projectDirectory.file('gradle/test-config.gradle')
from rootProject.layout.projectDirectory.file('gradle/grails-extension-gradle-config.gradle')
}
\ No newline at end of file
diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/AsyncFunctionalSpec.groovy b/grails-gsp/grails-layout/src/main/groovy/org/grails/plugins/web/GrailsLayoutGrailsPlugin.groovy
similarity index 55%
copy from grails-test-examples/app1/src/integration-test/groovy/functionaltests/AsyncFunctionalSpec.groovy
copy to grails-gsp/grails-layout/src/main/groovy/org/grails/plugins/web/GrailsLayoutGrailsPlugin.groovy
index 9db409059e..844f5533fa 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/AsyncFunctionalSpec.groovy
+++ b/grails-gsp/grails-layout/src/main/groovy/org/grails/plugins/web/GrailsLayoutGrailsPlugin.groovy
@@ -17,29 +17,26 @@
* under the License.
*/
-package functionaltests
+package org.grails.plugins.web
-import grails.gorm.transactions.Rollback
-import grails.plugin.geb.ContainerGebSpec
-import grails.testing.mixin.integration.Integration
+import org.grails.plugins.web.taglib.RenderGrailsLayoutTagLib
+import org.grails.plugins.web.taglib.GrailsLayoutTagLib
-import spock.lang.PendingFeature
+import grails.plugins.Plugin
+import grails.util.GrailsUtil
+import groovy.util.logging.Slf4j
/**
- * @author graemerocher
+ * Plugin responsible for Grails Layout specific configuration.
*/
-@Integration(applicationClass = Application)
-@Rollback
-class AsyncFunctionalSpec extends ContainerGebSpec {
-
- @PendingFeature(reason = 'pageSource == <html><head></head><body></body></html>')
- void "Test async response rendering works"() {
- when: "When an async response is rendered"
- go('/async/test')
-
- then: "The rendered page is correct"
- pageSource.contains('Hello')
- }
-
+class GrailsLayoutGrailsPlugin extends Plugin {
+ def grailsVersion = '7.0.0-SNAPSHOT > *'
+ def dependsOn = [core: GrailsUtil.getGrailsVersion(), i18n: GrailsUtil.getGrailsVersion()]
+ def observe = ['controllers']
+ def loadBefore = ['groovyPages']
+
+ def providedArtefacts = [
+ RenderGrailsLayoutTagLib,
+ GrailsLayoutTagLib
+ ]
}
-
diff --git a/grails-gsp/grails-layout/src/main/groovy/org/grails/plugins/web/taglib/GrailsLayoutTagLib.groovy b/grails-gsp/grails-layout/src/main/groovy/org/grails/plugins/web/taglib/GrailsLayoutTagLib.groovy
new file mode 100644
index 0000000000..206cb3de99
--- /dev/null
+++ b/grails-gsp/grails-layout/src/main/groovy/org/grails/plugins/web/taglib/GrailsLayoutTagLib.groovy
@@ -0,0 +1,235 @@
+/*
+ * 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.web.taglib
+
+import com.opensymphony.module.sitemesh.RequestConstants
+import grails.artefact.TagLibrary
+import grails.gsp.TagLib
+import groovy.transform.CompileStatic
+import jakarta.servlet.http.HttpServletRequest
+import org.grails.buffer.FastStringWriter
+import org.grails.buffer.GrailsPrintWriter
+import org.grails.buffer.StreamCharBuffer
+import org.grails.encoder.CodecLookup
+import org.grails.encoder.Encoder
+import org.grails.gsp.compiler.GrailsLayoutPreprocessor
+import org.apache.grails.web.layout.GSPGrailsLayoutPage
+
+/**
+ * Internal Grails Layout pre-processor tags.
+ *
+ * @author Graeme Rocher
+ * @since 1.2
+ */
+@CompileStatic
+@TagLib
+class GrailsLayoutTagLib implements RequestConstants, TagLibrary {
+ protected static final String GSP_GRAILS_LAYOUT_PAGE = 'org.apache.grails.web.layout.EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE'
+
+ static String namespace = 'grailsLayout'
+ CodecLookup codecLookup
+
+ def captureTagContent(GrailsPrintWriter writer, String tagname, Map attrs, Object body, boolean noEndTagForEmpty = false) {
+ def content = null
+ if (body != null) {
+ if (body instanceof Closure) {
+ content = body()
+ } else {
+ content = body
+ }
+ }
+
+ if (content instanceof StreamCharBuffer) {
+ content.setPreferSubChunkWhenWritingToOtherBuffer(true)
+ }
+ writer << '<'
+ writer << tagname
+ def useXmlClosingForEmptyTag = false
+ if (attrs) {
+ def xmlClosingString = attrs.remove(GrailsLayoutPreprocessor.XML_CLOSING_FOR_EMPTY_TAG_ATTRIBUTE_NAME)
+ if (xmlClosingString == '/') {
+ useXmlClosingForEmptyTag = true
+ }
+ Encoder htmlEncoder = codecLookup?.lookupEncoder('HTML')
+ attrs.each { k, v ->
+ writer << ' '
+ writer << k
+ writer << '="'
+ writer << (htmlEncoder != null ? htmlEncoder.encode(v) : v)
+ writer << '"'
+ }
+ }
+
+ if (content) {
+ writer << '>'
+ // the following row must be written separately (append StreamCharBuffer gets appended as subchunk)
+ writer << content
+ writer << '</'
+ writer << tagname
+ writer << '>'
+ } else {
+ if (!useXmlClosingForEmptyTag) {
+ writer << '>'
+ // in valid HTML , closing of an empty tag depends on the element name
+ // for empty title, the tag must be closed properly
+ // for empty meta tag shouldn't be closed at all, see GRAILS-5696
+ if (!noEndTagForEmpty) {
+ writer << '</'
+ writer << tagname
+ writer << '>'
+ }
+ } else {
+ // XML / XHTML empty tag
+ writer << '/>'
+ }
+ }
+ content
+ }
+
+ def StreamCharBuffer wrapContentInBuffer(Object content) {
+ if (content instanceof Closure) {
+ content = content()
+ }
+ if (!(content instanceof StreamCharBuffer)) {
+ // the body closure might be a string constant, so wrap it in a StreamCharBuffer in that case
+ FastStringWriter stringWriter = new FastStringWriter()
+ stringWriter.print((Object) content)
+ StreamCharBuffer newbuffer = stringWriter.buffer
+ newbuffer.setPreferSubChunkWhenWritingToOtherBuffer(true)
+ return newbuffer
+ } else {
+ return (StreamCharBuffer) content
+ }
+ }
+
+ /**
+ * Captures the <head> tag.
+ */
+ Closure captureHead = { Map attrs, body ->
+ def content = captureTagContent(out, 'head', attrs, body)
+
+ if (content != null) {
+ def smpage = findGSPGrailsLayoutPage(request)
+ if (smpage) {
+ smpage.setHeadBuffer(wrapContentInBuffer(content))
+ }
+ }
+ }
+
+ protected GSPGrailsLayoutPage findGSPGrailsLayoutPage(HttpServletRequest request) {
+ (GSPGrailsLayoutPage) request.getAttribute(GSP_GRAILS_LAYOUT_PAGE)
+ }
+
+ /**
+ * Allows passing of parameters to Grails layout.<br/>
+ *
+ * <grailsLayout:parameter name="foo" value="bar" />
+ */
+ Closure parameter = { Map attrs, body ->
+ def smpage = findGSPGrailsLayoutPage(request)
+ def name = attrs.name?.toString()
+ def val = attrs.value?.toString()
+ if (smpage && name && val != null) {
+ smpage.addProperty("page.$name", val)
+ }
+ }
+
+ /**
+ * Captures the <body> tag.
+ */
+ Closure captureBody = { Map attrs, body ->
+ def content = captureTagContent(out, 'body', attrs, body)
+ if (content != null) {
+ def smpage = findGSPGrailsLayoutPage(request)
+ if (smpage) {
+ smpage.setBodyBuffer(wrapContentInBuffer(content))
+ if (attrs) {
+ attrs.each { k, v ->
+ smpage.addProperty("body.${k?.toString()?.toLowerCase()}", v?.toString())
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Captures the individual <content> tags.
+ */
+ Closure captureContent = { Map attrs, body ->
+ if (body != null) {
+ def smpage = findGSPGrailsLayoutPage(request)
+ if (smpage && attrs.tag) {
+ smpage.setContentBuffer(attrs.tag as String, wrapContentInBuffer(body))
+ }
+ }
+ }
+
+ /**
+ * Captures the individual <meta> tags.
+ */
+ Closure captureMeta = { Map attrs, body ->
+ def content = captureTagContent(out, 'meta', attrs, body, true)
+ def smpage = findGSPGrailsLayoutPage(request)
+ def val = attrs.content?.toString()
+ if (attrs && smpage && val != null) {
+ if (attrs.name) {
+ smpage.addProperty("meta.${attrs.name}", val)
+ smpage.addProperty("meta.${attrs.name.toString().toLowerCase()}", val)
+ } else if (attrs['http-equiv']) {
+ String httpEquiv = attrs['http-equiv'] as String
+ def httpEquivFormats = [httpEquiv, httpEquiv.toLowerCase()]
+ if (httpEquiv.equalsIgnoreCase('content-type')) {
+ httpEquivFormats << 'Content-Type'
+ }
+ for (def httpEquivFormat : httpEquivFormats) {
+ smpage.addProperty("meta.http-equiv.${httpEquivFormat}", val)
+ }
+ }
+ }
+ }
+
+ /**
+ * Captures the <title> tag.
+ */
+ Closure captureTitle = { Map attrs, body ->
+ def smpage = findGSPGrailsLayoutPage(request)
+ def content = captureTagContent(out, 'title', attrs, body)
+ if (smpage && content != null) {
+ smpage.addProperty('title', content?.toString())
+ smpage.setTitleCaptured(true)
+ }
+ }
+
+ /**
+ * Wraps the title tag so that the buffer can be cleared out from the head buffer
+ */
+ Closure wrapTitleTag = { Map attrs, body ->
+ if (body != null) {
+ def smpage = findGSPGrailsLayoutPage(request)
+ if (smpage) {
+ def wrapped = wrapContentInBuffer(body)
+ smpage.setTitleBuffer(wrapped)
+ out << wrapped
+ } else if (body instanceof Closure) {
+ out << body()
+ }
+ }
+ }
+}
diff --git a/grails-gsp/grails-layout/src/main/groovy/org/grails/plugins/web/taglib/RenderGrailsLayoutTagLib.groovy b/grails-gsp/grails-layout/src/main/groovy/org/grails/plugins/web/taglib/RenderGrailsLayoutTagLib.groovy
new file mode 100644
index 0000000000..24b4a65265
--- /dev/null
+++ b/grails-gsp/grails-layout/src/main/groovy/org/grails/plugins/web/taglib/RenderGrailsLayoutTagLib.groovy
@@ -0,0 +1,346 @@
+/*
+ * 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.web.taglib
+
+import com.opensymphony.module.sitemesh.Decorator
+import com.opensymphony.module.sitemesh.DecoratorMapper
+import com.opensymphony.module.sitemesh.HTMLPage
+import com.opensymphony.module.sitemesh.Page
+import com.opensymphony.module.sitemesh.PageParser
+import com.opensymphony.module.sitemesh.RequestConstants
+import com.opensymphony.module.sitemesh.Factory
+
+import grails.artefact.TagLibrary
+import grails.core.GrailsApplication
+import grails.core.support.GrailsApplicationAware
+import grails.gsp.TagLib
+import grails.util.TypeConvertingMap
+import groovy.text.Template
+import groovy.transform.CompileStatic
+import org.apache.grails.web.layout.FactoryHolder
+import org.apache.grails.web.layout.GSPGrailsLayoutPage
+import org.apache.grails.web.layout.GrailsHTMLPageParser
+import org.apache.grails.web.layout.EmbeddedGrailsLayoutView
+import org.apache.grails.web.layout.GroovyPageLayoutFinder
+import org.apache.grails.web.layout.SpringMVCViewDecorator
+import org.grails.buffer.FastStringWriter
+import org.grails.buffer.StreamCharBuffer
+import org.grails.gsp.GroovyPageTemplate
+import org.grails.gsp.GroovyPagesTemplateEngine
+import org.grails.gsp.compiler.GroovyPageParser
+import org.grails.taglib.TagLibraryLookup
+import org.grails.taglib.TagOutput
+import org.grails.taglib.encoder.OutputContextLookupHelper
+import jakarta.servlet.http.HttpServletRequest
+
+import java.nio.charset.StandardCharsets
+
+/**
+ * Tags to help rendering of views and layouts.
+ *
+ * @author Graeme Rocher
+ */
+@CompileStatic
+@TagLib
+class RenderGrailsLayoutTagLib implements RequestConstants, TagLibrary, GrailsApplicationAware {
+ GroovyPagesTemplateEngine groovyPagesTemplateEngine
+ TagLibraryLookup gspTagLibraryLookup
+ GroovyPageLayoutFinder groovyPageLayoutFinder
+ protected boolean grailsLayoutPreprocessMode = true
+
+ protected HTMLPage getPage() {
+ (HTMLPage) getRequest().getAttribute(PAGE)
+ }
+
+ @Override
+ void setGrailsApplication(GrailsApplication grailsApplication) {
+ grailsLayoutPreprocessMode = grailsApplication.config.getProperty(GroovyPageParser.CONFIG_PROPERTY_GSP_GRAILS_LAYOUT_PREPROCESS, Boolean, true)
+ }
+
+ protected boolean isGrailsLayoutPreprocessMode() {
+ grailsLayoutPreprocessMode
+ }
+
+
+ /**
+ * Apply a layout to a particular block of text or to the given view or template.<br/>
+ *
+ * <g:applyLayout name="myLayout">some text</g:applyLayout><br/>
+ * <g:applyLayout name="myLayout" template="mytemplate" /><br/>
+ * <g:applyLayout name="myLayout" url="http://www.google.com" /><br/>
+ * <g:applyLayout name="myLayout" action="myAction" controller="myController"><br/>
+ *
+ * @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 encoding Optional. The encoding to use
+ * @attr params Optional. The params to pass onto the page object
+ * @attr parse Optional. If true, Grails Layout parser will always be used to parse the content.
+ */
+ Closure applyLayout = { Map attrs, body ->
+ if (!groovyPagesTemplateEngine) throw new IllegalStateException('Property [groovyPagesTemplateEngine] must be set!')
+ def oldPage = getPage()
+ 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 : [:]
+ Object content = null
+ GSPGrailsLayoutPage gspGrailsLayoutPage = null
+ if (attrs.url) {
+ content = new URL(attrs.url as String).getText(StandardCharsets.UTF_8.name())
+ } else if (attrs.action && attrs.controller) {
+ def includeAttrs = [action: attrs.action, controller: attrs.controller, params: pageParams, model: viewModel]
+ content = TagOutput.captureTagOutput(gspTagLibraryLookup, 'g', 'include', includeAttrs, null, OutputContextLookupHelper.lookupOutputContext())
+ } else {
+ def oldGspGrailsLayoutPage = request.getAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE)
+ try {
+ gspGrailsLayoutPage = new GSPGrailsLayoutPage()
+ request.setAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE, gspGrailsLayoutPage)
+ if (attrs.view || attrs.template) {
+ content = TagOutput.captureTagOutput(gspTagLibraryLookup, 'g', 'render', attrs, null, OutputContextLookupHelper.lookupOutputContext())
+ } else {
+ def bodyClosure = TagOutput.createOutputCapturingClosure(this, body, OutputContextLookupHelper.lookupOutputContext())
+ content = bodyClosure()
+ }
+ if (content instanceof StreamCharBuffer) {
+ gspGrailsLayoutPage.pageBuffer = (StreamCharBuffer) content
+ gspGrailsLayoutPage.used = isGrailsLayoutPreprocessMode()
+ } else if (content != null) {
+ def stringWriter = new FastStringWriter()
+ stringWriter.print((Object) content)
+ gspGrailsLayoutPage.pageBuffer = stringWriter.buffer
+ gspGrailsLayoutPage.used = isGrailsLayoutPreprocessMode()
+ }
+ }
+ finally {
+ request.setAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE, oldGspGrailsLayoutPage)
+ }
+ }
+ if (content == null) {
+ content = ''
+ }
+
+ Page page = null
+ if (!((TypeConvertingMap) attrs).boolean('parse') && gspGrailsLayoutPage != null && gspGrailsLayoutPage.used) {
+ page = gspGrailsLayoutPage
+ } else {
+ def parser = createPageParser(contentType)
+ char[] charArray
+ if (content instanceof StreamCharBuffer) {
+ charArray = ((StreamCharBuffer) content).toCharArray()
+ } else {
+ charArray = content.toString().toCharArray()
+ }
+ page = parser.parse(charArray)
+ }
+
+ def decorator = findDecorator(request, attrs.name as String)
+ if (decorator && decorator.page) {
+ pageParams.each { k, v ->
+ page.addProperty(k as String, v as String)
+ }
+ try {
+ request.setAttribute(PAGE, page)
+ def template = findTemplate(decorator)
+ template.make(viewModel).writeTo(out)
+ }
+ finally {
+ request.setAttribute(PAGE, oldPage)
+ }
+ } else {
+ out << content
+ }
+ }
+
+ protected Template findTemplate(Decorator decorator) {
+ Template template
+ if (decorator instanceof SpringMVCViewDecorator) {
+ template = ((SpringMVCViewDecorator) decorator).template
+ if (template instanceof GroovyPageTemplate) {
+ GroovyPageTemplate gpt = (GroovyPageTemplate) template
+ gpt = (GroovyPageTemplate) gpt.clone()
+ gpt.allowSettingContentType = false
+ template = gpt
+ }
+ } else {
+ template = groovyPagesTemplateEngine.createTemplate(decorator.page)
+ }
+ return template
+ }
+
+ protected Decorator findDecorator(HttpServletRequest req, String layoutName) {
+ def decoratorMapper = sitemeshFactory?.decoratorMapper
+ Decorator d
+ if (decoratorMapper) {
+ d = decoratorMapper.getNamedDecorator(req, layoutName)
+ } else {
+ d = groovyPageLayoutFinder.getNamedDecorator(req, layoutName)
+ }
+ return d
+ }
+
+ protected PageParser createPageParser(String contentType) {
+ def parser = sitemeshFactory?.getPageParser(contentType)
+ if (parser == null) {
+ parser = new GrailsHTMLPageParser()
+ }
+ return parser
+ }
+
+ protected Factory getSitemeshFactory() {
+ FactoryHolder.grailsLayoutFactory
+ }
+
+ /**
+ * Used to retrieve a property of the decorated page.<br/>
+ *
+ * <g:pageProperty default="defaultValue" name="body.onload" /><br/>
+ *
+ * @emptyTag
+ *
+ * @attr REQUIRED name the property name
+ * @attr default the default value to use if the property is null
+ * @attr writeEntireProperty if true, writes the property in the form 'foo = "bar"', otherwise renders 'bar'
+ */
+ Closure pageProperty = { Map attrs ->
+ if (!attrs.name) {
+ throwTagError('Tag [pageProperty] is missing required attribute [name]')
+ }
+
+ String propertyName = attrs.name as String
+ def htmlPage = page
+ def propertyValue
+
+ if (htmlPage instanceof GSPGrailsLayoutPage) {
+ // check if there is an component content buffer
+ propertyValue = ((GSPGrailsLayoutPage) htmlPage).getContentBuffer(propertyName)
+ }
+
+ if (!propertyValue) {
+ propertyValue = htmlPage.getProperty(propertyName)
+ }
+
+ if (!propertyValue) {
+ propertyValue = attrs.'default'
+ }
+
+ if (propertyValue) {
+ if (attrs.writeEntireProperty) {
+ out << ' '
+ out << propertyName.substring(propertyName.lastIndexOf('.') + 1)
+ out << '="'
+ out << propertyValue
+ out << '"'
+ } else {
+ out << propertyValue
+ }
+ }
+ }
+
+ /**
+ * Invokes the body of this tag if the page property exists:<br/>
+ *
+ * <g:ifPageProperty name="meta.index">body to invoke</g:ifPageProperty><br/>
+ *
+ * or it equals a certain value:<br/>
+ *
+ * <g:ifPageProperty name="meta.index" equals="blah">body to invoke</g:ifPageProperty>
+ *
+ * @attr name REQUIRED the property name
+ * @attr equals optional value to test against
+ */
+ Closure ifPageProperty = { Map attrs, body ->
+ if (!attrs.name) {
+ return
+ }
+
+ def htmlPage = getPage()
+ List names = ((attrs.name instanceof List) ? (List) attrs.name : [attrs.name])
+
+ def invokeBody = true
+ for (i in 0..<names.size()) {
+ String propertyName = names[i] as String
+ def propertyValue = null
+ if (htmlPage instanceof GSPGrailsLayoutPage) {
+ // check if there is an component content buffer
+ propertyValue = (htmlPage as GSPGrailsLayoutPage).getContentBuffer(propertyName)
+ }
+
+ if (!propertyValue) {
+ propertyValue = htmlPage.getProperty(propertyName)
+ }
+
+ if (propertyValue) {
+ if (attrs.containsKey('equals')) {
+ if (attrs.equals instanceof List) {
+ invokeBody = ((List) attrs.equals)[i] == propertyValue
+ } else {
+ invokeBody = attrs.equals == propertyValue
+ }
+ }
+ } else {
+ invokeBody = false
+ break
+ }
+ }
+ if (invokeBody && body instanceof Closure) {
+ out << body()
+ }
+ }
+
+ /**
+ * Used in layouts to render the page title from the SiteMesh page.<br/>
+ *
+ * <g:layoutTitle default="The Default title" />
+ *
+ * @emptyTag
+ *
+ * @attr default the value to use if the title isn't specified in the GSP
+ */
+ Closure layoutTitle = { Map attrs ->
+ String title = page.title
+ if (!title && attrs.'default') title = attrs.'default'
+ if (title) out << title
+ }
+
+ /**
+ * Used in layouts to render the body of a SiteMesh layout.<br/>
+ *
+ * <g:layoutBody />
+ *
+ * @emptyTag
+ */
+ Closure layoutBody = { Map attrs ->
+ page.writeBody(out)
+ }
+
+ /**
+ * Used in layouts to render the head of a SiteMesh layout.<br/>
+ *
+ * <g:layoutHead />
+ *
+ * @emptyTag
+ */
+ Closure layoutHead = { Map attrs ->
+ page.writeHead(out)
+ }
+}
diff --git a/grails-gsp/grails-sitemesh3/README.md b/grails-gsp/grails-sitemesh3/README.md
deleted file mode 100644
index 2fe24feb88..0000000000
--- a/grails-gsp/grails-sitemesh3/README.md
+++ /dev/null
@@ -1,18 +0,0 @@
-<!--
-SPDX-License-Identifier: Apache-2.0
-
-Licensed 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.
--->
-
-# SiteMesh 3 Grails Plugin
-Grails Plugin that adds support for [SiteMesh 3](https://github.com/sitemesh/sitemesh3)
\ No newline at end of file
diff --git a/grails-gsp/grails-sitemesh3/build.gradle b/grails-gsp/grails-sitemesh3/build.gradle
deleted file mode 100644
index 19cf1c2bdf..0000000000
--- a/grails-gsp/grails-sitemesh3/build.gradle
+++ /dev/null
@@ -1,89 +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.
- */
-
-plugins {
- id 'groovy'
- id 'java-library'
- id 'project-report'
- id 'org.apache.grails.gradle.grails-plugin'
-}
-
-version = projectVersion
-group = 'org.apache.grails'
-
-ext {
- pomTitle = 'SiteMesh 3 Grails Plugin'
- pomDescription = 'SiteMesh is a web-page layout and decoration framework and web- application integration framework to aid in creating sites consisting of many pages for which a consistent look/feel, navigation and layout scheme is required.'
- pomDevelopers = [codeconsole: 'Scott Murphy Heiberg']
-}
-
-dependencies {
-
- implementation platform(project(':grails-bom'))
- api "org.sitemesh:spring-boot-starter-sitemesh:$sitemeshVersion"
- api project(':grails-web-gsp-taglib')
-
-// api project(':grails-web-gsp'), { // GrailsConventionGroovyPageLocator
-// // API dependencies in grails-web-gsp
-// //exclude group: 'org.apache.grails.views', module: 'grails-gsp-core' // DefaultGroovyPageLocator
-// exclude group: 'org.apache.grails.web', module: 'grails-web-common'
-// exclude group: 'org.apache.grails.web', module: 'grails-web-taglib'
-// }
-// api project(':grails-web-gsp-taglib'), { // GrailsConventionGroovyPageLocator
-// // API dependencies in grails-web-gsp-taglib
-// exclude group: 'org.apache.grails.web', module: 'grails-taglib'
-// //exclude group: 'org.apache.grails.web', module: 'grails-web-gsp' // DefaultGroovyPageLocator
-// }
-// api "org.sitemesh:sitemesh:$sitemeshVersion" // SiteMeshFilter
-// api 'org.springframework:spring-webmvc' // AbstractHandlerAdapter, ParameterizableViewController, AbstractHandlerMapping
-// api 'org.springframework.boot:spring-boot' // FilterRegistrationBean
-//
-// implementation project(':grails-web-taglib'), { // TagLib, TagLibrary
-// // API dependencies in grails-web-taglib
-// exclude group: 'org.apache.grails.web', module: 'grails-taglib'
-// exclude group: 'org.apache.grails.web', module: 'grails-web-common'
-// }
-// implementation project(':grails-web-common'), { // WebUtils
-// // API dependencies in grails-web-common
-// exclude group: 'org.apache.groovy', module: 'groovy-templates'
-// exclude group: 'org.apache.grails', module: 'grails-core'
-// exclude group: 'org.apache.grails', module: 'grails-databinding'
-// exclude group: 'org.apache.grails', module: 'grails-encoder'
-// exclude group: 'org.springframework', module: 'spring-webmvc'
-// exclude group: 'org.springframework', module: 'spring-context-support'
-// }
-// implementation 'org.springframework:spring-beans' // Autowired, Qualifier
-// implementation 'org.springframework:spring-web' // HttpMethod, ResponseStatusException, HttpStatus
-//
-// runtimeOnly "org.apache.grails:grails-codecs:$grailsVersion" // Registers CodecLookup bean
-// runtimeOnly "org.sitemesh:spring-boot-starter-sitemesh:$sitemeshVersion"
-//
-// compileOnly 'jakarta.servlet:jakarta.servlet-api' // Provided by servlet container
-// compileOnly 'org.apache.groovy:groovy' // Provided by Grails Application
-
-}
-
-apply {
- // java-configuration must be applied first since tasks are now lazy registered
- from rootProject.layout.projectDirectory.file('gradle/java-config.gradle')
- from rootProject.layout.projectDirectory.file('gradle/docs-config.gradle')
- from rootProject.layout.projectDirectory.file('gradle/publish-config.gradle')
- from rootProject.layout.projectDirectory.file('gradle/test-config.gradle')
- from rootProject.layout.projectDirectory.file('gradle/grails-extension-gradle-config.gradle')
-}
\ No newline at end of file
diff --git a/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/sitemesh3/GrailsLayoutHandlerMapping.java b/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/sitemesh3/GrailsLayoutHandlerMapping.java
deleted file mode 100644
index df0a86d2a6..0000000000
--- a/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/sitemesh3/GrailsLayoutHandlerMapping.java
+++ /dev/null
@@ -1,67 +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.grails.web.gsp.io.GrailsConventionGroovyPageLocator;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.HttpMethod;
-import org.springframework.web.server.ResponseStatusException;
-import org.springframework.web.servlet.handler.AbstractHandlerMapping;
-import org.springframework.web.servlet.mvc.ParameterizableViewController;
-
-import jakarta.servlet.http.HttpServletRequest;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import static org.springframework.http.HttpStatus.NOT_FOUND;
-
-public class GrailsLayoutHandlerMapping extends AbstractHandlerMapping {
- @Autowired
- GrailsConventionGroovyPageLocator groovyPageLocator;
-
- Map<String, ParameterizableViewController> layoutCache = new HashMap<>();
-
- public GrailsLayoutHandlerMapping() {
- setOrder(-6);
- }
-
- @Override
- protected Object getHandlerInternal(HttpServletRequest request) {
- if (request.getAttribute("jakarta.servlet.forward.request_uri") == null) {
- return null; // only handle forwarded requests.
- }
- String servletPath = request.getServletPath();
- if (servletPath.startsWith("/layouts")) {
- ParameterizableViewController pvc = layoutCache.get(servletPath);
- if (pvc == null) {
- if (groovyPageLocator.findViewByPath(servletPath) == null) {
- throw new ResponseStatusException(NOT_FOUND, "Unable to find resource "+servletPath);
- }
- pvc = new ParameterizableViewController();
- pvc.setSupportedMethods(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name());
- pvc.setViewName(servletPath);
- layoutCache.put(servletPath, pvc);
- }
- return pvc;
- }
- return null;
- }
-}
\ No newline at end of file
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
deleted file mode 100644
index 8798f1ffb7..0000000000
--- a/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/sitemesh3/Sitemesh3GrailsPlugin.groovy
+++ /dev/null
@@ -1,98 +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 grails.plugins.Plugin
-import org.grails.config.PropertySourcesConfig
-import org.grails.plugins.web.taglib.RenderSitemeshTagLib
-import org.grails.plugins.web.taglib.SitemeshTagLib
-import org.grails.web.config.http.GrailsFilters
-import org.grails.web.util.WebUtils
-import org.springframework.core.env.ConfigurableEnvironment
-import org.springframework.core.env.MapPropertySource
-import org.springframework.core.env.PropertySource
-
-class Sitemesh3GrailsPlugin extends Plugin {
-
- def grailsVersion = '7.0.0-SNAPSHOT > *'
-
- def title = "SiteMesh 3"
- def author = "Scott Murphy"
- def authorEmail = ""
- def description = "Configures Grails to use SiteMesh 3 instead of SiteMesh 2"
- def profiles = ['web']
-
- def license = "APACHE"
-
- def developers = [[name: "Scott Murphy"]]
-
- def loadBefore = ['groovyPages']
-
- def providedArtefacts = [
- RenderSitemeshTagLib,
- SitemeshTagLib,
- ]
-
- static PropertySource getDefaultPropertySource(ConfigurableEnvironment configurableEnvironment, String defaultLayout) {
-
- Map props = [
- 'grails.gsp.view.layoutViewResolver': 'false',
- 'sitemesh.decorator.metaTag': 'layout',
- 'sitemesh.decorator.attribute': WebUtils.LAYOUT_ATTRIBUTE,
- 'sitemesh.decorator.prefix': '/layouts/',
- 'sitemesh.filter.order': GrailsFilters.SITEMESH_FILTER.order,
- 'sitemesh.decorator.tagRuleBundles': ['org.sitemesh.content.tagrules.html.Sm2TagRuleBundle']
- ]
- if (defaultLayout) {
- props['sitemesh.decorator.default'] = defaultLayout
- }
- // if property already exists, don't override
- props.clone().each {
- if (configurableEnvironment.getProperty(it.key)) {
- props.remove(it.key)
- }
- }
- return new MapPropertySource("sitemesh3Properties", props)
- }
-
-
- Closure doWithSpring() {
- { ->
- ConfigurableEnvironment configurableEnvironment = application.mainContext.environment
- def propertySources = configurableEnvironment.getPropertySources()
- // https://gsp.grails.org/latest/guide/layouts.html
- // Default view should be application, but it is inefficient to add a rule for a page that may not exist.
- String defaultLayout = grailsApplication.getConfig().getProperty("grails.sitemesh.default.layout")
- propertySources.addFirst(getDefaultPropertySource(configurableEnvironment, defaultLayout))
- application.config = new PropertySourcesConfig(propertySources)
- grailsLayoutHandlerMapping(GrailsLayoutHandlerMapping)
- }
- }
-
- void doWithApplicationContext() {}
-
- void doWithDynamicMethods() {}
-
- void onChange(Map<String, Object> event) {}
-
- void onConfigChange(Map<String, Object> event) {}
-
- void onShutdown(Map<String, Object> event) {}
-}
\ No newline at end of file
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
deleted file mode 100644
index c9d35acfba..0000000000
--- a/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/web/taglib/RenderSitemeshTagLib.groovy
+++ /dev/null
@@ -1,181 +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.web.taglib
-
-import org.grails.web.util.WebUtils
-import org.sitemesh.content.Content
-import org.sitemesh.content.ContentProperty
-import org.sitemesh.webapp.SiteMeshFilter
-import org.sitemesh.webapp.WebAppContext
-import org.sitemesh.webapp.contentfilter.ResponseMetaData
-import org.springframework.beans.factory.annotation.Autowired
-import org.springframework.beans.factory.annotation.Qualifier
-import org.springframework.boot.web.servlet.FilterRegistrationBean
-import grails.artefact.TagLibrary
-import grails.gsp.TagLib
-
-import java.nio.CharBuffer
-
-@TagLib
-class RenderSitemeshTagLib implements TagLibrary {
-
- SiteMeshFilter siteMeshFilter
-
- @Autowired
- RenderSitemeshTagLib(@Qualifier("sitemesh") FilterRegistrationBean sitemesh) {
- this.siteMeshFilter = (SiteMeshFilter) sitemesh.getFilter()
- }
-
- Closure applyLayout = { Map attrs, body ->
- String savedAttribute = request.getAttribute(WebUtils.LAYOUT_ATTRIBUTE)
- WebAppContext context = new WebAppContext("text/html", request, response,
- servletContext, siteMeshFilter.contentProcessor, new ResponseMetaData(), false);
- Content content = siteMeshFilter.contentProcessor.build(CharBuffer.wrap(body()), context);
- if (attrs.name) {
- request.setAttribute(WebUtils.LAYOUT_ATTRIBUTE, attrs.name)
- }
- String[] decoratorPaths = siteMeshFilter.decoratorSelector.selectDecoratorPaths(content, context);
- for (String decoratorPath : decoratorPaths) {
- content = context.decorate(decoratorPath, content);
- }
- content.getData().writeValueTo(out)
- request.setAttribute(WebUtils.LAYOUT_ATTRIBUTE, savedAttribute)
- }
-
- private ContentProperty getContentProperty(String name) {
- if (!name) {
- return null
- }
- Content content = request.getAttribute(WebAppContext.CONTENT_KEY)
- ContentProperty currentProperty = content.getExtractedProperties();
- for (String childPropertyName : name.split("\\.")) {
- currentProperty = currentProperty.getChild(childPropertyName);
- }
- currentProperty
- }
-
- /**
- * Used to retrieve a property of the decorated page.<br/>
- *
- * <g:pageProperty default="defaultValue" name="body.onload" /><br/>
- *
- * @emptyTag
- *
- * @attr REQUIRED name the property name
- * @attr default the default value to use if the property is null
- * @attr writeEntireProperty if true, writes the property in the form 'foo = "bar"', otherwise renders 'bar'
- */
- Closure pageProperty = { attrs ->
- if (!attrs.name) {
- throwTagError("Tag [pageProperty] is missing required attribute [name]")
- }
- String propertyName = attrs.name as String
- ContentProperty contentProperty = getContentProperty(propertyName)
- def propertyValue = contentProperty?.hasValue()? contentProperty.getValue() : attrs.'default' ?: null
-
- if (propertyValue) {
- if (attrs.writeEntireProperty) {
- out << ' '
- out << propertyName.substring(propertyName.lastIndexOf('.') + 1)
- out << "=\""
- out << propertyValue
- out << "\""
- } else {
- out << propertyValue
- }
- }
- }
-
- /**
- * Invokes the body of this tag if the page property exists:<br/>
- *
- * <g:ifPageProperty name="meta.index">body to invoke</g:ifPageProperty><br/>
- *
- * or it equals a certain value:<br/>
- *
- * <g:ifPageProperty name="meta.index" equals="blah">body to invoke</g:ifPageProperty>
- *
- * @attr name REQUIRED the property name
- * @attr equals optional value to test against
- */
- Closure ifPageProperty = { Map attrs, body ->
- if (!attrs.name) {
- return
- }
- List names = ((attrs.name instanceof List) ? (List)attrs.name : [attrs.name])
-
- def invokeBody = true
- for (i in 0..<names.size()) {
- def propertyValue = getContentProperty(names[i] as String)?.getValue()
- if (propertyValue) {
- if (attrs.containsKey('equals')) {
- if (attrs.equals instanceof List) {
- invokeBody = ((List)attrs.equals)[i] == propertyValue
- } else {
- invokeBody = attrs.equals == propertyValue
- }
- }
- } else {
- invokeBody = false
- break
- }
- }
- if (invokeBody && body instanceof Closure) {
- out << body()
- }
- }
-
- Closure layoutTitle = { attrs ->
- out << """<sitemesh:write property="title">${attrs.default?:''}</sitemesh:write>""".toString()
- }
-
- Closure layoutHead = { attrs, body ->
- StringBuilder tag = new StringBuilder('<sitemesh:write property="head"')
- String bodyContent = body()
- if (bodyContent) {
- tag.append('>')
- tag.append(bodyContent)
- tag.append('</sitemesh:write>')
- } else {
- tag.append('/>')
- }
- out << tag.toString()
- }
-
- Closure layoutBody = { attrs, body ->
- StringBuilder tag = new StringBuilder('<sitemesh:write property="body"')
- String bodyContent = body()
- if (bodyContent) {
- tag.append('>')
- tag.append(bodyContent)
- tag.append('</sitemesh:write>')
- } else {
- tag.append('/>')
- }
- out << tag.toString()
- }
-
- Closure content = { attrs, body ->
- StringBuilder tag = new StringBuilder("""<content tag="${attrs.tag}">""")
- tag.append(body())
- tag.append('</content>')
- out << tag.toString()
- }
-}
diff --git a/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/web/taglib/SitemeshTagLib.groovy b/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/web/taglib/SitemeshTagLib.groovy
deleted file mode 100644
index f52cce344c..0000000000
--- a/grails-gsp/grails-sitemesh3/src/main/groovy/org/grails/plugins/web/taglib/SitemeshTagLib.groovy
+++ /dev/null
@@ -1,146 +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.web.taglib
-
-import org.grails.buffer.GrailsPrintWriter
-import org.grails.buffer.StreamCharBuffer
-import org.grails.encoder.CodecLookup
-import org.grails.encoder.Encoder
-import grails.artefact.TagLibrary
-import grails.gsp.TagLib
-
-@TagLib
-class SitemeshTagLib implements TagLibrary {
- static String namespace = 'sitemesh'
- CodecLookup codecLookup
- // from org.grails.gsp.compiler.SitemeshPreprocessor
- public static final String XML_CLOSING_FOR_EMPTY_TAG_ATTRIBUTE_NAME = "gsp_sm_xmlClosingForEmptyTag";
-
- SitemeshTagLib(CodecLookup codecLookup) {
- this.codecLookup = codecLookup
- }
-
- def captureTagContent(GrailsPrintWriter writer, String tagname, Map attrs, Object body, boolean noEndTagForEmpty=false, boolean useNamespace = false) {
- def content = null
- if (body != null) {
- if (body instanceof Closure) {
- content = body()
- }
- else {
- content = body
- }
- }
-
- if (content instanceof StreamCharBuffer) {
- content.setPreferSubChunkWhenWritingToOtherBuffer(true)
- }
- writer << '<'+(useNamespace? SitemeshTagLib.namespace + ':' : '')
- writer << tagname
- def useXmlClosingForEmptyTag = false
- if (attrs) {
- def xmlClosingString = attrs.remove(XML_CLOSING_FOR_EMPTY_TAG_ATTRIBUTE_NAME)
- if (xmlClosingString=='/') {
- useXmlClosingForEmptyTag = true
- }
- Encoder htmlEncoder = codecLookup?.lookupEncoder('HTML')
- attrs.each { k, v ->
- writer << ' '
- writer << k
- writer << '="'
- writer << (htmlEncoder != null ? htmlEncoder.encode(v) : v)
- writer << '"'
- }
- }
-
- if (content) {
- writer << '>'
- // the following row must be written separately (append StreamCharBuffer gets appended as subchunk)
- writer << content
- writer << '</'+(useNamespace? SitemeshTagLib.namespace + ':' : '')
- writer << tagname
- writer << '>'
- }
- else {
- if (!useXmlClosingForEmptyTag) {
- writer << '>'
- // in valid HTML , closing of an empty tag depends on the element name
- // for empty title, the tag must be closed properly
- // for empty meta tag shouldn't be closed at all, see GRAILS-5696
- if (!noEndTagForEmpty) {
- writer << '</'+(useNamespace? SitemeshTagLib.namespace + ':' : '')
- writer << tagname
- writer << '>'
- }
- }
- else {
- // XML / XHTML empty tag
- writer << '/>'
- }
- }
- content
- }
-
- // Grails org.grails.gsp.compiler.SitemeshPreprocessor replaces
- // <content /> tags with <sitemesh:captureContent /> tags. This just changes them back.
- // Alternatively, a 'sitemesh:captureContent' ContentBlockExtractingRule can be used.
- // See: GrailsTagRuleBundle
- Closure captureContent = { attrs, body ->
- StringBuilder tag = new StringBuilder('<content ')
- attrs.each {
- tag.append("""${it.key}="${it.value}" """)
- }
- tag.append('>')
- tag.append(body())
- tag.append('</content>')
- out << tag.toString()
- }
-
- Closure captureHead = { Map attrs, body ->
- captureTagContent(out, 'head', attrs, body)
- }
-
- /**
- * Allows passing of parameters to Sitemesh layout.<br/>
- *
- * <sitemesh:parameter name="foo" value="bar" />
- */
- Closure parameter = { Map attrs, body ->
- captureTagContent(out, 'parameter', attrs, body)
- }
-
- Closure captureBody = { Map attrs, body ->
- captureTagContent(out, 'body', attrs, body)
- }
- Closure captureMeta = { Map attrs, body ->
- captureTagContent(out, 'meta', attrs, body, true)
- }
-
- Closure captureTitle = { Map attrs, body ->
- captureTagContent(out, 'title', attrs, body)
- }
-
- Closure wrapTitleTag = { Map attrs, body ->
- out << body()
- }
-
- Closure write = { Map attrs, body ->
- captureTagContent(out, 'write', attrs, body, false, true)
- }
-}
diff --git a/grails-gsp/grails-web-gsp/build.gradle b/grails-gsp/grails-web-gsp/build.gradle
index ffc612da76..79b4fbc4fe 100644
--- a/grails-gsp/grails-web-gsp/build.gradle
+++ b/grails-gsp/grails-web-gsp/build.gradle
@@ -39,6 +39,7 @@ dependencies {
api project(':grails-gsp-core')
api project(':grails-web-common')
api project(':grails-web-taglib')
+ implementation project(":grails-web-layout")
// api 'org.apache.grails:grails-core', { // GrailsDomainClass, CacheEntry, Environment, GrailsStringUtils, GrailsApplication, GrailsControllerClass, GrailsApplicationAware, ControllerArtefactHandler, GrailsPluginManager, GrailsFactoriesLoader
// // API dependencies in grails-core
diff --git a/grails-gsp/grails-web-gsp/src/main/groovy/org/grails/web/pages/GSPResponseWriter.java b/grails-gsp/grails-web-gsp/src/main/groovy/org/grails/web/pages/GSPResponseWriter.java
index 281a147ad3..7f334eb278 100644
--- a/grails-gsp/grails-web-gsp/src/main/groovy/org/grails/web/pages/GSPResponseWriter.java
+++ b/grails-gsp/grails-web-gsp/src/main/groovy/org/grails/web/pages/GSPResponseWriter.java
@@ -19,9 +19,9 @@
package org.grails.web.pages;
+import com.opensymphony.module.sitemesh.RequestConstants;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
-import org.grails.buffer.GrailsRoutablePrintWriter;
import org.grails.buffer.StreamCharBuffer;
import org.grails.buffer.StreamCharBuffer.LazyInitializingWriter;
import org.grails.buffer.StreamCharBuffer.StreamCharBufferWriter;
@@ -30,6 +30,8 @@ import org.grails.encoder.EncodedAppenderFactory;
import org.grails.encoder.Encoder;
import org.grails.encoder.EncoderAware;
import org.grails.web.servlet.mvc.GrailsWebRequest;
+import org.apache.grails.web.layout.GrailsContentBufferingResponse;
+import org.apache.grails.web.layout.GrailsRoutablePrintWriter;
import org.grails.web.util.BoundedCharsAsEncodedBytesCounter;
import org.springframework.objenesis.ObjenesisStd;
import org.springframework.objenesis.instantiator.ObjectInstantiator;
@@ -91,25 +93,29 @@ public class GSPResponseWriter extends GrailsRoutablePrintWriter implements Enco
final StreamCharBuffer.LazyInitializingWriter lazyResponseWriter = response::getWriter;
- streamBuffer.connectTo(new StreamCharBuffer.LazyInitializingMultipleWriter() {
- public Writer getWriter() {
- return null;
- }
+ if (!(response instanceof GrailsContentBufferingResponse)) {
+ streamBuffer.connectTo(new StreamCharBuffer.LazyInitializingMultipleWriter() {
+ public Writer getWriter() {
+ return null;
+ }
- public LazyInitializingWriter[] initializeMultiple(StreamCharBuffer buffer, boolean autoFlush) {
- final StreamCharBuffer.LazyInitializingWriter[] lazyWriters;
- if (CONTENT_LENGTH_COUNTING_ENABLED) {
- lazyWriters = new StreamCharBuffer.LazyInitializingWriter[]{() -> {
- bytesCounter.setCapacity(max * 2);
- bytesCounter.setEncoding(response.getCharacterEncoding());
- return bytesCounter.getCountingWriter();
- }, lazyResponseWriter};
- } else {
- lazyWriters = new StreamCharBuffer.LazyInitializingWriter[]{lazyResponseWriter};
+ public LazyInitializingWriter[] initializeMultiple(StreamCharBuffer buffer, boolean autoFlush) {
+ final StreamCharBuffer.LazyInitializingWriter[] lazyWriters;
+ if (CONTENT_LENGTH_COUNTING_ENABLED) {
+ lazyWriters = new StreamCharBuffer.LazyInitializingWriter[]{() -> {
+ bytesCounter.setCapacity(max * 2);
+ bytesCounter.setEncoding(response.getCharacterEncoding());
+ return bytesCounter.getCountingWriter();
+ }, lazyResponseWriter};
+ } else {
+ lazyWriters = new StreamCharBuffer.LazyInitializingWriter[]{lazyResponseWriter};
+ }
+ return lazyWriters;
}
- return lazyWriters;
- }
- }, AUTOFLUSH_ENABLED);
+ }, AUTOFLUSH_ENABLED);
+ } else {
+ streamBuffer.connectTo(lazyResponseWriter);
+ }
if (instantiator != null) {
GSPResponseWriter instance = instantiator.newInstance();
@@ -207,6 +213,12 @@ public class GSPResponseWriter extends GrailsRoutablePrintWriter implements Enco
response.setContentLength(size);
}
flushResponse();
+ } else if (!isTrouble()) {
+ GrailsWebRequest webRequest = GrailsWebRequest.lookup();
+ if (webRequest != null && webRequest.getCurrentRequest().getAttribute(RequestConstants.PAGE) != null) {
+ // flush the response if its a layout
+ flushResponse();
+ }
}
}
diff --git a/grails-gsp/grails-web-gsp/src/main/groovy/org/grails/web/servlet/view/EmbeddedGrailsLayoutViewResolver.java b/grails-gsp/grails-web-gsp/src/main/groovy/org/grails/web/servlet/view/EmbeddedGrailsLayoutViewResolver.java
new file mode 100644
index 0000000000..bf552c1da3
--- /dev/null
+++ b/grails-gsp/grails-web-gsp/src/main/groovy/org/grails/web/servlet/view/EmbeddedGrailsLayoutViewResolver.java
@@ -0,0 +1,112 @@
+/*
+ * 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.web.servlet.view;
+
+import java.util.Locale;
+
+import jakarta.servlet.ServletContext;
+
+import org.apache.grails.web.layout.EmbeddedGrailsLayoutView;
+import org.apache.grails.web.layout.GroovyPageLayoutFinder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.core.Ordered;
+import org.springframework.web.context.ServletContextAware;
+import org.springframework.web.servlet.SmartView;
+import org.springframework.web.servlet.View;
+import org.springframework.web.servlet.ViewResolver;
+
+public class EmbeddedGrailsLayoutViewResolver implements LayoutViewResolver, Ordered, ServletContextAware, ApplicationContextAware {
+
+ private static final Logger LOG = LoggerFactory.getLogger(EmbeddedGrailsLayoutViewResolver.class);
+
+ protected ViewResolver innerViewResolver;
+ protected GroovyPageLayoutFinder groovyPageLayoutFinder;
+ private int order = Ordered.LOWEST_PRECEDENCE - 30;
+ protected ServletContext servletContext;
+
+ public EmbeddedGrailsLayoutViewResolver(ViewResolver innerViewResolver, GroovyPageLayoutFinder groovyPageLayoutFinder) {
+ this.innerViewResolver = innerViewResolver;
+ this.groovyPageLayoutFinder = groovyPageLayoutFinder;
+ }
+
+ public EmbeddedGrailsLayoutViewResolver() {
+ }
+
+ @Override
+ public View resolveViewName(String viewName, Locale locale) throws Exception {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Resolving view for name {} and locale {}", viewName, locale);
+ }
+ View innerView = innerViewResolver.resolveViewName(viewName, locale);
+ if (innerView == null) {
+ return null;
+ } else if (innerView instanceof SmartView && ((SmartView) innerView).isRedirectView()) {
+ return innerView;
+ } else {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Creating layout view name {} and locale {}", viewName, locale);
+ }
+ return createLayoutView(innerView);
+ }
+ }
+
+ protected View createLayoutView(View innerView) {
+ return new EmbeddedGrailsLayoutView(groovyPageLayoutFinder, innerView);
+ }
+
+ @Override
+ public ViewResolver getInnerViewResolver() {
+ return innerViewResolver;
+ }
+
+ public int getOrder() {
+ return order;
+ }
+
+ public void setOrder(int order) {
+ this.order = order;
+ }
+
+ @Override
+ public void setServletContext(ServletContext servletContext) {
+ this.servletContext = servletContext;
+ if (innerViewResolver instanceof ServletContextAware) {
+ ((ServletContextAware) innerViewResolver).setServletContext(servletContext);
+ }
+ }
+
+ @Override
+ public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+ if (innerViewResolver instanceof ApplicationContextAware) {
+ ((ApplicationContextAware) innerViewResolver).setApplicationContext(applicationContext);
+ }
+ }
+
+ public void setInnerViewResolver(ViewResolver innerViewResolver) {
+ this.innerViewResolver = innerViewResolver;
+ }
+
+ public void setGroovyPageLayoutFinder(GroovyPageLayoutFinder groovyPageLayoutFinder) {
+ this.groovyPageLayoutFinder = groovyPageLayoutFinder;
+ }
+}
diff --git a/grails-gsp/grails-web-gsp/src/main/groovy/org/grails/web/servlet/view/GrailsLayoutViewResolver.java b/grails-gsp/grails-web-gsp/src/main/groovy/org/grails/web/servlet/view/GrailsLayoutViewResolver.java
new file mode 100644
index 0000000000..8d28de31c7
--- /dev/null
+++ b/grails-gsp/grails-web-gsp/src/main/groovy/org/grails/web/servlet/view/GrailsLayoutViewResolver.java
@@ -0,0 +1,147 @@
+/*
+ * 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.web.servlet.view;
+
+import java.util.Enumeration;
+import java.util.Map;
+import java.util.Collections;
+
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.ServletContext;
+
+import grails.core.GrailsApplication;
+import grails.core.support.GrailsApplicationAware;
+import org.apache.grails.web.layout.FactoryHolder;
+import org.apache.grails.web.layout.GroovyPageLayoutFinder;
+import org.apache.grails.web.layout.GrailsLayoutView;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.core.Ordered;
+import org.springframework.web.servlet.View;
+import org.springframework.web.servlet.ViewResolver;
+
+import com.opensymphony.module.sitemesh.Config;
+import com.opensymphony.module.sitemesh.Factory;
+import com.opensymphony.module.sitemesh.factory.DefaultFactory;
+import com.opensymphony.sitemesh.ContentProcessor;
+import com.opensymphony.sitemesh.compatability.PageParser2ContentProcessor;
+
+public class GrailsLayoutViewResolver extends EmbeddedGrailsLayoutViewResolver implements GrailsApplicationAware, DisposableBean, Ordered, ApplicationListener<ContextRefreshedEvent> {
+ private static final String FACTORY_SERVLET_CONTEXT_ATTRIBUTE = "grails.layout.factory";
+ private ContentProcessor contentProcessor;
+ protected GrailsApplication grailsApplication;
+ private boolean grailsLayoutConfigLoaded = false;
+ private int order = Ordered.LOWEST_PRECEDENCE - 50;
+
+ public GrailsLayoutViewResolver() {
+ super();
+ }
+
+ public GrailsLayoutViewResolver(ViewResolver innerViewResolver, GroovyPageLayoutFinder groovyPageLayoutFinder) {
+ super(innerViewResolver, groovyPageLayoutFinder);
+ }
+
+ @Override
+ protected View createLayoutView(View innerView) {
+ return new GrailsLayoutView(groovyPageLayoutFinder, innerView, contentProcessor);
+ }
+
+ public void init() {
+ if (servletContext == null) return;
+
+ Factory grailsLayoutFactory = (Factory) servletContext.getAttribute(FACTORY_SERVLET_CONTEXT_ATTRIBUTE);
+ if (grailsLayoutFactory == null) {
+ grailsLayoutFactory = loadGrailsLayoutConfig();
+ }
+ contentProcessor = new PageParser2ContentProcessor(grailsLayoutFactory);
+ }
+
+ protected Factory loadGrailsLayoutConfig() {
+ FilterConfig filterConfig = new FilterConfig() {
+ private Map<String, String> customConfig =
+ Collections.singletonMap("configFile",
+ "classpath:org/apache/grails/web/layout/grails-layout-default.xml");
+
+ @Override
+ public ServletContext getServletContext() {
+ return servletContext;
+ }
+
+ @Override
+ public Enumeration<String> getInitParameterNames() {
+ return Collections.enumeration(customConfig.keySet());
+ }
+
+ @Override
+ public String getInitParameter(String name) {
+ return customConfig.get(name);
+ }
+
+ @Override
+ public String getFilterName() {
+ return null;
+ }
+ };
+ Config config = new Config(filterConfig);
+
+ DefaultFactory grailsLayoutFactory = new DefaultFactory(config);
+ if (servletContext != null) {
+ servletContext.setAttribute(FACTORY_SERVLET_CONTEXT_ATTRIBUTE, grailsLayoutFactory);
+ }
+ grailsLayoutFactory.refresh();
+ FactoryHolder.setFactory(grailsLayoutFactory);
+ grailsLayoutConfigLoaded = true;
+ return grailsLayoutFactory;
+ }
+
+ @Override
+ public void setGrailsApplication(GrailsApplication grailsApplication) {
+ this.grailsApplication = grailsApplication;
+ }
+
+ @Override
+ public void destroy() throws Exception {
+ clearGrailsLayoutConfig();
+ }
+
+ protected void clearGrailsLayoutConfig() {
+ if (servletContext == null) return;
+ if (grailsLayoutConfigLoaded) {
+ FactoryHolder.setFactory(null);
+ if (servletContext != null) {
+ servletContext.removeAttribute(FACTORY_SERVLET_CONTEXT_ATTRIBUTE);
+ }
+ grailsLayoutConfigLoaded = false;
+ }
+ }
+
+ public int getOrder() {
+ return order;
+ }
+
+ public void setOrder(int order) {
+ this.order = order;
+ }
+
+ @Override
+ public void onApplicationEvent(ContextRefreshedEvent event) {
+ init();
+ }
+}
diff --git a/grails-gsp/spring-boot/build.gradle b/grails-gsp/grails-web-layout/build.gradle
similarity index 57%
copy from grails-gsp/spring-boot/build.gradle
copy to grails-gsp/grails-web-layout/build.gradle
index b73dc4330e..556fd89f2f 100644
--- a/grails-gsp/spring-boot/build.gradle
+++ b/grails-gsp/grails-web-layout/build.gradle
@@ -16,32 +16,44 @@
* specific language governing permissions and limitations
* under the License.
*/
-
plugins {
+ id 'groovy'
id 'java-library'
+ id 'project-report'
+ id 'org.apache.grails.gradle.grails-plugin'
}
version = projectVersion
-group = 'org.apache.grails'
-
-apply plugin: 'org.apache.grails.gradle.grails-plugin'
+group = 'org.apache.grails.views'
ext {
- pomTitle = 'GSP Spring Boot Plugin'
- pomDescription = 'Provides GSP for Spring Boot Applications'
- pomDevelopers = ['graemerocher': 'Graeme Rocher', lhotari: 'Lari Hotari', codeconsole: 'Scott Murphy Heiberg']
+ pomTitle = 'Layout Grails Plugin'
+ pomDescription = 'Provides layout & decoration features for the Grails Framework.'
}
dependencies {
implementation platform(project(':grails-bom'))
- api project(':grails-sitemesh3')
+ implementation 'org.apache.groovy:groovy'
+
+ api 'opensymphony:sitemesh'
+
+ compileOnly project(':grails-web-common')
+ compileOnly 'jakarta.servlet:jakarta.servlet-api' // Provided
+
+ testImplementation 'jakarta.servlet:jakarta.servlet-api'
+ testImplementation 'org.spockframework:spock-core'
+ testImplementation 'org.springframework:spring-test', {
+ // impl: MockHttpServletRequest
+ }
+ testImplementation project(':grails-web-common')
+ testRuntimeOnly 'org.slf4j:slf4j-nop' // Get rid of warning about missing slf4j implementation during test task
}
apply {
// java-configuration must be applied first since tasks are now lazy registered
from rootProject.layout.projectDirectory.file('gradle/java-config.gradle')
- //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/publish-config.gradle')
from rootProject.layout.projectDirectory.file('gradle/test-config.gradle')
from rootProject.layout.projectDirectory.file('gradle/grails-extension-gradle-config.gradle')
}
\ No newline at end of file
diff --git a/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/EmbeddedGrailsLayoutView.java b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/EmbeddedGrailsLayoutView.java
new file mode 100644
index 0000000000..a03ebcfa37
--- /dev/null
+++ b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/EmbeddedGrailsLayoutView.java
@@ -0,0 +1,172 @@
+/*
+ * 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.apache.grails.web.layout;
+
+import groovy.text.Template;
+
+import java.io.PrintWriter;
+import java.util.Map;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.grails.web.servlet.WrappedResponseHolder;
+import org.grails.web.servlet.mvc.GrailsWebRequest;
+import org.grails.web.servlet.mvc.OutputAwareHttpServletResponse;
+import org.grails.web.servlet.view.AbstractGrailsView;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.MediaType;
+import org.springframework.web.servlet.View;
+
+import com.opensymphony.module.sitemesh.RequestConstants;
+import com.opensymphony.sitemesh.Content;
+
+public class EmbeddedGrailsLayoutView extends AbstractGrailsView {
+
+ private static final Logger LOG = LoggerFactory.getLogger(EmbeddedGrailsLayoutView.class);
+ GroovyPageLayoutFinder groovyPageLayoutFinder;
+
+ protected View innerView;
+
+ public static final String GSP_GRAILS_LAYOUT_PAGE = EmbeddedGrailsLayoutView.class.getName() + ".GSP_GRAILS_LAYOUT_PAGE";
+
+ public EmbeddedGrailsLayoutView(GroovyPageLayoutFinder groovyPageLayoutFinder, View innerView) {
+ this.groovyPageLayoutFinder = groovyPageLayoutFinder;
+ this.innerView = innerView;
+ }
+
+ @Override
+ public String getContentType() {
+ return MediaType.ALL_VALUE;
+ }
+
+ @Override
+ protected void renderTemplate(Map<String, Object> model, GrailsWebRequest webRequest, HttpServletRequest request,
+ HttpServletResponse response) throws Exception {
+
+ boolean isCommitted = response.isCommitted() && (response instanceof OutputAwareHttpServletResponse) && !((OutputAwareHttpServletResponse) response).isWriterAvailable();
+ if (!isCommitted) {
+
+ Content content = obtainContent(model, webRequest, request, response);
+ if (content != null) {
+
+ beforeDecorating(content, model, webRequest, request, response);
+ switch (request.getDispatcherType()) {
+ case INCLUDE:
+ break;
+ case ASYNC:
+ case ERROR:
+ case FORWARD:
+ case REQUEST:
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Finding layout for request and content");
+ }
+ SpringMVCViewDecorator decorator = (SpringMVCViewDecorator) groovyPageLayoutFinder.findLayout(request, content);
+ if (decorator != null) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Found layout. Rendering content for layout {} and model {}", decorator.getPage(), model);
+ }
+
+ decorator.render(content, model, request, response, webRequest.getServletContext());
+ return;
+ }
+ break;
+ }
+ PrintWriter writer = response.getWriter();
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Layout not applicable to response, writing original content");
+ }
+ content.writeOriginal(writer);
+ if (!response.isCommitted()) {
+ writer.flush();
+ }
+ }
+ }
+
+
+ }
+
+ protected void beforeDecorating(Content content, Map<String, Object> model, GrailsWebRequest webRequest,
+ HttpServletRequest request, HttpServletResponse response) {
+ applyMetaHttpEquivContentType(content, response);
+ }
+
+ protected void applyMetaHttpEquivContentType(Content content, HttpServletResponse response) {
+ String contentType = content.getProperty("meta.http-equiv.Content-Type");
+ if (contentType != null && "text/html".equals(response.getContentType())) {
+ response.setContentType(contentType);
+ }
+ }
+
+ protected Content obtainContent(Map<String, Object> model, GrailsWebRequest webRequest, HttpServletRequest request,
+ HttpServletResponse response) throws Exception {
+ Object oldPage = request.getAttribute(RequestConstants.PAGE);
+ request.removeAttribute(RequestConstants.PAGE);
+ Object oldGspGrailsLayoutPage = request.getAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE);
+ HttpServletResponse previousResponse = webRequest.getWrappedResponse();
+ HttpServletResponse previousWrappedResponse = WrappedResponseHolder.getWrappedResponse();
+ try {
+ request.setAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE, new GSPGrailsLayoutPage());
+
+ GrailsContentBufferingResponse contentBufferingResponse = createContentBufferingResponse(model, webRequest, request, response);
+ webRequest.setWrappedResponse(contentBufferingResponse);
+ WrappedResponseHolder.setWrappedResponse(contentBufferingResponse);
+
+ renderInnerView(model, webRequest, request, response, contentBufferingResponse);
+
+ return contentBufferingResponse.getContent();
+ } finally {
+ if (oldGspGrailsLayoutPage != null) {
+ request.setAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE, oldGspGrailsLayoutPage);
+ }
+ if (oldPage != null) {
+ request.setAttribute(RequestConstants.PAGE, oldPage);
+ }
+ webRequest.setWrappedResponse(previousResponse);
+ WrappedResponseHolder.setWrappedResponse(previousWrappedResponse);
+ }
+ }
+
+ protected void renderInnerView(Map<String, Object> model, GrailsWebRequest webRequest, HttpServletRequest request,
+ HttpServletResponse response,
+ GrailsContentBufferingResponse contentBufferingResponse) throws Exception {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Rendering inner view for layout and model {}", model);
+ }
+ innerView.render(model, request, contentBufferingResponse);
+ }
+
+ protected GrailsContentBufferingResponse createContentBufferingResponse(Map<String, Object> model, GrailsWebRequest webRequest, HttpServletRequest request,
+ HttpServletResponse response) {
+ return new GrailsViewBufferingResponse(request, response);
+ }
+
+ @Override
+ public Template getTemplate() {
+ if (innerView instanceof AbstractGrailsView) {
+ return ((AbstractGrailsView) innerView).getTemplate();
+ }
+ return null;
+ }
+
+ public View getInnerView() {
+ return innerView;
+ }
+}
\ No newline at end of file
diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/AsyncFunctionalSpec.groovy b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/FactoryHolder.java
similarity index 55%
copy from grails-test-examples/app1/src/integration-test/groovy/functionaltests/AsyncFunctionalSpec.groovy
copy to grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/FactoryHolder.java
index 9db409059e..eaf52001d7 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/AsyncFunctionalSpec.groovy
+++ b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/FactoryHolder.java
@@ -17,29 +17,36 @@
* under the License.
*/
-package functionaltests
+package org.apache.grails.web.layout;
-import grails.gorm.transactions.Rollback
-import grails.plugin.geb.ContainerGebSpec
-import grails.testing.mixin.integration.Integration
+import grails.util.Holder;
-import spock.lang.PendingFeature
+import com.opensymphony.module.sitemesh.Factory;
/**
- * @author graemerocher
+ * Holds a reference to the Grails Layout Factory object.
+ *
+ * @author Graeme Rocher
+ * @since 0.6
*/
-@Integration(applicationClass = Application)
-@Rollback
-class AsyncFunctionalSpec extends ContainerGebSpec {
+public class FactoryHolder {
- @PendingFeature(reason = 'pageSource == <html><head></head><body></body></html>')
- void "Test async response rendering works"() {
- when: "When an async response is rendered"
- go('/async/test')
+ private static Holder<Factory> holder = new Holder<Factory>("factory");
- then: "The rendered page is correct"
- pageSource.contains('Hello')
+ private FactoryHolder() {
+ // static only
}
-}
+ public static Factory getFactory() {
+ Factory factory = holder.get();
+ return factory;
+ }
+ public static Factory getGrailsLayoutFactory() {
+ return getFactory();
+ }
+
+ public static synchronized void setFactory(Factory factory) {
+ holder.set(factory);
+ }
+}
\ No newline at end of file
diff --git a/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GSPGrailsLayoutPage.java b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GSPGrailsLayoutPage.java
new file mode 100644
index 0000000000..8c954f6b80
--- /dev/null
+++ b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GSPGrailsLayoutPage.java
@@ -0,0 +1,237 @@
+/*
+ * 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.apache.grails.web.layout;
+
+import com.opensymphony.module.sitemesh.HTMLPage;
+import com.opensymphony.module.sitemesh.parser.AbstractHTMLPage;
+import com.opensymphony.sitemesh.Content;
+import com.opensymphony.sitemesh.compatability.Content2HTMLPage;
+import org.grails.buffer.StreamCharBuffer;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Grails/GSP specific implementation of Sitemesh's AbstractHTMLPage
+ * <p>
+ * g:capture* tags in RenderTagLib are used to capture head, meta, title, component and body contents.
+ * No html parsing is required for templating since capture tags are added at GSP compilation time.
+ *
+ * @author Lari Hotari, Sagire Software Oy
+ */
+public class GSPGrailsLayoutPage extends AbstractHTMLPage implements Content {
+
+ StreamCharBuffer headBuffer;
+ StreamCharBuffer bodyBuffer;
+ StreamCharBuffer pageBuffer;
+ StreamCharBuffer titleBuffer;
+ boolean used;
+ boolean titleCaptured;
+ Map<String, StreamCharBuffer> contentBuffers;
+ private boolean renderingLayout;
+
+ public GSPGrailsLayoutPage() {
+ this(false);
+ }
+
+ public GSPGrailsLayoutPage(boolean renderingLayout) {
+ reset();
+ this.renderingLayout = renderingLayout;
+ }
+
+ public void reset() {
+ headBuffer = null;
+ bodyBuffer = null;
+ pageBuffer = null;
+ titleBuffer = null;
+ used = false;
+ titleCaptured = false;
+ contentBuffers = null;
+ renderingLayout = false;
+ }
+
+ public void addProperty(String name, Object value) {
+ addProperty(name, (value == null ? null : String.valueOf(value)));
+ }
+
+ @Override
+ public void addProperty(String name, String value) {
+ super.addProperty(name, value);
+ used = true;
+ }
+
+ @Override
+ public void writeHead(Writer out) throws IOException {
+ if (headBuffer == null) {
+ return;
+ }
+
+ if (titleCaptured) {
+ if (titleBuffer != null) {
+ int headlen = headBuffer.length();
+ titleBuffer.clear();
+ if (headBuffer.length() < headlen) {
+ headBuffer.writeTo(out);
+ return;
+ }
+ }
+ String headAsString = headBuffer.toString();
+ // strip out title for Grails Layout version of <head>
+ out.write(headAsString.replaceFirst("(?is)<title(\\s[^>]*)?>(.*?)</title>", ""));
+ } else {
+ headBuffer.writeTo(out);
+ }
+ }
+
+ @Override
+ public void writeBody(Writer out) throws IOException {
+ if (bodyBuffer != null) {
+ bodyBuffer.writeTo(out);
+ } else if (pageBuffer != null) {
+ // no body was captured, so write the whole page content
+ pageBuffer.writeTo(out);
+ }
+ }
+
+ @Override
+ public void writePage(Writer out) throws IOException {
+ if (pageBuffer != null) {
+ pageBuffer.writeTo(out);
+ }
+ }
+
+ public String getHead() {
+ if (headBuffer != null) {
+ return headBuffer.toString();
+ }
+ return null;
+ }
+
+ @Override
+ public String getBody() {
+ if (bodyBuffer != null) {
+ return bodyBuffer.toString();
+ }
+ return null;
+ }
+
+ @Override
+ public String getPage() {
+ if (pageBuffer != null) {
+ return pageBuffer.toString();
+ }
+ return null;
+ }
+
+ public int originalLength() {
+ return pageBuffer.size();
+ }
+
+ public void writeOriginal(Writer writer) throws IOException {
+ writePage(writer);
+ }
+
+ public void setHeadBuffer(StreamCharBuffer headBuffer) {
+ this.headBuffer = headBuffer;
+ applyStreamCharBufferSettings(headBuffer);
+ used = true;
+ }
+
+ private void applyStreamCharBufferSettings(StreamCharBuffer buffer) {
+ if (!renderingLayout && buffer != null) {
+ buffer.setPreferSubChunkWhenWritingToOtherBuffer(true);
+ }
+ }
+
+ public void setBodyBuffer(StreamCharBuffer bodyBuffer) {
+ this.bodyBuffer = bodyBuffer;
+ applyStreamCharBufferSettings(bodyBuffer);
+ used = true;
+ }
+
+ public void setPageBuffer(StreamCharBuffer pageBuffer) {
+ this.pageBuffer = pageBuffer;
+ applyStreamCharBufferSettings(pageBuffer);
+ }
+
+ public void setTitleBuffer(StreamCharBuffer titleBuffer) {
+ this.titleBuffer = titleBuffer;
+ applyStreamCharBufferSettings(titleBuffer);
+ }
+
+ public StreamCharBuffer getTitleBuffer() {
+ return titleBuffer;
+ }
+
+ public boolean isUsed() {
+ return used;
+ }
+
+ public void setUsed(boolean used) {
+ this.used = used;
+ }
+
+ /**
+ * @param tagName "tagName" name of buffer (without "page." prefix)
+ * @param buffer
+ */
+ public void setContentBuffer(String tagName, StreamCharBuffer buffer) {
+ used = true;
+ if (contentBuffers == null) {
+ contentBuffers = new HashMap<String, StreamCharBuffer>();
+ }
+ String propertyName = "page." + tagName;
+ contentBuffers.put(propertyName, buffer);
+ // just mark that the property is set
+ super.addProperty(propertyName, "");
+ }
+
+ /**
+ * @param name propertyName of contentBuffer (with "page." prefix)
+ * @return the buffer for the specified name
+ */
+ public Object getContentBuffer(String name) {
+ if (contentBuffers == null) {
+ return null;
+ }
+ return contentBuffers.get(name);
+ }
+
+ public static HTMLPage content2htmlPage(Content content) {
+ HTMLPage htmlPage = null;
+ if (content instanceof HTMLPage) {
+ htmlPage = (HTMLPage) content;
+ } else if (content instanceof TokenizedHTMLPage2Content) {
+ htmlPage = ((TokenizedHTMLPage2Content) content).getPage();
+ } else {
+ htmlPage = new Content2HTMLPage(content, null);
+ }
+ return htmlPage;
+ }
+
+ public boolean isTitleCaptured() {
+ return titleCaptured;
+ }
+
+ public void setTitleCaptured(boolean titleCaptured) {
+ this.titleCaptured = titleCaptured;
+ }
+}
\ No newline at end of file
diff --git a/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsContentBufferingResponse.java b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsContentBufferingResponse.java
new file mode 100644
index 0000000000..459cda176c
--- /dev/null
+++ b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsContentBufferingResponse.java
@@ -0,0 +1,141 @@
+/*
+ * 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.apache.grails.web.layout;
+
+import java.io.IOException;
+
+import grails.web.mvc.GrailsResponseMutator;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpServletResponseWrapper;
+
+import org.grails.web.servlet.mvc.GrailsWebRequest;
+import org.grails.web.util.WebUtils;
+
+import com.opensymphony.module.sitemesh.PageParser;
+import com.opensymphony.module.sitemesh.PageParserSelector;
+import com.opensymphony.sitemesh.Content;
+import com.opensymphony.sitemesh.ContentProcessor;
+import com.opensymphony.sitemesh.webapp.SiteMeshWebAppContext;
+
+public class GrailsContentBufferingResponse extends HttpServletResponseWrapper implements GrailsResponseMutator {
+
+ private final GrailsPageResponseWrapper pageResponseWrapper;
+ private final ContentProcessor contentProcessor;
+ private final SiteMeshWebAppContext webAppContext;
+ private boolean redirectCalled;
+
+ public GrailsContentBufferingResponse(HttpServletResponse response, final ContentProcessor contentProcessor, final SiteMeshWebAppContext webAppContext) {
+ super(new GrailsPageResponseWrapper(webAppContext.getRequest(), response, new PageParserSelector() {
+ public boolean shouldParsePage(String contentType) {
+ return contentProcessor.handles(contentType);
+ }
+
+ public PageParser getPageParser(String contentType) {
+ // Migration: Not actually needed by PageResponseWrapper, so long as getPage() isn't called.
+ return null;
+ }
+ }) {
+ @Override
+ public void setContentType(String contentType) {
+ webAppContext.setContentType(contentType);
+ super.setContentType(contentType);
+ }
+ });
+ this.contentProcessor = contentProcessor;
+ this.webAppContext = webAppContext;
+ pageResponseWrapper = (GrailsPageResponseWrapper) getResponse();
+ if (response.getContentType() != null) {
+ webAppContext.setContentType(response.getContentType());
+ }
+ }
+
+ public HttpServletResponse getTargetResponse() {
+ return (HttpServletResponse) pageResponseWrapper.getResponse();
+ }
+
+ public boolean isUsingStream() {
+ return pageResponseWrapper.isUsingStream();
+ }
+
+ public boolean isActive() {
+ return pageResponseWrapper.isGrailsLayoutActive() || pageResponseWrapper.isGspGrailsLayoutActive();
+ }
+
+ public void deactivateGrailsLayout() {
+ pageResponseWrapper.deactivateGrailsLayout();
+ }
+
+ public Content getContent() throws IOException {
+ if (!pageResponseWrapper.isGrailsLayoutActive()) {
+ return null;
+ }
+
+ GSPGrailsLayoutPage content = (GSPGrailsLayoutPage) webAppContext.getRequest().getAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE);
+ if (content != null && content.isUsed()) {
+ return content;
+ }
+
+ char[] data = pageResponseWrapper.getContents();
+ if (data != null && webAppContext.getContentType() != null) {
+ return contentProcessor.build(data, webAppContext);
+ }
+
+ return null;
+ }
+
+ @Override
+ public void sendError(int sc) throws IOException {
+ GrailsWebRequest webRequest = GrailsWebRequest.lookup();
+
+ try {
+ if (!redirectCalled && !isCommitted()) {
+ super.sendError(sc);
+ }
+ } finally {
+ if (webRequest != null) {
+ WebUtils.storeGrailsWebRequest(webRequest);
+ }
+ }
+ }
+
+ @Override
+ public void sendError(int sc, String msg) throws IOException {
+ GrailsWebRequest webRequest = GrailsWebRequest.lookup();
+ try {
+ if (!redirectCalled && !isCommitted()) {
+ super.sendError(sc, msg);
+ }
+ } finally {
+ if (webRequest != null) {
+ WebUtils.storeGrailsWebRequest(webRequest);
+ }
+ }
+ }
+
+ @Override
+ public void sendRedirect(String location) throws IOException {
+ this.redirectCalled = true;
+ super.sendRedirect(location);
+ }
+
+ @Override
+ public void deactivateResponseMutator() {
+ deactivateGrailsLayout();
+ }
+}
diff --git a/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsHTMLPageParser.java b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsHTMLPageParser.java
new file mode 100644
index 0000000000..254ea686d9
--- /dev/null
+++ b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsHTMLPageParser.java
@@ -0,0 +1,86 @@
+/*
+ * 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.apache.grails.web.layout;
+
+import java.io.IOException;
+
+import com.opensymphony.module.sitemesh.Page;
+import com.opensymphony.module.sitemesh.html.HTMLProcessor;
+import com.opensymphony.module.sitemesh.html.State;
+import com.opensymphony.module.sitemesh.html.StateTransitionRule;
+import com.opensymphony.module.sitemesh.html.rules.BodyTagRule;
+import com.opensymphony.module.sitemesh.html.rules.ContentBlockExtractingRule;
+import com.opensymphony.module.sitemesh.html.rules.FramesetRule;
+import com.opensymphony.module.sitemesh.html.rules.HeadExtractingRule;
+import com.opensymphony.module.sitemesh.html.rules.HtmlAttributesRule;
+import com.opensymphony.module.sitemesh.html.rules.MSOfficeDocumentPropertiesRule;
+import com.opensymphony.module.sitemesh.html.rules.MetaTagRule;
+import com.opensymphony.module.sitemesh.html.rules.PageBuilder;
+import com.opensymphony.module.sitemesh.html.rules.ParameterExtractingRule;
+import com.opensymphony.module.sitemesh.html.rules.TitleExtractingRule;
+import com.opensymphony.module.sitemesh.html.util.CharArray;
+import com.opensymphony.module.sitemesh.parser.HTMLPageParser;
+import com.opensymphony.module.sitemesh.parser.TokenizedHTMLPage;
+import com.opensymphony.sitemesh.Content;
+
+public class GrailsHTMLPageParser extends HTMLPageParser {
+
+ @Override
+ public Page parse(char[] data) throws IOException {
+ CharArray head = new CharArray(64);
+ CharArray body = new CharArray(4096);
+ TokenizedHTMLPage page = new TokenizedHTMLPage(data, body, head);
+ HTMLProcessor processor = new HTMLProcessor(data, body);
+ State html = processor.defaultState();
+
+ // Core rules for Grails Layout to be functional.
+ html.addRule(new HeadExtractingRule(head)); // contents of <head>
+ html.addRule(new BodyTagRule(page, body)); // contents of <body>
+ html.addRule(new TitleExtractingRule(page)); // the <title>
+ html.addRule(new FramesetRule(page)); // if the page is a frameset
+
+ // Additional rules - designed to be tweaked.
+ addUserDefinedRules(html, page);
+
+ processor.process();
+ return page;
+ }
+
+ public Content parseContent(char[] data) throws IOException {
+ return new TokenizedHTMLPage2Content((TokenizedHTMLPage) parse(data));
+ }
+
+ @Override
+ protected void addUserDefinedRules(State html, PageBuilder page) {
+ // Ensure that while in <xml> tag, none of the other rules kick in.
+ // For example <xml><book><title>hello</title></book></xml> should not change the affect the title of the page.
+ State xml = new State();
+ html.addRule(new StateTransitionRule("xml", xml));
+
+ // Useful properties
+ html.addRule(new HtmlAttributesRule(page)); // attributes in <html> element
+ html.addRule(new MetaTagRule(page)); // all <meta> tags
+ html.addRule(new ParameterExtractingRule(page)); // <parameter> blocks
+ html.addRule(new ContentBlockExtractingRule(page)); // <content> blocks
+
+ // Capture properties written to documents by MS Office (author, version, company, etc).
+ // Note: These properties are from the xml state, not the html state.
+ xml.addRule(new MSOfficeDocumentPropertiesRule(page));
+ }
+}
diff --git a/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsLayoutDecoratorMapper.java b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsLayoutDecoratorMapper.java
new file mode 100644
index 0000000000..d4549bdf67
--- /dev/null
+++ b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsLayoutDecoratorMapper.java
@@ -0,0 +1,91 @@
+/*
+ * 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.apache.grails.web.layout;
+
+import java.util.Properties;
+
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.http.HttpServletRequest;
+
+import grails.core.GrailsApplication;
+import org.grails.web.util.WebUtils;
+import org.springframework.web.context.WebApplicationContext;
+import org.springframework.web.context.support.WebApplicationContextUtils;
+
+import com.opensymphony.module.sitemesh.Config;
+import com.opensymphony.module.sitemesh.Decorator;
+import com.opensymphony.module.sitemesh.DecoratorMapper;
+import com.opensymphony.module.sitemesh.Page;
+import com.opensymphony.module.sitemesh.mapper.AbstractDecoratorMapper;
+
+/**
+ * Implements the SiteMesh decorator mapper interface and allows grails views to map to grails layouts.
+ *
+ * @author Graeme Rocher
+ * @author Lari Hotari
+ */
+public class GrailsLayoutDecoratorMapper extends AbstractDecoratorMapper {
+
+ private GroovyPageLayoutFinder groovyPageLayoutFinder;
+
+ @Override
+ public void init(Config c, Properties properties, DecoratorMapper parentMapper) throws InstantiationException {
+ super.init(c, properties, parentMapper);
+ ServletContext servletContext = c.getServletContext();
+ WebApplicationContext applicationContext = WebApplicationContextUtils.getWebApplicationContext(servletContext);
+ if (applicationContext != null) {
+ GrailsApplication grailsApplication = applicationContext.getBean(GrailsApplication.APPLICATION_ID, GrailsApplication.class);
+ groovyPageLayoutFinder = grailsApplication.getMainContext().getBean("groovyPageLayoutFinder", GroovyPageLayoutFinder.class);
+ }
+ }
+
+ @Override
+ public Decorator getDecorator(HttpServletRequest request, Page page) {
+ if (groovyPageLayoutFinder == null) {
+ return super.getDecorator(request, page);
+ }
+
+ Decorator layout = groovyPageLayoutFinder.findLayout(request, page);
+ if (layout != null) {
+ return layout;
+ }
+ layout = parent != null ? super.getDecorator(request, page) : null;
+ if (layout == null || layout.getPage() == null) {
+ layout = new GrailsNoDecorator();
+ }
+ return layout;
+ }
+
+ @Override
+ public Decorator getNamedDecorator(HttpServletRequest request, String name) {
+ if (groovyPageLayoutFinder == null) {
+ return super.getNamedDecorator(request, name);
+ }
+
+ Decorator layout = groovyPageLayoutFinder.getNamedDecorator(request, name);
+ if (layout != null) {
+ return layout;
+ }
+ layout = parent != null ? super.getNamedDecorator(request, name) : null;
+ if (layout == null || layout.getPage() == null) {
+ layout = new GrailsNoDecorator();
+ }
+ return layout;
+ }
+}
diff --git a/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsLayoutRenderViewMutator.groovy b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsLayoutRenderViewMutator.groovy
new file mode 100644
index 0000000000..19c7e4925f
--- /dev/null
+++ b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsLayoutRenderViewMutator.groovy
@@ -0,0 +1,51 @@
+/*
+ * 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.apache.grails.web.layout
+
+import grails.web.pages.GrailsRenderViewMutator
+import org.springframework.beans.factory.NoSuchBeanDefinitionException
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.web.servlet.View
+
+class GrailsLayoutRenderViewMutator implements GrailsRenderViewMutator {
+
+ @Autowired
+ GroovyPageLayoutFinder groovyPageLayoutFinder
+
+ @Override
+ View mutateView(boolean renderWithLayout, String templateUri, Locale locale, View existingView) {
+ View toReturn = existingView
+
+ // if automatic decoration occurred unwrap, since this is a partial
+ if (toReturn instanceof EmbeddedGrailsLayoutView) {
+ toReturn = ((EmbeddedGrailsLayoutView) toReturn).getInnerView()
+ }
+
+ if (renderWithLayout) {
+ try {
+ toReturn = new EmbeddedGrailsLayoutView(groovyPageLayoutFinder, toReturn)
+ } catch (NoSuchBeanDefinitionException ignored) {
+ // ignore
+ }
+ }
+
+ return toReturn
+ }
+}
diff --git a/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsLayoutView.java b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsLayoutView.java
new file mode 100644
index 0000000000..e515280cb4
--- /dev/null
+++ b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsLayoutView.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.apache.grails.web.layout;
+
+import java.util.Map;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.grails.web.servlet.mvc.GrailsWebRequest;
+import org.springframework.web.servlet.View;
+
+import com.opensymphony.sitemesh.ContentProcessor;
+import com.opensymphony.sitemesh.webapp.SiteMeshWebAppContext;
+
+public class GrailsLayoutView extends EmbeddedGrailsLayoutView {
+
+ private final ContentProcessor contentProcessor;
+
+ public GrailsLayoutView(GroovyPageLayoutFinder groovyPageLayoutFinder, View innerView, ContentProcessor contentProcessor) {
+ super(groovyPageLayoutFinder, innerView);
+ this.contentProcessor = contentProcessor;
+ }
+
+ @Override
+ protected GrailsContentBufferingResponse createContentBufferingResponse(Map<String, Object> model,
+ GrailsWebRequest webRequest, HttpServletRequest request, HttpServletResponse response) {
+ return new GrailsContentBufferingResponse(response, contentProcessor, new SiteMeshWebAppContext(request, response, webRequest.getServletContext()));
+ }
+}
\ No newline at end of file
diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/AsyncFunctionalSpec.groovy b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsNoDecorator.java
similarity index 52%
copy from grails-test-examples/app1/src/integration-test/groovy/functionaltests/AsyncFunctionalSpec.groovy
copy to grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsNoDecorator.java
index 9db409059e..b775cddcfb 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/AsyncFunctionalSpec.groovy
+++ b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsNoDecorator.java
@@ -17,29 +17,41 @@
* under the License.
*/
-package functionaltests
+package org.apache.grails.web.layout;
-import grails.gorm.transactions.Rollback
-import grails.plugin.geb.ContainerGebSpec
-import grails.testing.mixin.integration.Integration
-
-import spock.lang.PendingFeature
+import java.util.Iterator;
+import com.opensymphony.module.sitemesh.Decorator;
+import com.opensymphony.sitemesh.webapp.decorator.NoDecorator;
/**
- * @author graemerocher
+ * Grails version of Sitemesh's NoDecorator
+ *
+ * @author Lari Hotari, Sagire Software Oy
*/
-@Integration(applicationClass = Application)
-@Rollback
-class AsyncFunctionalSpec extends ContainerGebSpec {
+public class GrailsNoDecorator extends NoDecorator implements Decorator {
- @PendingFeature(reason = 'pageSource == <html><head></head><body></body></html>')
- void "Test async response rendering works"() {
- when: "When an async response is rendered"
- go('/async/test')
+ public String getPage() {
+ return null;
+ }
- then: "The rendered page is correct"
- pageSource.contains('Hello')
+ public String getName() {
+ return null;
}
-}
+ public String getURIPath() {
+ return null;
+ }
+ public String getRole() {
+ return null;
+ }
+
+ public String getInitParameter(String paramName) {
+ return null;
+ }
+
+ @SuppressWarnings("rawtypes")
+ public Iterator getInitParameterNames() {
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsPageResponseWrapper.java b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsPageResponseWrapper.java
new file mode 100644
index 0000000000..265140ebc4
--- /dev/null
+++ b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsPageResponseWrapper.java
@@ -0,0 +1,391 @@
+/*
+ * 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.apache.grails.web.layout;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+
+import jakarta.servlet.ServletOutputStream;
+import jakarta.servlet.WriteListener;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpServletResponseWrapper;
+
+import org.grails.web.servlet.mvc.GrailsWebRequest;
+import org.grails.buffer.GrailsPrintWriterAdapter;
+import org.grails.buffer.StreamByteBuffer;
+import org.grails.buffer.StreamCharBuffer;
+import org.grails.web.util.WebUtils;
+
+import com.opensymphony.module.sitemesh.Page;
+import com.opensymphony.module.sitemesh.PageParser;
+import com.opensymphony.module.sitemesh.PageParserSelector;
+import com.opensymphony.module.sitemesh.filter.HttpContentType;
+import com.opensymphony.module.sitemesh.filter.RoutableServletOutputStream;
+import com.opensymphony.module.sitemesh.filter.TextEncoder;
+
+/**
+ * @author Graeme Rocher
+ * @since 1.0.4
+ */
+public class GrailsPageResponseWrapper extends HttpServletResponseWrapper {
+
+ private final GrailsRoutablePrintWriter routablePrintWriter;
+ private final RoutableServletOutputStream routableServletOutputStream;
+ private final PageParserSelector parserSelector;
+ private final HttpServletRequest request;
+
+ private GrailsBuffer buffer;
+ private boolean parseablePage = false;
+ private GSPGrailsLayoutPage gspGrailsLayoutPage;
+
+ public GrailsPageResponseWrapper(final HttpServletRequest request, final HttpServletResponse response,
+ PageParserSelector parserSelector) {
+ super(response);
+ this.parserSelector = parserSelector;
+
+ routablePrintWriter = GrailsRoutablePrintWriter.newInstance(new GrailsRoutablePrintWriter.DestinationFactory() {
+ public PrintWriter activateDestination() throws IOException {
+ return response.getWriter();
+ }
+ });
+ routableServletOutputStream = new RoutableServletOutputStream(new RoutableServletOutputStream.DestinationFactory() {
+ public ServletOutputStream create() throws IOException {
+ return response.getOutputStream();
+ }
+ });
+
+ this.request = request;
+
+ gspGrailsLayoutPage = (GSPGrailsLayoutPage) request.getAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE);
+
+ applyContentType(response.getContentType());
+ }
+
+ @Override
+ public void sendError(int sc) throws IOException {
+ clearBuffer();
+ GrailsWebRequest webRequest = GrailsWebRequest.lookup();
+ try {
+ super.sendError(sc);
+ } finally {
+ if (webRequest != null) {
+ WebUtils.storeGrailsWebRequest(webRequest);
+ }
+ }
+ }
+
+ @Override
+ public void sendError(int sc, String msg) throws IOException {
+ clearBuffer();
+ GrailsWebRequest webRequest = GrailsWebRequest.lookup();
+ try {
+ super.sendError(sc, msg);
+ } finally {
+ if (webRequest != null) {
+ WebUtils.storeGrailsWebRequest(webRequest);
+ }
+ }
+ }
+
+ /**
+ * Set the content-type of the request and store it so it can
+ * be passed to the {@link com.opensymphony.module.sitemesh.PageParser}.
+ */
+ @Override
+ public void setContentType(String type) {
+ super.setContentType(type);
+ applyContentType(type);
+ }
+
+ protected void applyContentType(String type) {
+ if (type == null) {
+ return;
+ }
+
+ HttpContentType httpContentType = new HttpContentType(type);
+
+ if (parserSelector.shouldParsePage(httpContentType.getType())) {
+ activateGrailsLayout(httpContentType.getType(), httpContentType.getEncoding());
+ } else {
+ deactivateGrailsLayout();
+ }
+ }
+
+ public void activateGrailsLayout(String contentType, String encoding) {
+ if (parseablePage) {
+ return; // already activated
+ }
+
+ buffer = new GrailsBuffer(parserSelector, contentType, encoding, gspGrailsLayoutPage);
+ routablePrintWriter.updateDestination(new GrailsRoutablePrintWriter.DestinationFactory() {
+ public PrintWriter activateDestination() {
+ return buffer.getWriter();
+ }
+ });
+ routablePrintWriter.blockFlushAndClose();
+ routableServletOutputStream.updateDestination(new RoutableServletOutputStream.DestinationFactory() {
+ public ServletOutputStream create() {
+ return buffer.getOutputStream();
+ }
+ });
+ parseablePage = true;
+ }
+
+ public void deactivateGrailsLayout() {
+ parseablePage = false;
+ buffer = null;
+ if (gspGrailsLayoutPage != null) {
+ gspGrailsLayoutPage.reset();
+ }
+ routablePrintWriter.updateDestination(new GrailsRoutablePrintWriter.DestinationFactory() {
+ public PrintWriter activateDestination() throws IOException {
+ return getResponse().getWriter();
+ }
+ });
+ routablePrintWriter.unBlockFlushAndClose();
+ routableServletOutputStream.updateDestination(new RoutableServletOutputStream.DestinationFactory() {
+ public ServletOutputStream create() throws IOException {
+ return getResponse().getOutputStream();
+ }
+ });
+ }
+
+ /**
+ * Prevent content-length being set if page is parseable.
+ */
+ @Override
+ public void setContentLength(int contentLength) {
+ if (!parseablePage) super.setContentLength(contentLength);
+ }
+
+ /**
+ * Prevent buffer from being flushed if this is a page being parsed.
+ */
+ @Override
+ public void flushBuffer() throws IOException {
+ if (!parseablePage) super.flushBuffer();
+ }
+
+ /**
+ * Prevent content-length being set if page is parseable.
+ */
+ @Override
+ public void setHeader(String name, String value) {
+ if (name.equalsIgnoreCase("content-type")) { // ensure ContentType is always set through setContentType()
+ setContentType(value);
+ } else if (!parseablePage || !name.equalsIgnoreCase("content-length")) {
+ super.setHeader(name, value);
+ }
+ }
+
+ /**
+ * Prevent content-length being set if page is parseable.
+ */
+ @Override
+ public void addHeader(String name, String value) {
+ if (name.equalsIgnoreCase("content-type")) { // ensure ContentType is always set through setContentType()
+ setContentType(value);
+ } else if (!parseablePage || !name.equalsIgnoreCase("content-length")) {
+ super.addHeader(name, value);
+ }
+ }
+
+ /**
+ * If 'not modified' (304) HTTP status is being sent - then abort parsing, as there shouldn't be any body
+ */
+ @Override
+ public void setStatus(int sc) {
+ if (sc == HttpServletResponse.SC_NOT_MODIFIED) {
+ // route any content back to the original writer. There shouldn't be any content, but just to be safe
+ deactivateGrailsLayout();
+ } else if (sc >= 400) {
+ clearBuffer();
+ }
+ super.setStatus(sc);
+ }
+
+ protected void clearBuffer() {
+ if (buffer != null) {
+ buffer.clear();
+ }
+ }
+
+ @Override
+ public ServletOutputStream getOutputStream() {
+ return routableServletOutputStream;
+ }
+
+ @Override
+ public PrintWriter getWriter() {
+ return routablePrintWriter;
+ }
+
+ public Page getPage() throws IOException {
+ if (isGrailsLayoutNotActive()) {
+ return null;
+ }
+
+ GSPGrailsLayoutPage page = (GSPGrailsLayoutPage) request.getAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE);
+ if (page != null && page.isUsed()) {
+ return page;
+ }
+
+ return buffer.parse();
+ }
+
+ @Override
+ public void sendRedirect(String location) throws IOException {
+ clearBuffer();
+ super.sendRedirect(location);
+ }
+
+ public boolean isUsingStream() {
+ return buffer != null && buffer.isUsingStream();
+ }
+
+ public char[] getContents() throws IOException {
+ if (isGrailsLayoutNotActive()) {
+ return null;
+ }
+
+ return buffer.getContents();
+ }
+
+ public boolean isGrailsLayoutActive() {
+ return !isGrailsLayoutNotActive();
+ }
+
+ public boolean isGspGrailsLayoutActive() {
+ return (gspGrailsLayoutPage != null && gspGrailsLayoutPage.isUsed());
+ }
+
+ private boolean isGrailsLayoutNotActive() {
+ return !parseablePage;
+ }
+
+ private static class GrailsBuffer {
+ private final PageParserSelector parserSelector;
+ private final String contentType;
+ private final String encoding;
+ private final static TextEncoder TEXT_ENCODER = new TextEncoder();
+
+ private StreamCharBuffer charBuffer;
+ private GrailsPrintWriterAdapter exposedWriter;
+ private StreamByteBuffer byteBuffer;
+ private ServletOutputStream exposedStream;
+
+ private GSPGrailsLayoutPage gspGrailsLayoutPage;
+
+ public GrailsBuffer(PageParserSelector parserSelector, String contentType, String encoding, GSPGrailsLayoutPage gspGrailsLayoutPage) {
+ this.parserSelector = parserSelector;
+ this.contentType = contentType;
+ this.encoding = encoding;
+ this.gspGrailsLayoutPage = gspGrailsLayoutPage;
+ }
+
+ public void clear() {
+ if (charBuffer != null) {
+ charBuffer.clear();
+ } else if (byteBuffer != null) {
+ byteBuffer.clear();
+ }
+ if (gspGrailsLayoutPage != null) {
+ gspGrailsLayoutPage.reset();
+ }
+ }
+
+ private char[] getContents() throws IOException {
+ if (charBuffer != null) {
+ if (!charBuffer.isEmpty()) {
+ return charBuffer.toCharArray();
+ } else {
+ return null;
+ }
+ }
+ if (byteBuffer != null) {
+ return TEXT_ENCODER.encode(byteBuffer.readAsByteArray(), encoding);
+ }
+
+ return new char[0];
+ }
+
+ public Page parse() throws IOException {
+ PageParser pageParser = parserSelector.getPageParser(contentType);
+ return pageParser != null ? pageParser.parse(getContents()) : null;
+ }
+
+ public PrintWriter getWriter() {
+ if (charBuffer == null) {
+ if (byteBuffer != null) {
+ throw new IllegalStateException("response.getWriter() called after response.getOutputStream()");
+ }
+ charBuffer = new StreamCharBuffer();
+ charBuffer.setNotifyParentBuffersEnabled(false);
+ if (gspGrailsLayoutPage != null) {
+ gspGrailsLayoutPage.setPageBuffer(charBuffer);
+ }
+ exposedWriter = GrailsPrintWriterAdapter.newInstance(charBuffer.getWriter());
+ }
+ return exposedWriter;
+ }
+
+ public ServletOutputStream getOutputStream() {
+ if (byteBuffer == null) {
+ if (charBuffer != null) {
+ throw new IllegalStateException("response.getOutputStream() called after response.getWriter()");
+ }
+ byteBuffer = new StreamByteBuffer();
+ final OutputStream out = byteBuffer.getOutputStream();
+ exposedStream = new ServletOutputStream() {
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+
+ @Override
+ public void setWriteListener(WriteListener writeListener) {
+ throw new UnsupportedOperationException("Method setWriteListener not supported");
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ out.write(b, off, len);
+ }
+
+ @Override
+ public void write(byte[] b) throws IOException {
+ out.write(b);
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ out.write(b);
+ }
+ };
+ }
+ return exposedStream;
+ }
+
+ public boolean isUsingStream() {
+ return byteBuffer != null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/grails-encoder/src/main/groovy/org/grails/buffer/GrailsRoutablePrintWriter.java b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsRoutablePrintWriter.java
similarity index 97%
rename from grails-encoder/src/main/groovy/org/grails/buffer/GrailsRoutablePrintWriter.java
rename to grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsRoutablePrintWriter.java
index 6febc65af3..e082911c7d 100644
--- a/grails-encoder/src/main/groovy/org/grails/buffer/GrailsRoutablePrintWriter.java
+++ b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsRoutablePrintWriter.java
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails.buffer;
+package org.apache.grails.web.layout;
import groovy.lang.Writable;
@@ -24,13 +24,14 @@ import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
+import org.grails.buffer.GrailsPrintWriter;
+import org.grails.buffer.GrailsPrintWriterAdapter;
+import org.grails.buffer.StreamCharBuffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.objenesis.ObjenesisStd;
import org.springframework.objenesis.instantiator.ObjectInstantiator;
-// Moved from grails-gsp:
-// https://github.com/grails/grails-gsp/blob/v6.0.2/grails-web-sitemesh/src/main/groovy/org/grails/web/sitemesh/GrailsRoutablePrintWriter.java
public class GrailsRoutablePrintWriter extends GrailsPrintWriterAdapter {
private static final Logger LOG = LoggerFactory.getLogger(GrailsRoutablePrintWriter.class);
private DestinationFactory factory;
diff --git a/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsViewBufferingResponse.java b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsViewBufferingResponse.java
new file mode 100644
index 0000000000..ec90d84282
--- /dev/null
+++ b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GrailsViewBufferingResponse.java
@@ -0,0 +1,59 @@
+/*
+ * 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.apache.grails.web.layout;
+
+import java.io.IOException;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import com.opensymphony.sitemesh.Content;
+import com.opensymphony.sitemesh.ContentProcessor;
+import com.opensymphony.sitemesh.SiteMeshContext;
+import com.opensymphony.sitemesh.webapp.SiteMeshWebAppContext;
+
+public class GrailsViewBufferingResponse extends GrailsContentBufferingResponse {
+
+ private static class SimpleWebAppContext extends SiteMeshWebAppContext {
+ public SimpleWebAppContext(HttpServletRequest request, HttpServletResponse response) {
+ super(request, response, request.getServletContext());
+ }
+ }
+
+ private static class SimpleHtmlOnlyContentProcessor implements ContentProcessor {
+ @Override
+ public Content build(final char[] data, SiteMeshContext context) throws IOException {
+ return new GrailsHTMLPageParser().parseContent(data);
+ }
+
+ @Override
+ public boolean handles(SiteMeshContext context) {
+ return handles(context.getContentType());
+ }
+
+ @Override
+ public boolean handles(String contentType) {
+ return contentType != null && contentType.contains("html");
+ }
+ }
+
+ public GrailsViewBufferingResponse(HttpServletRequest request, HttpServletResponse response) {
+ super(response, new SimpleHtmlOnlyContentProcessor(), new SimpleWebAppContext(request, response));
+ }
+}
\ No newline at end of file
diff --git a/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GroovyPageLayoutFinder.java b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GroovyPageLayoutFinder.java
new file mode 100644
index 0000000000..a24eee1493
--- /dev/null
+++ b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/GroovyPageLayoutFinder.java
@@ -0,0 +1,310 @@
+/*
+ * 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.apache.grails.web.layout;
+
+import grails.util.Environment;
+import grails.util.GrailsNameUtils;
+import grails.web.pages.GroovyPagesUriService;
+import groovy.lang.GroovyObject;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import grails.util.GrailsClassUtils;
+import grails.util.GrailsStringUtils;
+import org.grails.core.artefact.ControllerArtefactHandler;
+import org.grails.io.support.GrailsResourceUtils;
+import org.grails.web.servlet.mvc.GrailsWebRequest;
+import org.grails.web.util.GrailsApplicationAttributes;
+import org.grails.web.servlet.view.AbstractGrailsView;
+import org.grails.web.servlet.view.GrailsViewResolver;
+import org.grails.web.servlet.view.LayoutViewResolver;
+import org.grails.web.util.WebUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.core.Ordered;
+import org.springframework.web.servlet.View;
+import org.springframework.web.servlet.ViewResolver;
+
+import com.opensymphony.module.sitemesh.Decorator;
+import com.opensymphony.module.sitemesh.Page;
+import com.opensymphony.sitemesh.Content;
+
+/**
+ * Provides the logic for GrailsLayoutDecoratorMapper without so many ties to
+ * the Sitemesh API.
+ *
+ * @author Graeme Rocher
+ * @since 2.0
+ */
+public class GroovyPageLayoutFinder implements ApplicationListener<ContextRefreshedEvent>, Ordered {
+
+ private static final Logger LOG = LoggerFactory.getLogger(GrailsLayoutDecoratorMapper.class);
+ private static final long LAYOUT_CACHE_EXPIRATION_MILLIS = Long.getLong("grails.gsp.reload.interval", 5000);
+ private static final String LAYOUTS_PATH = "/layouts";
+
+ private static final int ORDER = Ordered.LOWEST_PRECEDENCE - 1;
+
+ private Map<String, DecoratorCacheValue> decoratorCache = new ConcurrentHashMap<String, DecoratorCacheValue>();
+ private Map<LayoutCacheKey, DecoratorCacheValue> layoutDecoratorCache = new ConcurrentHashMap<LayoutCacheKey, DecoratorCacheValue>();
+
+ private String defaultDecoratorName;
+ private boolean gspReloadEnabled;
+ private boolean cacheEnabled = (Environment.getCurrent() != Environment.DEVELOPMENT);
+ private ViewResolver viewResolver;
+ private boolean enableNonGspViews = false;
+
+ @Override
+ public int getOrder() {
+ return ORDER;
+ }
+
+ public void setDefaultDecoratorName(String defaultDecoratorName) {
+ this.defaultDecoratorName = defaultDecoratorName;
+ }
+
+ public void setEnableNonGspViews(boolean enableNonGspViews) {
+ this.enableNonGspViews = enableNonGspViews;
+ }
+
+ public void setGspReloadEnabled(boolean gspReloadEnabled) {
+ this.gspReloadEnabled = gspReloadEnabled;
+ }
+
+ public void setCacheEnabled(boolean cacheEnabled) {
+ this.cacheEnabled = cacheEnabled;
+ }
+
+ public void setViewResolver(ViewResolver viewResolver) {
+ if (viewResolver instanceof LayoutViewResolver) {
+ this.viewResolver = ((LayoutViewResolver) viewResolver).getInnerViewResolver();
+ } else {
+ this.viewResolver = viewResolver;
+ }
+ }
+
+ public Decorator findLayout(HttpServletRequest request, Content page) {
+ return findLayout(request, GSPGrailsLayoutPage.content2htmlPage(page));
+ }
+
+ public Decorator findLayout(HttpServletRequest request, Page page) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Evaluating layout for request: {}", request.getRequestURI());
+ }
+ final Object layoutAttribute = request.getAttribute(WebUtils.LAYOUT_ATTRIBUTE);
+ if (request.getAttribute(GroovyPagesUriService.RENDERING_VIEW_ATTRIBUTE) != null || layoutAttribute != null) {
+ String layoutName = layoutAttribute == null ? null : layoutAttribute.toString();
+
+ if (layoutName == null) {
+ layoutName = page.getProperty("meta.layout");
+ }
+
+ Decorator d = null;
+
+ if (GrailsStringUtils.isBlank(layoutName)) {
+ GroovyObject controller = (GroovyObject) request.getAttribute(GrailsApplicationAttributes.CONTROLLER);
+ if (controller != null) {
+ GrailsWebRequest webRequest = GrailsWebRequest.lookup(request);
+ String controllerName = webRequest.getControllerName();
+ if (controllerName == null) {
+ controllerName = GrailsNameUtils.getLogicalPropertyName(controller.getClass().getName(), ControllerArtefactHandler.TYPE);
+ }
+ String actionUri = webRequest.getAttributes().getControllerActionUri(request);
+
+ if (controllerName != null && actionUri != null) {
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Found controller in request, locating layout for controller [{}] and action [{}}]", controllerName, actionUri);
+ }
+
+ LayoutCacheKey cacheKey = null;
+ boolean cachedIsNull = false;
+
+ if (cacheEnabled) {
+ cacheKey = new LayoutCacheKey(controllerName, actionUri);
+ DecoratorCacheValue cacheValue = layoutDecoratorCache.get(cacheKey);
+ if (cacheValue != null && (!gspReloadEnabled || !cacheValue.isExpired())) {
+ d = cacheValue.getDecorator();
+ if (d == null) {
+ cachedIsNull = true;
+ }
+ }
+ }
+
+ if (d == null && !cachedIsNull) {
+ d = resolveDecorator(request, controller, controllerName, actionUri);
+ if (cacheEnabled) {
+ if (LOG.isDebugEnabled() && d != null) {
+ LOG.debug("Caching resolved layout {} for controller {} and action {}", d.getPage(), controllerName, actionUri);
+ }
+ layoutDecoratorCache.put(cacheKey, new DecoratorCacheValue(d));
+ }
+ }
+ }
+ } else {
+ d = getApplicationDefaultDecorator(request);
+ }
+ } else {
+ d = getNamedDecorator(request, layoutName);
+ }
+
+ if (d != null) {
+ return d;
+ }
+ }
+ return null;
+ }
+
+ protected Decorator getApplicationDefaultDecorator(HttpServletRequest request) {
+ return getNamedDecorator(request, defaultDecoratorName == null ? "application" : defaultDecoratorName,
+ !enableNonGspViews || defaultDecoratorName == null);
+ }
+
+ public Decorator getNamedDecorator(HttpServletRequest request, String name) {
+ return getNamedDecorator(request, name, !enableNonGspViews);
+ }
+
+ public Decorator getNamedDecorator(HttpServletRequest request, String name, boolean viewMustExist) {
+ if (GrailsStringUtils.isBlank(name) || WebUtils.NONE_LAYOUT.equals(name)) {
+ return null;
+ }
+
+ if (cacheEnabled) {
+ DecoratorCacheValue cacheValue = decoratorCache.get(name);
+ if (cacheValue != null && (!gspReloadEnabled || !cacheValue.isExpired())) {
+ return cacheValue.getDecorator();
+ }
+ }
+
+ View view;
+ try {
+ view = viewResolver.resolveViewName(GrailsResourceUtils.cleanPath(GrailsResourceUtils.appendPiecesForUri(LAYOUTS_PATH, name)),
+ request.getLocale());
+ // it's only possible to check that GroovyPageView exists
+ if (viewMustExist && !(view instanceof AbstractGrailsView)) {
+ view = null;
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Unable to resolve view", e);
+ }
+
+ Decorator d = null;
+ if (view != null) {
+ d = createDecorator(name, view);
+ }
+
+ if (cacheEnabled) {
+ decoratorCache.put(name, new DecoratorCacheValue(d));
+ }
+ return d;
+ }
+
+ private Decorator resolveDecorator(HttpServletRequest request, GroovyObject controller, String controllerName,
+ String actionUri) {
+ Decorator d = null;
+
+ Object layoutProperty = GrailsClassUtils.getStaticPropertyValue(controller.getClass(), "layout");
+ if (layoutProperty instanceof CharSequence) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("layout property found in controller, looking for template named {}", layoutProperty);
+ }
+ d = getNamedDecorator(request, layoutProperty.toString());
+ } else {
+ if (!GrailsStringUtils.isBlank(actionUri)) {
+ d = getNamedDecorator(request, actionUri.substring(1), true);
+ }
+
+ if (d == null && !GrailsStringUtils.isBlank(controllerName)) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Action layout not found, trying controller");
+ }
+ d = getNamedDecorator(request, controllerName, true);
+ }
+
+ if (d == null) {
+ d = getApplicationDefaultDecorator(request);
+ }
+ }
+
+ return d;
+ }
+
+ private Decorator createDecorator(String decoratorName, View view) {
+ return new SpringMVCViewDecorator(decoratorName, view);
+ }
+
+ private static class LayoutCacheKey {
+ private String controllerName;
+ private String actionUri;
+
+ public LayoutCacheKey(String controllerName, String actionUri) {
+ this.controllerName = controllerName;
+ this.actionUri = actionUri;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ LayoutCacheKey that = (LayoutCacheKey) o;
+
+ if (!actionUri.equals(that.actionUri)) return false;
+ if (!controllerName.equals(that.controllerName)) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = controllerName.hashCode();
+ result = 31 * result + actionUri.hashCode();
+ return result;
+ }
+ }
+
+ private static class DecoratorCacheValue {
+ Decorator decorator;
+ long createTimestamp = System.currentTimeMillis();
+
+ public DecoratorCacheValue(Decorator decorator) {
+ this.decorator = decorator;
+ }
+
+ public Decorator getDecorator() {
+ return decorator;
+ }
+
+ public boolean isExpired() {
+ return System.currentTimeMillis() - createTimestamp > LAYOUT_CACHE_EXPIRATION_MILLIS;
+ }
+ }
+
+ @Override
+ public void onApplicationEvent(ContextRefreshedEvent event) {
+ if (!(viewResolver instanceof GrailsViewResolver)) {
+ setViewResolver(event.getApplicationContext().getBean(GrailsViewResolver.class));
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/AsyncFunctionalSpec.groovy b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/LayoutSelector.groovy
similarity index 56%
copy from grails-test-examples/app1/src/integration-test/groovy/functionaltests/AsyncFunctionalSpec.groovy
copy to grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/LayoutSelector.groovy
index 9db409059e..0be3240ae6 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/AsyncFunctionalSpec.groovy
+++ b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/LayoutSelector.groovy
@@ -17,29 +17,15 @@
* under the License.
*/
-package functionaltests
+package org.apache.grails.web.layout
-import grails.gorm.transactions.Rollback
-import grails.plugin.geb.ContainerGebSpec
-import grails.testing.mixin.integration.Integration
+import grails.web.pages.GrailsLayoutSelector
+import org.grails.web.util.WebUtils
-import spock.lang.PendingFeature
-
-/**
- * @author graemerocher
- */
-@Integration(applicationClass = Application)
-@Rollback
-class AsyncFunctionalSpec extends ContainerGebSpec {
-
- @PendingFeature(reason = 'pageSource == <html><head></head><body></body></html>')
- void "Test async response rendering works"() {
- when: "When an async response is rendered"
- go('/async/test')
-
- then: "The rendered page is correct"
- pageSource.contains('Hello')
+class LayoutSelector implements GrailsLayoutSelector {
+
+ @Override
+ String selectLayout(String explicitLayoutArg, boolean renderView) {
+ explicitLayoutArg != null ? explicitLayoutArg : (renderView ? null : WebUtils.NONE_LAYOUT)
}
-
}
-
diff --git a/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/SpringMVCViewDecorator.java b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/SpringMVCViewDecorator.java
new file mode 100644
index 0000000000..32d0c7cd31
--- /dev/null
+++ b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/SpringMVCViewDecorator.java
@@ -0,0 +1,114 @@
+/*
+ * 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.apache.grails.web.layout;
+
+import groovy.text.Template;
+
+import java.util.Collections;
+import java.util.Map;
+
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.grails.web.util.GrailsApplicationAttributes;
+import org.grails.web.servlet.view.AbstractGrailsView;
+import org.grails.web.util.WebUtils;
+import org.springframework.web.servlet.View;
+import org.springframework.web.servlet.view.AbstractUrlBasedView;
+
+import com.opensymphony.module.sitemesh.HTMLPage;
+import com.opensymphony.module.sitemesh.RequestConstants;
+import com.opensymphony.module.sitemesh.mapper.DefaultDecorator;
+import com.opensymphony.sitemesh.Content;
+import com.opensymphony.sitemesh.SiteMeshContext;
+import com.opensymphony.sitemesh.webapp.SiteMeshWebAppContext;
+
+/**
+ * Encapsulates the logic for rendering a layout.
+ *
+ * @author Graeme Rocher
+ * @since 2.0
+ */
+public class SpringMVCViewDecorator extends DefaultDecorator implements com.opensymphony.sitemesh.Decorator {
+ private View view;
+
+ public SpringMVCViewDecorator(String name, View view) {
+ super(name, (view instanceof AbstractUrlBasedView) ? ((AbstractUrlBasedView)view).getUrl() : view.toString(), Collections.emptyMap());
+ this.view = view;
+ }
+
+ public void render(Content content, SiteMeshContext context) {
+ SiteMeshWebAppContext ctx = (SiteMeshWebAppContext) context;
+ render(content, Collections.<String, Object>emptyMap(), ctx.getRequest(), ctx.getResponse(), ctx.getServletContext());
+ }
+
+ public void render(Content content, Map<String, ?> model, HttpServletRequest request,
+ HttpServletResponse response, ServletContext servletContext) {
+ HTMLPage htmlPage = GSPGrailsLayoutPage.content2htmlPage(content);
+ request.setAttribute(RequestConstants.PAGE, htmlPage);
+
+ // get the dispatcher for the decorator
+ if (!response.isCommitted()) {
+ boolean dispatched = false;
+ try {
+ request.setAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE, new GSPGrailsLayoutPage(true));
+ try {
+ view.render(model, request, response);
+ dispatched = true;
+ if (!response.isCommitted()) {
+ response.getWriter().flush();
+ }
+ } catch (Exception e) {
+ cleanRequestAttributes(request);
+ String message = "Error applying layout : " + getName();
+ if(view instanceof AbstractGrailsView) {
+ ((AbstractGrailsView)view).rethrowRenderException(e, message);
+ } else {
+ throw new RuntimeException(message, e);
+ }
+ }
+ } finally {
+ if (!dispatched) {
+ cleanRequestAttributes(request);
+ }
+ }
+ }
+
+ request.removeAttribute(RequestConstants.PAGE);
+ request.removeAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE);
+ }
+
+ private void cleanRequestAttributes(HttpServletRequest request) {
+ request.removeAttribute(GrailsApplicationAttributes.PAGE_SCOPE);
+ request.removeAttribute(WebUtils.LAYOUT_ATTRIBUTE);
+ }
+
+ public View getView() {
+ return view;
+ }
+
+ public Template getTemplate() {
+ if(view instanceof AbstractGrailsView) {
+ return ((AbstractGrailsView)view).getTemplate();
+ }
+ return null;
+ }
+}
diff --git a/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/TokenizedHTMLPage2Content.java b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/TokenizedHTMLPage2Content.java
new file mode 100644
index 0000000000..2350ca34c0
--- /dev/null
+++ b/grails-gsp/grails-web-layout/src/main/groovy/org/apache/grails/web/layout/TokenizedHTMLPage2Content.java
@@ -0,0 +1,78 @@
+/*
+ * 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.apache.grails.web.layout;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import com.opensymphony.sitemesh.Content;
+import com.opensymphony.module.sitemesh.parser.TokenizedHTMLPage;
+
+final class TokenizedHTMLPage2Content implements Content {
+
+ private final TokenizedHTMLPage page;
+
+ TokenizedHTMLPage2Content(TokenizedHTMLPage page) {
+ this.page = page;
+ }
+
+ @Override
+ public void writeOriginal(Writer out) throws IOException {
+ page.writePage(out);
+ }
+
+ @Override
+ public void writeHead(Writer out) throws IOException {
+ page.writeHead(out);
+ }
+
+ @Override
+ public void writeBody(Writer out) throws IOException {
+ page.writeBody(out);
+ }
+
+ @Override
+ public int originalLength() {
+ return page.getContentLength();
+ }
+
+ @Override
+ public String getTitle() {
+ return page.getTitle();
+ }
+
+ @Override
+ public String[] getPropertyKeys() {
+ return getPropertyKeys();
+ }
+
+ @Override
+ public String getProperty(String name) {
+ return page.getProperty(name);
+ }
+
+ @Override
+ public void addProperty(String name, String value) {
+ page.addProperty(name, value);
+ }
+
+ public TokenizedHTMLPage getPage() {
+ return page;
+ }
+}
\ No newline at end of file
diff --git a/grails-gsp/grails-web-layout/src/main/resources/org/apache/grails/web/layout/grails-layout-default.xml b/grails-gsp/grails-web-layout/src/main/resources/org/apache/grails/web/layout/grails-layout-default.xml
new file mode 100644
index 0000000000..43a018986e
--- /dev/null
+++ b/grails-gsp/grails-web-layout/src/main/resources/org/apache/grails/web/layout/grails-layout-default.xml
@@ -0,0 +1,33 @@
+<!--
+ ~ 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.
+ -->
+
+<sitemesh>
+ <page-parsers>
+ <parser content-type="text/html"
+ class="org.apache.grails.web.layout.GrailsHTMLPageParser" />
+ <parser content-type="text/html;charset=ISO-8859-1"
+ class="org.apache.grails.web.layout.GrailsHTMLPageParser" />
+ <parser content-type="text/html;charset=UTF-8"
+ class="org.apache.grails.web.layout.GrailsHTMLPageParser" />
+ </page-parsers>
+
+ <decorator-mappers>
+ <mapper class="org.apache.grails.web.layout.GrailsLayoutDecoratorMapper" />
+ </decorator-mappers>
+</sitemesh>
\ No newline at end of file
diff --git a/grails-gsp/grails-web-layout/src/test/groovy/org/apache/grails/web/layout/GroovyPageLayoutFinderSpec.groovy b/grails-gsp/grails-web-layout/src/test/groovy/org/apache/grails/web/layout/GroovyPageLayoutFinderSpec.groovy
new file mode 100644
index 0000000000..cba9b30827
--- /dev/null
+++ b/grails-gsp/grails-web-layout/src/test/groovy/org/apache/grails/web/layout/GroovyPageLayoutFinderSpec.groovy
@@ -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.apache.grails.web.layout
+
+import org.grails.web.servlet.view.GrailsViewResolver
+import org.springframework.mock.web.MockHttpServletRequest
+import spock.lang.Issue
+import spock.lang.Specification
+import spock.lang.Unroll
+import jakarta.servlet.http.HttpServletRequest
+
+class GroovyPageLayoutFinderSpec extends Specification {
+
+ @Issue('https://github.com/apache/grails-core/issues/10371')
+ @Unroll('Layout name mapping: #layoutName => #expectedPath')
+ void 'testing layout name resolution'() {
+ given:
+ GroovyPageLayoutFinder layoutFinder = new GroovyPageLayoutFinder()
+ HttpServletRequest request = new MockHttpServletRequest()
+ layoutFinder.viewResolver = Mock(GrailsViewResolver)
+ when:
+ layoutFinder.getNamedDecorator(request, layoutName, false)
+
+ then:
+ 1 * layoutFinder.viewResolver.resolveViewName(expectedPath, Locale.ENGLISH)
+ notThrown(Exception)
+
+ where:
+ layoutName | expectedPath
+ 'foo' | '/layouts/foo'
+ 'foo/bar' | '/layouts/foo/bar'
+ '../foo' | '/foo'
+ '../foo/bar' | '/foo/bar'
+ '../../foo' | '/../foo'
+ '../foo/../../bar' | '/../bar'
+ 'foo/../../bar' | '/bar'
+ }
+}
diff --git a/grails-gsp/plugin/build.gradle b/grails-gsp/plugin/build.gradle
index 45bce77b51..c5ee7ea3d0 100644
--- a/grails-gsp/plugin/build.gradle
+++ b/grails-gsp/plugin/build.gradle
@@ -43,7 +43,7 @@ dependencies {
implementation platform(project(':grails-bom'))
astImplementation platform(project(':grails-bom'))
- api project(':grails-sitemesh3')
+ api project(':grails-layout')
api project(':grails-gsp-core'), { // GroovyPageResourceLoader, GroovyPagesTemplateEngine, CachingGroovyPageStaticResourceLocator
// API dependencies in grails-gsp
@@ -84,7 +84,8 @@ dependencies {
exclude group: 'org.apache.grails', module: 'grails-encoder'
exclude group: 'org.springframework', module: 'spring-core'
}
- implementation project(':grails-web-gsp-taglib'), { // RenderTagLib
+
+ api project(':grails-web-gsp-taglib'), { // RenderTagLib
// API dependencies in grails-web-gsp-taglib
exclude group: 'org.apache.grails.views', module: 'grails-taglib'
exclude group: 'org.apache.grails.views', module: 'grails-web-gsp'
diff --git a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/GrailsLayoutViewResolverPostProcessor.groovy b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/GrailsLayoutViewResolverPostProcessor.groovy
new file mode 100644
index 0000000000..38283f8e44
--- /dev/null
+++ b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/GrailsLayoutViewResolverPostProcessor.groovy
@@ -0,0 +1,82 @@
+/*
+ * 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.web
+
+
+import org.grails.web.servlet.view.GrailsLayoutViewResolver
+import org.springframework.beans.BeansException
+import org.springframework.beans.MutablePropertyValues
+import org.springframework.beans.factory.config.BeanDefinition
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory
+import org.springframework.beans.factory.config.RuntimeBeanReference
+import org.springframework.beans.factory.support.BeanDefinitionRegistry
+import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor
+import org.springframework.beans.factory.support.GenericBeanDefinition
+import org.springframework.core.Ordered
+
+/**
+ * This BeanDefinitionRegistryPostProcessor replaces the existing jspViewResolver bean with EmbeddedGrailsLayoutViewResolver
+ * and moves the previous jspViewResolver bean configuration as an inner bean of EmbeddedGrailsLayoutViewResolver to be used as
+ * the innerViewResolver of it.
+ *
+ * Scaffolding plugin replaces jspViewResolver with it's own implementation and this solution makes it easier to customize
+ * the inner view resolver.
+ *
+ *
+ * @author Lari Hotari
+ * @since 2.4.0
+ * @see org.grails.web.servlet.view.EmbeddedGrailsLayoutViewResolver*
+ */
+class GrailsLayoutViewResolverPostProcessor implements BeanDefinitionRegistryPostProcessor, Ordered {
+
+ private static final String GRAILS_VIEW_RESOLVER_BEAN_NAME = "jspViewResolver"
+ private static final String GROOVY_PAGE_LAYOUT_FINDER_BEAN_NAME = "groovyPageLayoutFinder"
+ int order = 0
+ Class<?> layoutViewResolverClass = GrailsLayoutViewResolver
+ String layoutViewResolverBeanParentName = null
+ boolean markBeanPrimary = true
+
+ boolean enabled = true
+
+ @Override
+ void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
+ }
+
+ @Override
+ void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
+ if (enabled && registry.containsBeanDefinition(GRAILS_VIEW_RESOLVER_BEAN_NAME)) {
+ BeanDefinition previousViewResolver = registry.getBeanDefinition(GRAILS_VIEW_RESOLVER_BEAN_NAME);
+ registry.removeBeanDefinition(GRAILS_VIEW_RESOLVER_BEAN_NAME);
+
+ GenericBeanDefinition beanDefinition = new GenericBeanDefinition()
+ beanDefinition.beanClass = layoutViewResolverClass
+ if (layoutViewResolverBeanParentName) {
+ beanDefinition.parentName = layoutViewResolverBeanParentName
+ }
+ if (markBeanPrimary) {
+ beanDefinition.primary = true
+ }
+ final MutablePropertyValues propertyValues = beanDefinition.getPropertyValues()
+ propertyValues.addPropertyValue('innerViewResolver', previousViewResolver)
+ propertyValues.addPropertyValue('groovyPageLayoutFinder', new RuntimeBeanReference((String) GROOVY_PAGE_LAYOUT_FINDER_BEAN_NAME, false))
+ registry.registerBeanDefinition(GRAILS_VIEW_RESOLVER_BEAN_NAME, beanDefinition)
+ }
+ }
+}
diff --git a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/GroovyPagesGrailsPlugin.groovy b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/GroovyPagesGrailsPlugin.groovy
index 0d18080d9c..589d222619 100644
--- a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/GroovyPagesGrailsPlugin.groovy
+++ b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/GroovyPagesGrailsPlugin.groovy
@@ -29,6 +29,8 @@ import grails.util.Metadata
import grails.web.pages.GroovyPagesUriService
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
+import org.apache.grails.web.layout.GrailsLayoutRenderViewMutator
+import org.apache.grails.web.layout.LayoutSelector
import org.grails.core.artefact.gsp.TagLibArtefactHandler
import org.grails.gsp.GroovyPageResourceLoader
import org.grails.gsp.GroovyPagesTemplateEngine
@@ -38,6 +40,7 @@ import org.grails.plugins.web.taglib.ApplicationTagLib
import org.grails.plugins.web.taglib.CountryTagLib
import org.grails.plugins.web.taglib.FormTagLib
import org.grails.plugins.web.taglib.FormatTagLib
+import org.grails.plugins.web.taglib.GrailsLayoutTagLib
import org.grails.plugins.web.taglib.JavascriptTagLib
import org.grails.plugins.web.taglib.PluginTagLib
import org.grails.plugins.web.taglib.RenderTagLib
@@ -53,6 +56,7 @@ import org.grails.web.pages.DefaultGroovyPagesUriService
import org.grails.web.pages.FilteringCodecsByContentTypeSettings
import org.grails.web.pages.GroovyPagesServlet
import org.grails.web.servlet.view.GroovyPageViewResolver
+import org.apache.grails.web.layout.GroovyPageLayoutFinder
import org.grails.web.util.GrailsApplicationAttributes
import org.springframework.beans.factory.config.PropertiesFactoryBean
import org.springframework.boot.web.servlet.ServletRegistrationBean
@@ -71,6 +75,9 @@ class GroovyPagesGrailsPlugin extends Plugin {
public static final String GSP_RELOAD_INTERVAL = 'grails.gsp.reload.interval'
public static final String GSP_VIEWS_DIR = 'grails.gsp.view.dir'
+ public static final String GSP_VIEW_LAYOUT_RESOLVER_ENABLED = 'grails.gsp.view.layoutViewResolver'
+ public static final String DEFAULT_LAYOUT = 'grails.views.layout.default'
+ public static final String GRAILS_LAYOUT_ENABLE_NONGSP = 'grails.views.layout.enable.nongsp'
def watchedResources = ['file:./plugins/*/grails-app/taglib/**/*TagLib.groovy',
'file:./grails-app/taglib/**/*TagLib.groovy']
@@ -89,7 +96,8 @@ class GroovyPagesGrailsPlugin extends Plugin {
RenderTagLib,
UrlMappingTagLib,
ValidationTagLib,
- PluginTagLib
+ PluginTagLib,
+ GrailsLayoutTagLib
]
@@ -122,6 +130,10 @@ class GroovyPagesGrailsPlugin extends Plugin {
long gspCacheTimeout = config.getProperty(GSP_RELOAD_INTERVAL, Long, (developmentMode && env == Environment.DEVELOPMENT) ? 0L : 5000L)
boolean enableCacheResources = !config.getProperty(GroovyPagesTemplateEngine.CONFIG_PROPERTY_DISABLE_CACHING_RESOURCES, Boolean, false)
String viewsDir = config.getProperty(GSP_VIEWS_DIR, '')
+ boolean enableLayoutViewResolver = config.getProperty(GSP_VIEW_LAYOUT_RESOLVER_ENABLED, Boolean, true)
+ String defaultDecoratorNameSetting = config.getProperty(DEFAULT_LAYOUT, '')
+ def grailsLayoutEnableNonGspViews = config.getProperty(GRAILS_LAYOUT_ENABLE_NONGSP, Boolean, false)
+
RuntimeSpringConfiguration spring = springConfig
@@ -253,6 +265,20 @@ class GroovyPagesGrailsPlugin extends Plugin {
bean.parent = 'abstractViewResolver'
}
+ // "grails.gsp.view.layoutViewResolver=false" can be used to disable EmbeddedGrailsLayoutViewResolver
+ // containsKey check must be made to check existence of boolean false values in ConfigObject
+
+ if (enableLayoutViewResolver) {
+ groovyPageLayoutFinder(GroovyPageLayoutFinder) {
+ gspReloadEnabled = enableReload
+ defaultDecoratorName = defaultDecoratorNameSetting ?: null
+ enableNonGspViews = grailsLayoutEnableNonGspViews
+ }
+ grailsRenderViewMutator(GrailsLayoutRenderViewMutator)
+ grailsLayoutSelector(LayoutSelector)
+ grailsLayoutViewResolverPostProcessor(GrailsLayoutViewResolverPostProcessor)
+ }
+
// Now go through tag libraries and configure them in Spring too. With AOP proxies and so on
for (taglib in application.tagLibClasses) {
diff --git a/grails-gsp/plugin/src/test/groovy/org/apache/grails/views/gsp/layout/FactoryHolderTests.groovy b/grails-gsp/plugin/src/test/groovy/org/apache/grails/views/gsp/layout/FactoryHolderTests.groovy
new file mode 100644
index 0000000000..8cd1d6361e
--- /dev/null
+++ b/grails-gsp/plugin/src/test/groovy/org/apache/grails/views/gsp/layout/FactoryHolderTests.groovy
@@ -0,0 +1,71 @@
+/*
+ * 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.apache.grails.views.gsp.layout
+
+import com.opensymphony.module.sitemesh.DecoratorMapper
+import com.opensymphony.module.sitemesh.Factory
+import com.opensymphony.module.sitemesh.PageParser
+import org.apache.grails.web.layout.FactoryHolder
+import org.junit.jupiter.api.Test
+
+import static org.junit.jupiter.api.Assertions.*
+
+class FactoryHolderTests {
+
+ @Test
+ void testGetFactoryReturnsFactoryForNonNullFactory() {
+ def factory = new DummyFactory()
+ FactoryHolder.setFactory(factory)
+ assertSame(factory, FactoryHolder.getFactory())
+ }
+
+ @Test
+ void testGetFactoryForNullFactory() {
+ FactoryHolder.setFactory(null)
+ assertNull(FactoryHolder.getFactory())
+ }
+
+ @Test
+ void testSetFactory() {
+ def factory = new DummyFactory()
+ FactoryHolder.setFactory(factory)
+ assertSame(factory, FactoryHolder.factory)
+ }
+
+ @Test
+ // Silly test, but a necessary evil in order to get Cobertura to give us 100% coverage
+ void testPrivateConstructor() {
+ assertNotNull(new FactoryHolder())
+ }
+}
+
+/** A bare minimum implementation needed to test the factory above. */
+class DummyFactory extends Factory {
+
+ boolean isPathExcluded(String path) { false }
+
+ boolean shouldParsePage(String contentType) { false }
+
+ DecoratorMapper getDecoratorMapper() { null }
+
+ void refresh() {}
+
+ PageParser getPageParser(String contentType) { null }
+}
+
diff --git a/grails-gsp/plugin/src/test/groovy/org/apache/grails/views/gsp/layout/FullGrailsLayoutLifeCycleTests.groovy b/grails-gsp/plugin/src/test/groovy/org/apache/grails/views/gsp/layout/FullGrailsLayoutLifeCycleTests.groovy
new file mode 100644
index 0000000000..635bd165a7
--- /dev/null
+++ b/grails-gsp/plugin/src/test/groovy/org/apache/grails/views/gsp/layout/FullGrailsLayoutLifeCycleTests.groovy
@@ -0,0 +1,270 @@
+/*
+ * 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.apache.grails.views.gsp.layout
+
+import com.opensymphony.module.sitemesh.Config
+import com.opensymphony.module.sitemesh.Decorator
+import com.opensymphony.module.sitemesh.DecoratorMapper
+import com.opensymphony.module.sitemesh.PageParser
+import com.opensymphony.module.sitemesh.RequestConstants
+import com.opensymphony.module.sitemesh.factory.BaseFactory
+import org.apache.grails.web.layout.FactoryHolder
+import org.apache.grails.web.layout.GSPGrailsLayoutPage
+import org.apache.grails.web.layout.GrailsHTMLPageParser
+import org.apache.grails.web.layout.EmbeddedGrailsLayoutView
+import org.grails.core.io.MockStringResourceLoader
+import org.grails.web.taglib.AbstractGrailsTagTests
+import org.junit.jupiter.api.Test
+import org.springframework.mock.web.MockServletConfig
+
+import static org.junit.jupiter.api.Assertions.assertEquals
+
+/**
+ * Tests the grails layout capturing and rendering tags end-to-end
+ *
+ * @author Graeme Rocher
+ * @since 1.2
+ */
+class FullGrailsLayoutLifeCycleTests extends AbstractGrailsTagTests {
+
+ @Test
+ void testSimpleLayout() {
+ def template = '''
+<html>
+ <head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><title>This is the title</title></head>
+ <body onload="test();">body text</body>
+</html>
+'''
+
+ assertOutputEquals('''
+<html>
+ <head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><title>This is the title</title></head>
+ <body onload="test();">body text</body>
+</html>
+''', template)
+
+ def layout = '''
+<html>
+ <head><title>Decorated <g:layoutTitle /></title><g:layoutHead /></head>
+ <body><h1>Hello</h1><g:layoutBody /></body>
+</html>
+'''
+ def result = applyLayout(layout, template)
+
+ assertEquals '''
+<html>
+ <head><title>Decorated This is the title</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head>
+ <body><h1>Hello</h1>body text</body>
+</html>
+''', result
+ }
+
+ @Test
+ void testTitleInSubTemplate() {
+ def resourceLoader = new MockStringResourceLoader()
+ resourceLoader.registerMockResource('/_title.gsp', '<title>This is the title</title>')
+ appCtx.groovyPageLocator.addResourceLoader(resourceLoader)
+
+ def template = '''
+<html>
+ <head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><g:render template="/title"/></head>
+ <body onload="test();">body text</body>
+</html>
+'''
+
+ assertOutputEquals('''
+<html>
+ <head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><title>This is the title</title></head>
+ <body onload="test();">body text</body>
+</html>
+''', template)
+
+ def layout = '''
+<html>
+ <head><title>Decorated <g:layoutTitle /></title><g:layoutHead /></head>
+ <body><h1>Hello</h1><g:layoutBody /></body>
+</html>
+'''
+ def result = applyLayout(layout, template)
+
+ assertEquals('''
+<html>
+ <head><title>Decorated This is the title</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head>
+ <body><h1>Hello</h1>body text</body>
+</html>
+''', result)
+ }
+
+ static class DummyGrailsLayoutFactory extends BaseFactory {
+
+ DummyGrailsLayoutFactory(Config config) {
+ super(config)
+ }
+
+ @Override
+ void refresh() {
+ }
+
+ @Override
+ public PageParser getPageParser(String contentType) {
+ new GrailsHTMLPageParser()
+ }
+ }
+
+ def configureGrailsLayout() {
+ def mockServletConfig = new MockServletConfig()
+ def grailsLayoutConfig = new Config(mockServletConfig)
+ def grailsLayoutFactory = new DummyGrailsLayoutFactory(grailsLayoutConfig)
+ def decorator = { name -> [getPage: { -> "/layout/${name}.gsp".toString() }] as Decorator }
+ grailsLayoutFactory.decoratorMapper = [getNamedDecorator: { request, name -> decorator(name) }] as DecoratorMapper
+ FactoryHolder.factory = grailsLayoutFactory
+ }
+
+ @Test
+ void testMultipleLevelsOfLayouts() {
+ def resourceLoader = new MockStringResourceLoader()
+ resourceLoader.registerMockResource('/layout/dialog.gsp', '''<html>
+ <head><g:layoutHead /><title>Dialog - <g:layoutTitle /></title></head>
+ <body onload="${g.pageProperty(name:'body.onload')}"><div id="dialog"><g:layoutBody /></div></body>
+</html>''')
+ resourceLoader.registerMockResource('/layout/base.gsp', '''<html>
+ <head><g:layoutHead /><title>Base - <g:layoutTitle /></title></head>
+ <body onload="${g.pageProperty(name:'body.onload')}"><div id="base"><g:layoutBody /></div></body>
+</html>''')
+ appCtx.groovyPageLocator.addResourceLoader resourceLoader
+
+ configureGrailsLayout()
+
+ def template = '''
+<g:applyLayout name="base"><g:applyLayout name="dialog">
+<html>
+ <head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><title>This is the title</title></head>
+ <body onload="test();">body text</body>
+</html>
+</g:applyLayout></g:applyLayout>
+'''
+ request.setAttribute(RequestConstants.PAGE, new GSPGrailsLayoutPage())
+ request.setAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE, new GSPGrailsLayoutPage())
+ assertOutputEquals '''
+<html>
+ <head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><title>Base - Dialog - This is the title</title></head>
+ <body onload="test();"><div id="base"><div id="dialog">body text</div></div></body>
+</html>
+''', template
+
+ def layout = '''
+<html>
+ <head><title>Decorated <g:layoutTitle default="defaultTitle"/></title><g:layoutHead /></head>
+ <body><h1>Hello</h1><g:layoutBody /></body>
+</html>
+'''
+ def result = applyLayout(layout, template)
+
+ assertEquals '''
+<html>
+ <head><title>Decorated Base - Dialog - This is the title</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head>
+ <body><h1>Hello</h1><div id="base"><div id="dialog">body text</div></div></body>
+</html>
+''', result
+ }
+
+ @Test
+ void testParameters() {
+ def template = '''
+<html>
+ <head>
+ <title>Simple GSP page</title>
+ <meta name="layout" content="main"/>
+ <parameter name="navigation" value="here!"/>
+ </head>
+ <body>Place your content here</body>
+</html>
+'''
+
+ def layout = '<h1>pageProperty: ${pageProperty(name: \'page.navigation\')}</h1>'
+
+ def result = applyLayout(layout, template)
+
+ assertEquals '<h1>pageProperty: here!</h1>', result
+ }
+
+ @Test
+ void testParametersWithLogic() {
+ def template = '''
+<html>
+ <head>
+ <title>Simple GSP page</title>
+ <meta name="layout" content="main"/>
+ <parameter name="sideBarSetting" value="vendor"/>
+ </head>
+ <body>Place your content here</body>
+</html>
+'''
+ def layout = '''<g:if test="${pageProperty(name:'page.sideBarSetting') == 'vendor'}">good</g:if>'''
+
+ def result = applyLayout(layout, template, [:])
+
+ assertEquals 'good', result
+ }
+
+ // GRAILS-11484
+ @Test
+ void testMultilineTitle() {
+ def template = '''
+<html>
+ <head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<title>
+This is the title
+</title></head>
+ <body onload="test();">body text</body>
+</html>
+'''
+
+ assertOutputEquals '''
+<html>
+ <head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<title>
+This is the title
+</title></head>
+ <body onload="test();">body text</body>
+</html>
+''', template
+
+ def layout = '''
+<html>
+ <head><title>Decorated <g:layoutTitle /></title><g:layoutHead /></head>
+ <body><h1>Hello</h1><g:layoutBody /></body>
+</html>
+'''
+ def result = applyLayout(layout, template)
+
+ assertEquals '''
+<html>
+ <head><title>Decorated
+This is the title
+</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+</head>
+ <body><h1>Hello</h1>body text</body>
+</html>
+''', result
+ }
+
+}
+
diff --git a/grails-gsp/plugin/src/test/groovy/org/apache/grails/views/gsp/layout/FullGrailsLayoutLifeCycleWithNoPreprocessingTests.groovy b/grails-gsp/plugin/src/test/groovy/org/apache/grails/views/gsp/layout/FullGrailsLayoutLifeCycleWithNoPreprocessingTests.groovy
new file mode 100644
index 0000000000..99b18e6da1
--- /dev/null
+++ b/grails-gsp/plugin/src/test/groovy/org/apache/grails/views/gsp/layout/FullGrailsLayoutLifeCycleWithNoPreprocessingTests.groovy
@@ -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.apache.grails.views.gsp.layout
+
+import com.opensymphony.module.sitemesh.RequestConstants
+import org.apache.grails.web.layout.GSPGrailsLayoutPage
+import org.apache.grails.web.layout.EmbeddedGrailsLayoutView
+import org.junit.jupiter.api.BeforeEach
+
+class FullGrailsLayoutLifeCycleWithNoPreprocessingTests extends FullGrailsLayoutLifeCycleTests {
+
+ @BeforeEach
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp()
+ def config = new ConfigSlurper().parse('''
+grails.views.gsp.layout.preprocess=false
+''')
+ buildMockRequest(config)
+ def page = new GSPGrailsLayoutPage()
+ request.setAttribute(RequestConstants.PAGE, page)
+ request.setAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE, page)
+ }
+
+ @Override
+ void testMultipleLevelsOfLayouts() {
+ // no-op
+ }
+}
+
diff --git a/grails-gsp/plugin/src/test/groovy/org/apache/grails/views/gsp/layout/GSPGrailsLayoutPageTests.groovy b/grails-gsp/plugin/src/test/groovy/org/apache/grails/views/gsp/layout/GSPGrailsLayoutPageTests.groovy
new file mode 100644
index 0000000000..2698aa0291
--- /dev/null
+++ b/grails-gsp/plugin/src/test/groovy/org/apache/grails/views/gsp/layout/GSPGrailsLayoutPageTests.groovy
@@ -0,0 +1,164 @@
+/*
+ * 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.apache.grails.views.gsp.layout
+
+import com.opensymphony.module.sitemesh.RequestConstants
+import org.apache.grails.web.layout.GSPGrailsLayoutPage
+import org.apache.grails.web.layout.EmbeddedGrailsLayoutView
+import org.grails.buffer.FastStringWriter
+import org.grails.web.taglib.AbstractGrailsTagTests
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.Test
+import org.springframework.web.context.request.RequestContextHolder
+
+import static org.junit.jupiter.api.Assertions.assertEquals
+
+class GSPGrailsLayoutPageTests extends AbstractGrailsTagTests {
+
+ @Test
+ void testCaptureContent() {
+ def template = '<grailsLayout:captureContent tag=\"testtag\">this is the captured content</grailsLayout:captureContent>'
+ def gspGrailsLayoutPage = new GSPGrailsLayoutPage()
+ webRequest.currentRequest.setAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE, gspGrailsLayoutPage)
+ def result = applyTemplate(template, [:])
+ println("RESULT IS $result")
+ assertEquals('this is the captured content', gspGrailsLayoutPage.getContentBuffer('page.testtag').toString())
+ }
+
+ @Test
+ void testCaptureContent2() {
+ def template = '<grailsLayout:captureContent tag=\"testtag\">this is the <g:if test="${true}">captured</g:if> content</grailsLayout:captureContent>'
+ def gspGrailsLayoutPage = new GSPGrailsLayoutPage()
+ webRequest.currentRequest.setAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE, gspGrailsLayoutPage)
+ def result = applyTemplate(template, [:])
+ assertEquals('this is the captured content', gspGrailsLayoutPage.getContentBuffer('page.testtag').toString())
+ }
+
+ @Test
+ void testCaptureContent3() {
+ def template = '<content tag=\"testtag\">this is the <g:if test="${true}">captured</g:if> content</content>'
+ def gspGrailsLayoutPage = new GSPGrailsLayoutPage()
+ webRequest.currentRequest.setAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE, gspGrailsLayoutPage)
+ def result = applyTemplate(template, [:])
+ assertEquals('this is the captured content', gspGrailsLayoutPage.getContentBuffer('page.testtag').toString())
+ }
+
+ @Test
+ void testCaptureTitleAndBody() {
+ def template = '<html><head><title>This is the title</title></head><body onload="somejs();">body here</body></html>'
+ def gspGrailsLayoutPage = new GSPGrailsLayoutPage()
+ webRequest.currentRequest.setAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE, gspGrailsLayoutPage)
+ def result = applyTemplate(template, [:])
+ assertEquals('This is the title', gspGrailsLayoutPage.getProperty('title'))
+ FastStringWriter writer = new FastStringWriter()
+ gspGrailsLayoutPage.writeBody(writer)
+ assertEquals('body here', writer.toString())
+ assertEquals('somejs();', gspGrailsLayoutPage.getProperty('body.onload'))
+ }
+
+ @Test
+ void testMetaObjectValues() {
+ // GRAILS-5603 test case
+ def template = '<html><head><meta name="intval" content="${123}"/><meta name="dateval" content="${new Date(0)}"/><title>This is the title</title></head><body onload="somejs();">body here</body></html>'
+ def gspGrailsLayoutPage = new GSPGrailsLayoutPage()
+ webRequest.currentRequest.setAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE, gspGrailsLayoutPage)
+ def result = applyTemplate(template, [:])
+ assertEquals('123', gspGrailsLayoutPage.getProperty('meta.intval'))
+ assertEquals(new Date(0).toString(), gspGrailsLayoutPage.getProperty('meta.dateval'))
+ }
+
+ @Test
+ void testLayoutTags() {
+ def template = '<html><head><title>This is the title</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head><body onload="somejs();">body here</body></html>'
+ def gspGrailsLayoutPage = new GSPGrailsLayoutPage()
+ webRequest.currentRequest.setAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE, gspGrailsLayoutPage)
+ def result = applyTemplate(template, [:])
+ assertEquals 'This is the title', gspGrailsLayoutPage.getProperty('title')
+
+ def gspGrailsLayoutPage2 = new GSPGrailsLayoutPage()
+ webRequest.currentRequest.setAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE, gspGrailsLayoutPage2)
+ webRequest.currentRequest.setAttribute(RequestConstants.PAGE, gspGrailsLayoutPage)
+ def template2 = '<html><head><title><g:layoutTitle/></title><g:layoutHead/></head><body onload=\"${pageProperty(name:\'body.onload\')}\"><g:layoutBody/></body></html>'
+ def result2 = applyTemplate(template2, [:])
+
+ assertEquals '<html><head><title>This is the title</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head><body onload="somejs();">body here</body></html>', result2
+ }
+
+ @Test
+ void testLayoutTagsBodyIsWholePage() {
+ def template = 'body here'
+ def gspGrailsLayoutPage = new GSPGrailsLayoutPage()
+ webRequest.currentRequest.setAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE, gspGrailsLayoutPage)
+ def target1 = new FastStringWriter()
+ gspGrailsLayoutPage.setPageBuffer(target1.buffer)
+ def result = applyTemplate(template, [:], target1)
+
+ def gspGrailsLayoutPage2 = new GSPGrailsLayoutPage()
+ webRequest.currentRequest.setAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE, gspGrailsLayoutPage2)
+ webRequest.currentRequest.setAttribute(RequestConstants.PAGE, gspGrailsLayoutPage)
+ def target2 = new FastStringWriter()
+ gspGrailsLayoutPage2.setPageBuffer(target2.buffer)
+ def template2 = '<body><g:layoutBody/></body>'
+ def result2 = applyTemplate(template2, [:], target2)
+
+ assertEquals('<body>body here</body>', result2)
+ }
+
+ @Test
+ void testLayoutcontent() {
+ def template = '<html><head><title>This is the title</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/></head><body onload="somejs();">body here</body><content tag="nav">Navigation content</content></html>'
+ def gspGrailsLayoutPage = new GSPGrailsLayoutPage()
+ webRequest.currentRequest.setAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE, gspGrailsLayoutPage)
+ def result = applyTemplate(template, [:])
+ assertEquals('This is the title', gspGrailsLayoutPage.getProperty('title'))
+
+ def gspGrailsLayoutPage2 = new GSPGrailsLayoutPage()
+ webRequest.currentRequest.setAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE, gspGrailsLayoutPage2)
+ webRequest.currentRequest.setAttribute(RequestConstants.PAGE, gspGrailsLayoutPage)
+ def template2 = '<html><head><title><g:layoutTitle/></title><g:layoutHead/></head><body onload=\"${pageProperty(name:\'body.onload\')}\"><g:layoutBody/> <g:pageProperty name="page.nav"/></body></html>'
+ def result2 = applyTemplate(template2, [:])
+
+ assertEquals('<html><head><title>This is the title</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/></head><body onload="somejs();">body here Navigation content</body></html>', result2)
+ }
+
+ @Test
+ void testEmptyTitle() {
+ // GRAILS-7510 , GRAILS-7736
+ def template = '<html><head><title></title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/></head><body onload="somejs();">body here</body><content tag="nav">Navigation content</content></html>'
+ def gspGrailsLayoutPage = new GSPGrailsLayoutPage()
+ webRequest.currentRequest.setAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE, gspGrailsLayoutPage)
+ def result = applyTemplate(template, [:])
+ assertEquals('', gspGrailsLayoutPage.getProperty('title'))
+
+ def gspGrailsLayoutPage2 = new GSPGrailsLayoutPage()
+ webRequest.currentRequest.setAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE, gspGrailsLayoutPage2)
+ webRequest.currentRequest.setAttribute(RequestConstants.PAGE, gspGrailsLayoutPage)
+ def template2 = '<html><head><title><g:layoutTitle/></title><g:layoutHead/></head><body onload=\"${pageProperty(name:\'body.onload\')}\"><g:layoutBody/> <g:pageProperty name="page.nav"/></body></html>'
+ def result2 = applyTemplate(template2, [:])
+
+ assertEquals('<html><head><title></title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/></head><body onload="somejs();">body here Navigation content</body></html>', result2)
+ }
+
+ @AfterEach
+ void tearDown() {
+ RequestContextHolder.resetRequestAttributes()
+ }
+}
+
diff --git a/grails-gsp/plugin/src/test/groovy/org/grails/web/pages/GrailsLayoutPreprocessorTests.groovy b/grails-gsp/plugin/src/test/groovy/org/grails/web/pages/GrailsLayoutPreprocessorTests.groovy
new file mode 100644
index 0000000000..135fe28a04
--- /dev/null
+++ b/grails-gsp/plugin/src/test/groovy/org/grails/web/pages/GrailsLayoutPreprocessorTests.groovy
@@ -0,0 +1,165 @@
+/*
+ * 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.web.pages
+
+import org.grails.gsp.compiler.GrailsLayoutPreprocessor
+import org.junit.jupiter.api.Test
+
+import static org.junit.jupiter.api.Assertions.assertEquals
+
+class GrailsLayoutPreprocessorTests {
+
+ @Test
+ void testSimpleParse() {
+ def gspBody = '''
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <title>This is the title</title></head>
+ <body onload="test();">
+ body text
+ </body>
+</html>
+'''
+ def preprocessor = new GrailsLayoutPreprocessor()
+ def gspBodyExpected = '''
+<html>
+ <grailsLayout:captureHead>
+ <grailsLayout:captureMeta gsp_sm_xmlClosingForEmptyTag="" http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <grailsLayout:wrapTitleTag><grailsLayout:captureTitle>This is the title</grailsLayout:captureTitle></grailsLayout:wrapTitleTag></grailsLayout:captureHead>
+ <grailsLayout:captureBody onload="test();">
+ body text
+ </grailsLayout:captureBody>
+</html>
+'''
+ assertEquals(gspBodyExpected, preprocessor.addGspGrailsLayoutCapturing(gspBody))
+ }
+
+ @Test
+ void testContentParse() {
+ def gspBody = '''
+<html>
+ <head><title>This is the title</title></head>
+ <body onload="test();">
+ body text
+ </body>
+ <content tag="nav">
+ content test
+ </content>
+</html>
+'''
+ def preprocessor = new GrailsLayoutPreprocessor()
+ def gspBodyExpected = '''
+<html>
+ <grailsLayout:captureHead><grailsLayout:wrapTitleTag><grailsLayout:captureTitle>This is the title</grailsLayout:captureTitle></grailsLayout:wrapTitleTag></grailsLayout:captureHead>
+ <grailsLayout:captureBody onload="test();">
+ body text
+ </grailsLayout:captureBody>
+ <grailsLayout:captureContent tag="nav">
+ content test
+ </grailsLayout:captureContent>
+</html>
+'''
+ assertEquals(gspBodyExpected, preprocessor.addGspGrailsLayoutCapturing(gspBody))
+ }
+
+ @Test
+ void testContentParse2() {
+ def gspBody = '''
+<html>
+ <head><title>This is the title</title></head>
+ <body onload="test();">
+ body text
+ </body>
+ <content tag="nav">
+ content test
+ </content>
+ <content tag="nav">
+ content test
+ </content>
+</html>
+'''
+ def preprocessor = new GrailsLayoutPreprocessor()
+ def gspBodyExpected = '''
+<html>
+ <grailsLayout:captureHead><grailsLayout:wrapTitleTag><grailsLayout:captureTitle>This is the title</grailsLayout:captureTitle></grailsLayout:wrapTitleTag></grailsLayout:captureHead>
+ <grailsLayout:captureBody onload="test();">
+ body text
+ </grailsLayout:captureBody>
+ <grailsLayout:captureContent tag="nav">
+ content test
+ </grailsLayout:captureContent>
+ <grailsLayout:captureContent tag="nav">
+ content test
+ </grailsLayout:captureContent>
+</html>
+'''
+ assertEquals(gspBodyExpected, preprocessor.addGspGrailsLayoutCapturing(gspBody))
+ }
+
+ @Test
+ void testGrailsLayoutParameterParse() {
+ def gspBody = '''
+<html>
+ <head><title>This is the title</title>
+ <parameter name="foo" value="bar" />
+ </head>
+ <body>
+ body text
+ </body>
+</html>
+'''
+ def preprocessor = new GrailsLayoutPreprocessor()
+ def gspBodyExpected = '''
+<html>
+ <grailsLayout:captureHead><grailsLayout:wrapTitleTag><grailsLayout:captureTitle>This is the title</grailsLayout:captureTitle></grailsLayout:wrapTitleTag>
+ <grailsLayout:parameter name="foo" value="bar" />
+ </grailsLayout:captureHead>
+ <grailsLayout:captureBody>
+ body text
+ </grailsLayout:captureBody>
+</html>
+'''
+ assertEquals(gspBodyExpected, preprocessor.addGspGrailsLayoutCapturing(gspBody))
+ }
+
+ @Test
+ void testOtherParse() {
+ def gspBody = '''
+<html>
+ <head ><titlenot>This is not the title</titlenot></head>
+ <body>
+ body text
+ </body>
+</html>
+'''
+ def preprocessor = new GrailsLayoutPreprocessor()
+ def gspBodyExpected = '''
+<html>
+ <grailsLayout:captureHead ><titlenot>This is not the title</titlenot></grailsLayout:captureHead>
+ <grailsLayout:captureBody>
+ body text
+ </grailsLayout:captureBody>
+</html>
+'''
+ assertEquals(gspBodyExpected, preprocessor.addGspGrailsLayoutCapturing(gspBody))
+ }
+}
+
diff --git a/grails-gsp/plugin/src/test/groovy/org/grails/web/pages/ParseSpec.groovy b/grails-gsp/plugin/src/test/groovy/org/grails/web/pages/ParseSpec.groovy
index 3e5e73a55f..0dd03a6593 100644
--- a/grails-gsp/plugin/src/test/groovy/org/grails/web/pages/ParseSpec.groovy
+++ b/grails-gsp/plugin/src/test/groovy/org/grails/web/pages/ParseSpec.groovy
@@ -36,7 +36,6 @@ import org.springframework.mock.web.MockServletContext
import org.springframework.web.context.WebApplicationContext
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.context.support.GenericWebApplicationContext
-import spock.lang.PendingFeature
import spock.lang.Specification
/**
@@ -210,7 +209,6 @@ public static final String TAGLIB_CODEC = 'none'
'\n\n\nThanks' == output.htmlParts[1]
}
- @PendingFeature
void 'body with gstring attribute'() {
given:
String expected = makeImports() +
@@ -252,7 +250,6 @@ public static final String TAGLIB_CODEC = 'none'
'\n<body>text</body>' == result.htmlParts[0]
}
- @PendingFeature
void 'meta with gstring attribute'() {
given:
String expected = makeImports() +
diff --git a/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/AbstractGrailsTagTests.groovy b/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/AbstractGrailsTagTests.groovy
index 859f091fdc..8e69f510e8 100644
--- a/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/AbstractGrailsTagTests.groovy
+++ b/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/AbstractGrailsTagTests.groovy
@@ -19,6 +19,7 @@
package org.grails.web.taglib
+import com.opensymphony.module.sitemesh.RequestConstants
import grails.build.support.MetaClassRegistryCleaner
import grails.core.DefaultGrailsApplication
import grails.core.GrailsApplication
@@ -40,6 +41,7 @@ import org.grails.gsp.GroovyPage
import org.grails.gsp.GroovyPageMetaInfo
import org.grails.gsp.GroovyPageTemplate
import org.grails.gsp.GroovyPagesTemplateEngine
+import org.grails.gsp.compiler.GrailsLayoutPreprocessor
import org.grails.plugins.DefaultGrailsPlugin
import org.grails.plugins.MockGrailsPluginManager
import org.grails.taglib.GroovyPageAttributes
@@ -53,6 +55,9 @@ import org.grails.web.pages.DefaultGroovyPagesUriService
import org.grails.web.pages.GSPResponseWriter
import org.grails.web.servlet.context.support.WebRuntimeSpringConfiguration
import org.grails.web.servlet.mvc.GrailsWebRequest
+import org.apache.grails.web.layout.GSPGrailsLayoutPage
+import org.apache.grails.web.layout.GrailsHTMLPageParser
+import org.apache.grails.web.layout.EmbeddedGrailsLayoutView
import org.grails.web.util.GrailsApplicationAttributes
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
@@ -307,6 +312,7 @@ abstract class AbstractGrailsTagTests {
dependantPluginClasses << gcl.loadClass('org.grails.plugins.i18n.I18nGrailsPlugin')
dependantPluginClasses << gcl.loadClass('org.grails.plugins.web.mapping.UrlMappingsGrailsPlugin')
dependantPluginClasses << gcl.loadClass('org.grails.plugins.web.controllers.ControllersGrailsPlugin')
+ dependantPluginClasses << gcl.loadClass('org.grails.plugins.web.GrailsLayoutGrailsPlugin')
dependantPluginClasses << gcl.loadClass('org.grails.plugins.web.GroovyPagesGrailsPlugin')
def dependentPlugins = dependantPluginClasses.collect { new DefaultGrailsPlugin(it as Class<?>, grailsApplication) }
@@ -509,6 +515,37 @@ abstract class AbstractGrailsTagTests {
return target.toString()
}
+ /**
+ * Applies grails layout preprocessing to a template
+ */
+ String grailsLayoutPreprocess(String template) {
+ def preprocessor = new GrailsLayoutPreprocessor()
+ preprocessor.addGspGrailsLayoutCapturing(template)
+ }
+
+ String applyLayout(String layout, String template, Map params = [:]) {
+ def gspGrailsLayoutPage = new GSPGrailsLayoutPage()
+ request.setAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE, gspGrailsLayoutPage)
+ def content = applyTemplate(template, params)
+ request.removeAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE)
+
+ def page = null
+ if (!params.parse && gspGrailsLayoutPage != null && gspGrailsLayoutPage.used) {
+ page = gspGrailsLayoutPage
+ } else {
+ def parser = new GrailsHTMLPageParser()
+ page = parser.parse(content.toCharArray())
+ }
+ try {
+ request.setAttribute(RequestConstants.PAGE, page)
+ request.setAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE, new GSPGrailsLayoutPage())
+ return applyTemplate(layout, params, null, '/layouts/test_' + System.currentTimeMillis())
+ }
+ finally {
+ request.removeAttribute(RequestConstants.PAGE)
+ request.removeAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE)
+ }
+ }
/**
* Parses the given XML text and creates a DOM document from it.
*/
diff --git a/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/RenderTagLibTests.groovy b/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/RenderTagLibTests.groovy
index 71a248f285..cf9c042915 100644
--- a/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/RenderTagLibTests.groovy
+++ b/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/RenderTagLibTests.groovy
@@ -18,13 +18,21 @@
*/
package org.grails.web.taglib
+import com.opensymphony.module.sitemesh.RequestConstants
+import com.opensymphony.module.sitemesh.html.util.CharArray
+import com.opensymphony.module.sitemesh.parser.HTMLPageParser
+import com.opensymphony.module.sitemesh.parser.TokenizedHTMLPage
import grails.artefact.Artefact
import grails.testing.web.UrlMappingsUnitTest
import grails.util.GrailsUtil
+import org.grails.buffer.FastStringWriter
import org.grails.core.io.MockStringResourceLoader
import org.grails.gsp.GroovyPageBinding
import org.grails.plugins.web.taglib.RenderTagLib
import org.grails.taglib.GrailsTagException
+import org.apache.grails.web.layout.FactoryHolder
+import org.apache.grails.web.layout.GSPGrailsLayoutPage
+import org.apache.grails.web.layout.GrailsLayoutDecoratorMapper
import org.grails.web.util.GrailsApplicationAttributes
import org.springframework.web.servlet.support.RequestContextUtils as RCU
import spock.lang.Specification
@@ -247,6 +255,44 @@ class RenderTagLibTests extends Specification implements UrlMappingsUnitTest<Ren
assertOutputEquals('<th id="1" class="sortable" ><a href="/custompathCustomNamespace/mockcontroller/list?sort=id&order=asc">ID</a></th>', template)
}
+ void testPageProperty() {
+
+ def template = '<g:pageProperty name="foo.bar" />'
+
+ def head = ""
+ def page = new TokenizedHTMLPage([] as char[], new CharArray(0), new CharArray(0))
+ request[RequestConstants.PAGE] = page
+
+ page.addProperty('foo.bar', 'good')
+
+ assertOutputEquals('good', template)
+
+ template = '<g:pageProperty name="foo.bar" writeEntireProperty="true" />'
+ assertOutputEquals(' bar="good"', template)
+ }
+
+ void testIfPageProperty() {
+ def template = '<g:ifPageProperty name="foo.bar">Hello</g:ifPageProperty>'
+
+ def page = new TokenizedHTMLPage([] as char[], new CharArray(0), new CharArray(0))
+ request[RequestConstants.PAGE] = page
+
+ page.addProperty('foo.bar', 'true')
+
+ assertOutputEquals('Hello', template)
+
+ template = '<g:ifPageProperty name="page.contentbuffer">Hello 2</g:ifPageProperty>'
+
+ def smpage = new GSPGrailsLayoutPage()
+ request[RequestConstants.PAGE] = smpage
+
+ def sw = new FastStringWriter()
+ sw.write('true')
+ smpage.setContentBuffer('contentbuffer', sw.getBuffer())
+
+ assertOutputEquals('Hello 2', template)
+ }
+
void testTemplateNamespace() {
def resourceLoader = new MockStringResourceLoader()
resourceLoader.registerMockResource('/table/_tableRow.gsp', '<tr><td class="prop">${label}</td><td class="value">${value}</td></tr>')
@@ -693,10 +739,41 @@ class RenderTagLibTests extends Specification implements UrlMappingsUnitTest<Ren
assertEquals 'my/contenttype', response.getContentType()
}
+ void testApplyLayout() {
+ def decoratorMapper = new GrailsLayoutDecoratorMapper()
+ decoratorMapper.groovyPageLayoutFinder = appCtx.groovyPageLayoutFinder
+ FactoryHolder.setFactory([
+ getDecoratorMapper: { -> decoratorMapper }
+ ] as com.opensymphony.module.sitemesh.Factory)
+ def resourceLoader = new MockStringResourceLoader()
+ resourceLoader.registerMockResource('/layouts/layout.gsp', '<layoutapplied><g:layoutTitle /> - <g:layoutBody/></layoutapplied>')
+ appCtx.groovyPagesTemplateEngine.groovyPageLocator.addResourceLoader(resourceLoader)
+ def template = '<g:applyLayout name="layout"><html><head><title>title here</title></head><body>Hello world!</body></html></g:applyLayout>'
+ assertOutputEquals('<layoutapplied>title here - Hello world!</layoutapplied>', template)
+ }
+
+ void testApplyLayoutParse() {
+ def decoratorMapper = new GrailsLayoutDecoratorMapper()
+ decoratorMapper.groovyPageLayoutFinder = appCtx.groovyPageLayoutFinder
+ FactoryHolder.setFactory([
+ getDecoratorMapper: { -> decoratorMapper },
+ getPageParser : { String contentType -> new HTMLPageParser() }
+ ] as com.opensymphony.module.sitemesh.Factory)
+ def resourceLoader = new MockStringResourceLoader()
+ resourceLoader.registerMockResource('/layouts/layout.gsp', '<layoutapplied><g:layoutTitle /> - <g:layoutBody/></layoutapplied>')
+ appCtx.groovyPagesTemplateEngine.groovyPageLocator.addResourceLoader(resourceLoader)
+ def template = '<g:applyLayout name="layout" parse="${true}"><html><head><${"title"}>title here</${"title"}></head><body>Hello world!</body></html></g:applyLayout>'
+ assertOutputEquals('<layoutapplied>title here - Hello world!</layoutapplied>', template)
+
+ template = '<g:applyLayout name="layout" parse="false"><html><head><${"title"}>title here</${"title"}></head><body>Hello world!</body></html></g:applyLayout>'
+ assertOutputEquals('<layoutapplied> - Hello world!</layoutapplied>', template)
+ }
+
}
@Artefact('UrlMappings')
class RenderTagLibTestUrlMappings {
+
static mappings = {
name claimTab: "/claim/$id/$action" {
controller = 'Claim'
diff --git a/grails-gsp/spring-boot/build.gradle b/grails-gsp/spring-boot/build.gradle
index b73dc4330e..1a974d3997 100644
--- a/grails-gsp/spring-boot/build.gradle
+++ b/grails-gsp/spring-boot/build.gradle
@@ -34,7 +34,12 @@ ext {
dependencies {
implementation platform(project(':grails-bom'))
- api project(':grails-sitemesh3')
+ api project(':grails-layout')
+ api project(':grails-taglib')
+ api project(':grails-gsp-core')
+ api project(':grails-web-gsp')
+ api project(':grails-web-gsp-taglib')
+ api project(':grails-web-jsp')
}
apply {
diff --git a/grails-gsp/spring-boot/src/main/java/grails/gsp/boot/GspAutoConfiguration.java b/grails-gsp/spring-boot/src/main/java/grails/gsp/boot/GspAutoConfiguration.java
index be4a01fff5..e1313d7515 100644
--- a/grails-gsp/spring-boot/src/main/java/grails/gsp/boot/GspAutoConfiguration.java
+++ b/grails-gsp/spring-boot/src/main/java/grails/gsp/boot/GspAutoConfiguration.java
@@ -32,28 +32,23 @@ import org.grails.encoder.CodecLookup;
import org.grails.gsp.GroovyPagesTemplateEngine;
import org.grails.gsp.io.GroovyPageScriptSource;
import org.grails.gsp.jsp.TagLibraryResolver;
-import org.grails.plugins.web.taglib.RenderSitemeshTagLib;
+import org.grails.plugins.web.taglib.RenderGrailsLayoutTagLib;
import org.grails.taglib.TagLibraryLookup;
import org.grails.web.gsp.GroovyPagesTemplateRenderer;
import org.grails.web.pages.StandaloneTagLibraryLookup;
import org.grails.web.gsp.io.CachingGrailsConventionGroovyPageLocator;
import org.grails.web.gsp.io.GrailsConventionGroovyPageLocator;
import org.grails.gsp.jsp.TagLibraryResolverImpl;
+import org.grails.web.servlet.view.EmbeddedGrailsLayoutViewResolver;
import org.grails.web.servlet.view.GroovyPageViewResolver;
-import org.sitemesh.autoconfigure.SiteMeshAutoConfiguration;
-import org.grails.plugins.sitemesh3.GrailsLayoutHandlerMapping;
-import org.grails.plugins.sitemesh3.Sitemesh3GrailsPlugin;
-import org.springframework.beans.BeansException;
+import org.apache.grails.web.layout.GroovyPageLayoutFinder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanDefinition;
-import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
-import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
-import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
@@ -68,7 +63,7 @@ import org.springframework.web.context.ServletContextAware;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.ViewResolver;
-import org.grails.plugins.web.taglib.SitemeshTagLib;
+import org.grails.plugins.web.taglib.GrailsLayoutTagLib;
import org.grails.plugins.web.taglib.RenderTagLib;
import jakarta.servlet.ServletContext;
@@ -85,14 +80,6 @@ public class GspAutoConfiguration {
@Value("${spring.gsp.jspEnabled:true}")
boolean jspEnabled;
-
- @Value("${spring.gsp.sitmesh3:true}")
- boolean sitemesh3;
- }
-
- @Bean
- GrailsLayoutHandlerMapping grailsLayoutHandlerMapping() {
- return new GrailsLayoutHandlerMapping();
}
@Configuration
@@ -110,7 +97,7 @@ public class GspAutoConfiguration {
@Value("${spring.gsp.layout.caching:true}")
boolean gspLayoutCaching;
- @Value("${sitemesh.decorator.default:}")
+ @Value("${spring.gsp.layout.default:main}")
String defaultLayoutName;
@Bean
@@ -179,6 +166,17 @@ public class GspAutoConfiguration {
}
}
+ @Bean
+ @ConditionalOnMissingBean(name = "groovyPageLayoutFinder")
+ public GroovyPageLayoutFinder groovyPageLayoutFinder() {
+ GroovyPageLayoutFinder groovyPageLayoutFinder = new GroovyPageLayoutFinder();
+ groovyPageLayoutFinder.setGspReloadEnabled(gspReloadingEnabled);
+ groovyPageLayoutFinder.setCacheEnabled(gspLayoutCaching);
+ groovyPageLayoutFinder.setEnableNonGspViews(false);
+ groovyPageLayoutFinder.setDefaultDecoratorName(defaultLayoutName);
+ return groovyPageLayoutFinder;
+ }
+
@Bean
@ConditionalOnMissingBean(name = "groovyPagesTemplateRenderer")
GroovyPagesTemplateRenderer groovyPagesTemplateRenderer(GrailsConventionGroovyPageLocator groovyPageLocator) {
@@ -189,35 +187,16 @@ public class GspAutoConfiguration {
}
}
- @Configuration
- @AutoConfigureBefore(SiteMeshAutoConfiguration.class)
- @ConditionalOnMissingBean(name = "sitemesh3")
- protected static class Sitemesh3Configuration implements EnvironmentAware, BeanDefinitionRegistryPostProcessor {
- @Override
- public void setEnvironment(Environment environment) {
- if (environment instanceof ConfigurableEnvironment) {
- ConfigurableEnvironment configEnv = (ConfigurableEnvironment) environment;
- configEnv.getPropertySources().addFirst(Sitemesh3GrailsPlugin.getDefaultPropertySource(configEnv, null));
- }
- }
-
- @Override
- public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {}
-
- @Override
- public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {}
- }
-
@Configuration
protected static class GspViewResolverConfiguration extends AbstractGspConfig {
@Bean
@ConditionalOnMissingBean(name = "gspViewResolver")
- public ViewResolver gspViewResolver(GroovyPagesTemplateEngine groovyPagesTemplateEngine, GrailsConventionGroovyPageLocator groovyPageLocator) {
+ public ViewResolver gspViewResolver(GroovyPagesTemplateEngine groovyPagesTemplateEngine, GrailsConventionGroovyPageLocator groovyPageLocator, GroovyPageLayoutFinder groovyPageLayoutFinder) {
GroovyPageViewResolver groovyPageViewResolver = new GroovyPageViewResolver(groovyPagesTemplateEngine, groovyPageLocator);
groovyPageViewResolver.setResolveJspView(jspEnabled);
groovyPageViewResolver.setAllowGrailsViewCaching(!gspReloadingEnabled || viewCacheTimeout != 0);
groovyPageViewResolver.setCacheTimeout(gspReloadingEnabled ? viewCacheTimeout : -1);
- return groovyPageViewResolver;
+ return new EmbeddedGrailsLayoutViewResolver(groovyPageViewResolver, groovyPageLayoutFinder);
}
}
@@ -241,7 +220,7 @@ public class GspAutoConfiguration {
protected static class TagLibraryLookupRegistrar implements ImportBeanDefinitionRegistrar {
- public static final Class<?>[] DEFAULT_TAGLIB_CLASSES=new Class<?>[] { SitemeshTagLib.class, RenderTagLib.class, RenderSitemeshTagLib.class };
+ public static final Class<?>[] DEFAULT_TAGLIB_CLASSES=new Class<?>[] { GrailsLayoutTagLib.class, RenderTagLib.class, RenderGrailsLayoutTagLib.class };
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/AsyncFunctionalSpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/AsyncFunctionalSpec.groovy
index 9db409059e..26aab15e88 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/AsyncFunctionalSpec.groovy
+++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/AsyncFunctionalSpec.groovy
@@ -23,8 +23,6 @@ import grails.gorm.transactions.Rollback
import grails.plugin.geb.ContainerGebSpec
import grails.testing.mixin.integration.Integration
-import spock.lang.PendingFeature
-
/**
* @author graemerocher
*/
@@ -32,7 +30,6 @@ import spock.lang.PendingFeature
@Rollback
class AsyncFunctionalSpec extends ContainerGebSpec {
- @PendingFeature(reason = 'pageSource == <html><head></head><body></body></html>')
void "Test async response rendering works"() {
when: "When an async response is rendered"
go('/async/test')
diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/ForwardingSpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/ForwardingSpec.groovy
index e2299985f9..a0e02a2ac3 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/ForwardingSpec.groovy
+++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/ForwardingSpec.groovy
@@ -22,7 +22,6 @@ package functionaltests
import grails.gorm.transactions.Rollback
import grails.plugin.geb.ContainerGebSpec
import grails.testing.mixin.integration.Integration
-import spock.lang.PendingFeature
/**
*/
@@ -98,7 +97,6 @@ class ForwardingSpec extends ContainerGebSpec {
$('div', id: 'message').text() == 'flash.message is []'
}
- @PendingFeature
void 'forwarding to a view'() {
when: 'A forward is issued to a view'
go('/forwarding/forwardWithRender')
diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/layout/LayoutFunctionalSpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/layout/LayoutFunctionalSpec.groovy
index 4d9b06ced5..5fe4f53bf7 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/layout/LayoutFunctionalSpec.groovy
+++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/layout/LayoutFunctionalSpec.groovy
@@ -23,13 +23,11 @@ import functionaltests.Application
import grails.plugin.geb.ContainerGebSpec
import grails.testing.mixin.integration.Integration
import spock.lang.Issue
-import spock.lang.PendingFeature
@Integration(applicationClass = Application)
class LayoutFunctionalSpec extends ContainerGebSpec {
@Issue('GRAILS-12045')
- @PendingFeature(reason = 'title is empty')
void 'test layout by convention'() {
when:
go('/layoutByConvention')
@@ -39,7 +37,6 @@ class LayoutFunctionalSpec extends ContainerGebSpec {
}
@Issue('GRAILS-12045')
- @PendingFeature(reason='title is empty')
void 'test layout specified in controller property'() {
when:
go('/layoutSpecifiedByProperty')
@@ -50,7 +47,6 @@ class LayoutFunctionalSpec extends ContainerGebSpec {
}
@Issue('GRAILS-12045')
- @PendingFeature(reason='text does not contain the value')
void 'test layout specified in controller property applied to a GSP that does not contain a root html tag'() {
when:
go('/layoutSpecifiedByProperty/snippetView')
diff --git a/grails-test-examples/async-events-pubsub-demo/src/integration-test/groovy/pubsub/demo/TaskControllerSpec.groovy b/grails-test-examples/async-events-pubsub-demo/src/integration-test/groovy/pubsub/demo/TaskControllerSpec.groovy
index aef9b5833e..da5d83e4c6 100644
--- a/grails-test-examples/async-events-pubsub-demo/src/integration-test/groovy/pubsub/demo/TaskControllerSpec.groovy
+++ b/grails-test-examples/async-events-pubsub-demo/src/integration-test/groovy/pubsub/demo/TaskControllerSpec.groovy
@@ -44,11 +44,6 @@ class TaskControllerSpec extends Specification {
client = HttpClient.create("http://localhost:$serverPort".toURL())
}
- @PendingFeature(reason = '''
- For some reason the response body is blank with bootTestRun.
- However, when starting the application with bootRun,
- the response body is as expected.
- ''')
void 'test async error handling'() {
when: 'we invoke an endpoint that throws an exception'
diff --git a/grails-test-examples/gsp-layout/README.md b/grails-test-examples/gsp-layout/README.md
index 964086b547..8705e9161d 100644
--- a/grails-test-examples/gsp-layout/README.md
+++ b/grails-test-examples/gsp-layout/README.md
@@ -14,11 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
-# SiteMesh 3 Grails Plugin Example
-Example using the SiteMesh 3 Grails Plugin on how to use [SiteMesh 3](https://github.com/sitemesh/sitemesh3)
-
-## Want to See the Full Power for Sitemesh 3?
-Check out using SiteMesh 3 layouts on GSP and JSP pages, error pages, specifying layouts in controllers, and even applying multiple layouts to the same view!
+# Grails Layout Plugin Example
+Example using the Grails Layout Grails Plugin
You can see a working example by running this plugin:
```./gradlew bootRun```
diff --git a/grails-test-examples/gsp-layout/build.gradle b/grails-test-examples/gsp-layout/build.gradle
index 0cea1a3821..dfb8a643a2 100644
--- a/grails-test-examples/gsp-layout/build.gradle
+++ b/grails-test-examples/gsp-layout/build.gradle
@@ -32,7 +32,6 @@ dependencies {
implementation platform(project(':grails-bom'))
- implementation 'org.apache.grails:grails-sitemesh3'
implementation 'org.apache.grails:grails-core'
implementation 'org.apache.grails:grails-controllers'
implementation 'org.apache.grails:grails-rest-transforms'
diff --git a/grails-test-examples/gsp-layout/grails-app/conf/logback-spring.xml b/grails-test-examples/gsp-layout/grails-app/conf/logback-spring.xml
index fbf1088077..016803cb8b 100644
--- a/grails-test-examples/gsp-layout/grails-app/conf/logback-spring.xml
+++ b/grails-test-examples/gsp-layout/grails-app/conf/logback-spring.xml
@@ -35,5 +35,4 @@
<root level="error">
<appender-ref ref="STDOUT" />
</root>
- <logger name="org.sitemesh.config.ConfigurableSiteMeshFilter" level="INFO"/>
</configuration>
diff --git a/grails-test-examples/gsp-layout/grails-app/views/index.gsp b/grails-test-examples/gsp-layout/grails-app/views/index.gsp
index 87c28cae70..ba15ccf3f2 100644
--- a/grails-test-examples/gsp-layout/grails-app/views/index.gsp
+++ b/grails-test-examples/gsp-layout/grails-app/views/index.gsp
@@ -1,4 +1,4 @@
-<%@ page import="org.sitemesh.config.ConfigurableSiteMeshFilter; org.springframework.core.SpringVersion; org.springframework.boot.SpringBootVersion" %>
+<%@ page import="org.springframework.core.SpringVersion; org.springframework.boot.SpringBootVersion" %>
<%--
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
@@ -40,7 +40,6 @@
<li class="dropdown-item"><a href="#">Groovy version: ${GroovySystem.getVersion()}</a></li>
<li class="dropdown-item"><a href="#">Spring Boot version: ${SpringBootVersion.getVersion()}</a></li>
<li class="dropdown-item"><a href="#">Spring version: ${SpringVersion.getVersion()}</a></li>
- <li class="dropdown-item"><a href="#">SiteMesh version: ${ConfigurableSiteMeshFilter.class.getPackage().getSpecificationVersion()}</a></li>
<li class="dropdown-item"><a href="#">JVM version: ${System.getProperty('java.version')}</a></li>
<li role="separator" class="dropdown-divider"></li>
<li class="dropdown-item"><a href="#">Reloading active: ${grails.util.Environment.reloadingAgentEnabled}</a></li>
diff --git a/grails-test-examples/gsp-layout/src/integration-test/groovy/EndToEndSpec.groovy b/grails-test-examples/gsp-layout/src/integration-test/groovy/EndToEndSpec.groovy
index 5c26d2bb26..6cf1147873 100644
--- a/grails-test-examples/gsp-layout/src/integration-test/groovy/EndToEndSpec.groovy
+++ b/grails-test-examples/gsp-layout/src/integration-test/groovy/EndToEndSpec.groovy
@@ -16,9 +16,9 @@
* specific language governing permissions and limitations
* under the License.
*/
+
import grails.plugin.geb.ContainerGebSpec
import grails.testing.mixin.integration.Integration
-import spock.lang.PendingFeature
@Integration
class EndToEndSpec extends ContainerGebSpec {
@@ -44,7 +44,6 @@ class EndToEndSpec extends ContainerGebSpec {
</body></html>"""
}
- @PendingFeature
def 'multiple levels of layouts'() {
when:
go('endToEnd/multipleLevelsOfLayouts')
diff --git a/grails-test-examples/gsp-spring-boot/app/src/main/resources/application.properties b/grails-test-examples/gsp-spring-boot/app/src/main/resources/application.properties
index aa6f5f8d4b..69f50ce661 100644
--- a/grails-test-examples/gsp-spring-boot/app/src/main/resources/application.properties
+++ b/grails-test-examples/gsp-spring-boot/app/src/main/resources/application.properties
@@ -16,4 +16,4 @@
grails.gsp.tldScanPattern=classpath*:/META-INF/spring*.tld
spring.main.allow-circular-references=true
logging.level.web=trace
-sitemesh.decorator.default=main
\ No newline at end of file
+grails.views.layout.default=main
\ No newline at end of file
diff --git a/grails-test-examples/gsp-spring-boot/script/application.properties b/grails-test-examples/gsp-spring-boot/script/application.properties
index d3b2c33340..954732c425 100644
--- a/grails-test-examples/gsp-spring-boot/script/application.properties
+++ b/grails-test-examples/gsp-spring-boot/script/application.properties
@@ -14,5 +14,4 @@
# limitations under the License.
spring.gsp.templateRoots=classpath:/templates
-spring.main.allow-circular-references=true
-sitemesh.decorator.default=main
\ No newline at end of file
+spring.main.allow-circular-references=true
\ No newline at end of file
diff --git a/grails-test-suite-uber/build.gradle b/grails-test-suite-uber/build.gradle
index 6af35c9af7..036ecc7589 100644
--- a/grails-test-suite-uber/build.gradle
+++ b/grails-test-suite-uber/build.gradle
@@ -45,6 +45,7 @@ dependencies {
testImplementation 'org.apache.groovy:groovy'
testImplementation 'org.apache.groovy:groovy-test-junit5'
testImplementation 'org.junit.jupiter:junit-jupiter-api'
+ testImplementation 'com.fasterxml.jackson.core:jackson-databind'
testImplementation project(':grails-data-hibernate5-core')
testImplementation project(':grails-gsp')
testImplementation project(':grails-testing-support-datamapping'), {
diff --git a/grails-web-common/src/main/groovy/org/grails/web/config/http/GrailsFilters.java b/grails-web-common/src/main/groovy/org/grails/web/config/http/GrailsFilters.java
index b82d26c695..0c78ec7c4b 100644
--- a/grails-web-common/src/main/groovy/org/grails/web/config/http/GrailsFilters.java
+++ b/grails-web-common/src/main/groovy/org/grails/web/config/http/GrailsFilters.java
@@ -31,7 +31,6 @@ public enum GrailsFilters {
ASSET_PIPELINE_FILTER,
CHARACTER_ENCODING_FILTER,
HIDDEN_HTTP_METHOD_FILTER,
- SITEMESH_FILTER,
GRAILS_WEB_REQUEST_FILTER,
LAST(SecurityProperties.DEFAULT_FILTER_ORDER - 10);
diff --git a/grails-web-common/src/main/groovy/org/grails/web/util/WebUtils.java b/grails-web-common/src/main/groovy/org/grails/web/util/WebUtils.java
index 0ad6b6176f..6012f755c2 100644
--- a/grails-web-common/src/main/groovy/org/grails/web/util/WebUtils.java
+++ b/grails-web-common/src/main/groovy/org/grails/web/util/WebUtils.java
@@ -64,6 +64,7 @@ public class WebUtils extends org.springframework.web.util.WebUtils {
public static final String DISPATCH_ACTION_PARAMETER = "_action_";
public static final String SEND_ALLOW_HEADER_FOR_INVALID_HTTP_METHOD = "grails.http.invalid.method.allow.header";
public static final String LAYOUT_ATTRIBUTE = "org.grails.layout.name";
+ public static final String NONE_LAYOUT = "_none_"; // legacy, sitemesh2 setting to not set a layout
public static final String RENDERING_VIEW = "org.grails.rendering.view";
public static final String GRAILS_DISPATCH_EXTENSION = ".dispatch";
public static final String GRAILS_SERVLET_PATH = "/grails";
diff --git a/licenses/LICENSE-opensymphony.txt b/licenses/LICENSE-opensymphony.txt
new file mode 100644
index 0000000000..70dba55e35
--- /dev/null
+++ b/licenses/LICENSE-opensymphony.txt
@@ -0,0 +1,49 @@
+====================================================================
+The OpenSymphony Software License, Version 1.1
+
+(this license is derived and fully compatible with the Apache Software
+License - see http://www.apache.org/LICENSE.txt)
+
+Copyright (c) 2001 The OpenSymphony Group. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+3. The end-user documentation included with the redistribution,
+ if any, must include the following acknowledgment:
+ "This product includes software developed by the
+ OpenSymphony Group (http://www.opensymphony.com/)."
+ Alternately, this acknowledgment may appear in the software itself,
+ if and wherever such third-party acknowledgments normally appear.
+
+4. The names "OpenSymphony" and "The OpenSymphony Group"
+ must not be used to endorse or promote products derived from this
+ software without prior written permission. For written
+ permission, please contact license@opensymphony.com .
+
+5. Products derived from this software may not be called "OpenSymphony"
+ or "SiteMesh", nor may "OpenSymphony" or "SiteMesh" appear in their
+ name, without prior written permission of the OpenSymphony Group.
+
+THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
+ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGE.
+====================================================================
diff --git a/settings.gradle b/settings.gradle
index b44e9ba567..62972ed025 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -135,11 +135,12 @@ include(
// gsp
'grails-gsp-core',
'grails-gsp',
- 'grails-sitemesh3',
+ 'grails-layout',
'grails-taglib',
'grails-web-gsp',
'grails-web-gsp-taglib',
'grails-web-jsp',
+ 'grails-web-layout',
'grails-web-taglib',
'grails-gsp-spring-boot',
@@ -190,11 +191,12 @@ project(':grails-async').projectDir = file('grails-async/plugin')
project(':grails-gsp-core').projectDir = file('grails-gsp/core')
project(':grails-gsp').projectDir = file('grails-gsp/plugin')
-project(':grails-sitemesh3').projectDir = file('grails-gsp/grails-sitemesh3')
+project(':grails-layout').projectDir = file('grails-gsp/grails-layout')
project(':grails-taglib').projectDir = file('grails-gsp/grails-taglib')
project(':grails-web-gsp').projectDir = file('grails-gsp/grails-web-gsp')
project(':grails-web-gsp-taglib').projectDir = file('grails-gsp/grails-web-gsp-taglib')
project(':grails-web-jsp').projectDir = file('grails-gsp/grails-web-jsp')
+project(':grails-web-layout').projectDir = file('grails-gsp/grails-web-layout')
project(':grails-web-taglib').projectDir = file('grails-gsp/grails-web-taglib')
project(':grails-gsp-spring-boot').projectDir = file('grails-gsp/spring-boot')