diff --git a/grails-doc/src/en/guide/testing/unitTesting/unitTestingTagLibraries.adoc b/grails-doc/src/en/guide/testing/unitTesting/unitTestingTagLibraries.adoc
index f4d7d23ee2f..595fdeb5323 100644
--- a/grails-doc/src/en/guide/testing/unitTesting/unitTestingTagLibraries.adoc
+++ b/grails-doc/src/en/guide/testing/unitTesting/unitTestingTagLibraries.adoc
@@ -39,7 +39,7 @@ Adding the `TagLibUnitTest` trait to a test causes a new `tagLib` field to be
automatically created for the TagLib class under test. The `tagLib` property can
be used to test calling tags as function calls. The return value of a function
call is either a `org.grails.buffer,StreamCharBuffer`
-instance or the object returned from the tag closure when
+instance or the object returned from the tag handler when
`returnObjectForTags` feature is used.
To test a tag which accepts parameters, specify the parameter values as named
diff --git a/grails-doc/src/en/guide/theWebLayer/gsp/taglibs.adoc b/grails-doc/src/en/guide/theWebLayer/gsp/taglibs.adoc
index e6bfc66ed13..cfe5b738cd5 100644
--- a/grails-doc/src/en/guide/theWebLayer/gsp/taglibs.adoc
+++ b/grails-doc/src/en/guide/theWebLayer/gsp/taglibs.adoc
@@ -28,28 +28,63 @@ class SimpleTagLib {
}
----
-Now to create a tag create a Closure property that takes two arguments: the tag attributes and the body content:
+Now create tags using methods. You can access tag attributes through the implicit `attrs` map and body through the implicit `body` closure:
[source,groovy]
----
class SimpleTagLib {
- def simple = { attrs, body ->
+ def simple() {
+ // ...
}
}
----
-The `attrs` argument is a Map of the attributes of the tag, whilst the `body` argument is a Closure that returns the body content when invoked:
+Closure field-style tags are still supported for backward compatibility, but method-based tags are the recommended syntax.
+
+The implicit `attrs` property is a `Map` of the tag attributes, while `body()` returns the tag body content when invoked:
[source,groovy]
----
class SimpleTagLib {
- def emoticon = { attrs, body ->
+ def emoticon() {
out << body() << (attrs.happy == 'true' ? " :-)" : " :-(")
}
}
----
+For method-based tags, named attributes can also bind directly to method signature arguments:
+
+[source,groovy]
+----
+class SimpleTagLib {
+ def greeting(String name) {
+ out << "Hello, ${name}!"
+ }
+}
+----
+
+Used as:
+
+[source,xml]
+----
+
+----
+
+For tags with strict validation/error handling, you can keep a `Map attrs` handler and add typed overloads that delegate to it:
+
+[source,groovy]
+----
+def field(Map attrs) {
+ // existing validation + rendering path
+}
+
+def field(String type, Map attrs) {
+ attrs.type = type
+ field(attrs)
+}
+----
+
As demonstrated above there is an implicit `out` variable that refers to the output `Writer` which you can use to append content to the response. Then you can reference the tag inside your GSP; no imports are necessary:
[source,xml]
@@ -57,7 +92,7 @@ As demonstrated above there is an implicit `out` variable that refers to the out
Hi John
----
-NOTE: To help IDEs like Spring Tool Suite (STS) and others autocomplete tag attributes, you should add Javadoc comments to your tag closures with `@attr` descriptions. Since taglibs use Groovy code it can be difficult to reliably detect all usable attributes.
+NOTE: To help IDEs like Spring Tool Suite (STS) and others autocomplete tag attributes, add Javadoc comments with `@attr` descriptions to your tag methods. Since taglibs use Groovy code it can be difficult to reliably detect all usable attributes.
For example:
@@ -71,7 +106,7 @@ class SimpleTagLib {
* @attr happy whether to show a happy emoticon ('true') or
* a sad emoticon ('false')
*/
- def emoticon = { attrs, body ->
+ def emoticon() {
out << body() << (attrs.happy == 'true' ? " :-)" : " :-(")
}
}
@@ -89,7 +124,7 @@ class SimpleTagLib {
* @attr name REQUIRED the field name
* @attr value the field value
*/
- def passwordField = { attrs ->
+ def passwordField() {
attrs.type = "password"
attrs.tagName = "passwordField"
fieldImpl(out, attrs)
diff --git a/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/iterativeTags.adoc b/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/iterativeTags.adoc
index 26497339d72..45871599324 100644
--- a/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/iterativeTags.adoc
+++ b/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/iterativeTags.adoc
@@ -21,7 +21,7 @@ Iterative tags are easy too, since you can invoke the body multiple times:
[source,groovy]
----
-def repeat = { attrs, body ->
+def repeat() {
attrs.times?.toInteger()?.times { num ->
out << body(num)
}
@@ -48,7 +48,7 @@ That value is then passed as the default variable `it` to the tag. However, if y
[source,groovy]
----
-def repeat = { attrs, body ->
+def repeat() {
def var = attrs.var ?: "num"
attrs.times?.toInteger()?.times { num ->
out << body((var):num)
diff --git a/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/logicalTags.adoc b/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/logicalTags.adoc
index b6e7b442fef..fce648d10b5 100644
--- a/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/logicalTags.adoc
+++ b/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/logicalTags.adoc
@@ -21,7 +21,7 @@ You can also create logical tags where the body of the tag is only output once a
[source,groovy]
----
-def isAdmin = { attrs, body ->
+def isAdmin() {
def user = attrs.user
if (user && checkUserPrivs(user)) {
out << body()
diff --git a/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/namespaces.adoc b/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/namespaces.adoc
index d87d01223e5..91d25b83359 100644
--- a/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/namespaces.adoc
+++ b/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/namespaces.adoc
@@ -23,7 +23,7 @@ By default, tags are added to the default Grails namespace and are used with the
----
class SimpleTagLib {
static namespace = "my"
-
+ def example() {
def example = { attrs ->
//...
}
diff --git a/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/simpleTags.adoc b/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/simpleTags.adoc
index f564df62eb3..cc1e37b9458 100644
--- a/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/simpleTags.adoc
+++ b/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/simpleTags.adoc
@@ -21,7 +21,7 @@ As demonstrated in the previous example it is easy to write simple tags that hav
[source,groovy]
----
-def dateFormat = { attrs, body ->
+def dateFormat() {
out << new java.text.SimpleDateFormat(attrs.format).format(attrs.date)
}
----
@@ -33,11 +33,20 @@ The above uses Java's `SimpleDateFormat` class to format a date and then write i
----
+With method-based tags, attributes may also bind directly to method parameters by name:
+
+[source,groovy]
+----
+def dateFormat(String format, Date date) {
+ out << new java.text.SimpleDateFormat(format).format(date)
+}
+----
+
With simple tags sometimes you need to write HTML mark-up to the response. One approach would be to embed the content directly:
[source,groovy]
----
-def formatBook = { attrs, body ->
+def formatBook() {
out << "
"
out << "Title : ${attrs.book.title}"
out << "
"
@@ -48,7 +57,7 @@ Although this approach may be tempting it is not very clean. A better approach w
[source,groovy]
----
-def formatBook = { attrs, body ->
+def formatBook() {
out << render(template: "bookTemplate", model: [book: attrs.book])
}
----
diff --git a/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/tagReturnValue.adoc b/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/tagReturnValue.adoc
index 65b8a434925..6027e9653e9 100644
--- a/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/tagReturnValue.adoc
+++ b/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/tagReturnValue.adoc
@@ -19,13 +19,13 @@ under the License.
A taglib can be used in a GSP as an ordinary tag, or it might be used as a function in other taglibs or GSP expressions.
-Internally Grails intercepts calls to taglib closures.
+Internally Grails intercepts calls to tag handlers (method-based or closure-based).
The "out" that is available in a taglib is mapped to a `java.io.Writer` implementation that writes to a buffer
that "captures" the output of the taglib call. This buffer is the return value of a tag library call when it's
used as a function.
If the tag is listed in the library's static `returnObjectForTags` array, then its return value will be written to
-the output when it's used as a normal tag. The return value of the tag lib closure will be returned as-is
+the output when it's used as a normal tag. The return value of the tag method/closure will be returned as-is
if it's used as a function in GSP expressions or other taglibs.
If the tag is not included in the returnObjectForTags array, then its return value will be discarded.
@@ -37,7 +37,7 @@ Example:
class ObjectReturningTagLib {
static namespace = "cms"
static returnObjectForTags = ['content']
-
+ def content() {
def content = { attrs, body ->
CmsContent.findByCode(attrs.code)?.content
}
diff --git a/grails-doc/src/en/guide/theWebLayer/taglibs.adoc b/grails-doc/src/en/guide/theWebLayer/taglibs.adoc
index e7066068354..fa63946ca70 100644
--- a/grails-doc/src/en/guide/theWebLayer/taglibs.adoc
+++ b/grails-doc/src/en/guide/theWebLayer/taglibs.adoc
@@ -28,28 +28,63 @@ class SimpleTagLib {
}
----
-Now to create a tag create a Closure property that takes two arguments: the tag attributes and the body content:
+Now create tags using methods. You can access tag attributes through the implicit `attrs` map and body through the implicit `body` closure:
[source,groovy]
----
class SimpleTagLib {
- def simple = { attrs, body ->
+ def simple() {
+ // ...
}
}
----
-The `attrs` argument is a Map of the attributes of the tag, whilst the `body` argument is a Closure that returns the body content when invoked:
+Closure field-style tags are still supported for backward compatibility, but method-based tags are the recommended syntax.
+
+The implicit `attrs` property is a `Map` of the tag attributes, while `body()` returns the tag body content when invoked:
[source,groovy]
----
class SimpleTagLib {
- def emoticon = { attrs, body ->
+ def emoticon() {
out << body() << (attrs.happy == 'true' ? " :-)" : " :-(")
}
}
----
+For method-based tags, named attributes can also bind directly to method signature arguments:
+
+[source,groovy]
+----
+class SimpleTagLib {
+ def greeting(String name) {
+ out << "Hello, ${name}!"
+ }
+}
+----
+
+Used as:
+
+[source,xml]
+----
+
+----
+
+For tags with strict validation/error handling, keep a `Map attrs` handler and add typed overloads that delegate to it:
+
+[source,groovy]
+----
+def field(Map attrs) {
+ // existing validation + rendering path
+}
+
+def field(String type, Map attrs) {
+ attrs.type = type
+ field(attrs)
+}
+----
+
As demonstrated above there is an implicit `out` variable that refers to the output `Writer` which you can use to append content to the response. Then you can reference the tag inside your GSP; no imports are necessary:
[source,xml]
@@ -57,7 +92,7 @@ As demonstrated above there is an implicit `out` variable that refers to the out
Hi John
----
-NOTE: To help IDEs autocomplete tag attributes, you should add Javadoc comments to your tag closures with `@attr` descriptions. Since taglibs use Groovy code it can be difficult to reliably detect all usable attributes.
+NOTE: To help IDEs autocomplete tag attributes, add Javadoc comments with `@attr` descriptions to your tag methods. Since taglibs use Groovy code it can be difficult to reliably detect all usable attributes.
For example:
@@ -71,7 +106,7 @@ class SimpleTagLib {
* @attr happy whether to show a happy emoticon ('true') or
* a sad emoticon ('false')
*/
- def emoticon = { attrs, body ->
+ def emoticon() {
out << body() << (attrs.happy == 'true' ? " :-)" : " :-(")
}
}
@@ -89,7 +124,7 @@ class SimpleTagLib {
* @attr name REQUIRED the field name
* @attr value the field value
*/
- def passwordField = { attrs ->
+ def passwordField() {
attrs.type = "password"
attrs.tagName = "passwordField"
fieldImpl(out, attrs)
diff --git a/grails-fields/grails-app/taglib/grails/plugin/formfields/FormFieldsTagLib.groovy b/grails-fields/grails-app/taglib/grails/plugin/formfields/FormFieldsTagLib.groovy
index 3631c820012..fb5eebe4abd 100644
--- a/grails-fields/grails-app/taglib/grails/plugin/formfields/FormFieldsTagLib.groovy
+++ b/grails-fields/grails-app/taglib/grails/plugin/formfields/FormFieldsTagLib.groovy
@@ -247,7 +247,7 @@ class FormFieldsTagLib {
widgetAttrs.remove('class')
}
if (hasBody(body)) {
- model.widget = raw(body(model + [attrs: widgetAttrs] + widgetAttrs))
+ model.widget = body(model + [attrs: widgetAttrs] + widgetAttrs)?.encodeAsRaw()
} else {
model.widget = renderWidget(propertyAccessor, model, widgetAttrs, widgetFolder ?: templatesFolder, theme)
}
@@ -389,7 +389,7 @@ class FormFieldsTagLib {
out << render(template: "/templates/_fields/$template", model: attrs + [domainClass: domainClass, domainProperties: properties]) { prop ->
BeanPropertyAccessor propertyAccessor = resolveProperty(bean, prop.name)
Map model = buildModel(propertyAccessor, attrs, 'HTML')
- out << raw(renderDisplayWidget(propertyAccessor, model, attrs, templatesFolder, theme))
+ out << renderDisplayWidget(propertyAccessor, model, attrs, templatesFolder, theme)?.encodeAsRaw()
}
}
} else {
@@ -415,7 +415,7 @@ class FormFieldsTagLib {
String widgetsFolderToUse = widgetFolder ?: templatesFolder
if (hasBody(body)) {
- model.widget = raw(body(model + [attrs: widgetAttrs] + widgetAttrs))
+ model.widget = body(model + [attrs: widgetAttrs] + widgetAttrs)?.encodeAsRaw()
model.value = body(model)
} else {
model.widget = renderDisplayWidget(propertyAccessor, model, widgetAttrs, widgetsFolderToUse, theme)
@@ -426,7 +426,7 @@ class FormFieldsTagLib {
if (template) {
out << render(template: template.path, plugin: template.plugin, model: model + [attrs: wrapperAttrs] + wrapperAttrs)
} else {
- out << raw(renderDisplayWidget(propertyAccessor, model, attrs, widgetsFolderToUse, theme))
+ out << renderDisplayWidget(propertyAccessor, model, attrs, widgetsFolderToUse, theme)?.encodeAsRaw()
}
}
@@ -682,7 +682,7 @@ class FormFieldsTagLib {
message ?: defaultMessage
}
- protected CharSequence renderDefaultField(Map model, Map attrs = [:]) {
+ private CharSequence renderDefaultField(Map model, Map attrs = [:]) {
List classes = [attrs['class'] ?: 'fieldcontain']
if (model.invalid) classes << (attrs.remove('invalidClass') ?: 'error')
if (model.required) classes << (attrs.remove('requiredClass') ?: 'required')
@@ -715,11 +715,11 @@ class FormFieldsTagLib {
}
}
- CharSequence renderDefaultInput(Map model, Map attrs = [:]) {
+ private CharSequence renderDefaultInput(Map model, Map attrs = [:]) {
renderDefaultInput(null, model, attrs)
}
- CharSequence renderDefaultInput(BeanPropertyAccessor propertyAccessor, Map model, Map attrs = [:]) {
+ private CharSequence renderDefaultInput(BeanPropertyAccessor propertyAccessor, Map model, Map attrs = [:]) {
Constrained constrained = (Constrained) model.constraints
attrs.name = (model.prefix ?: '') + model.property
attrs.value = model.value
@@ -778,7 +778,7 @@ class FormFieldsTagLib {
}
}
- CharSequence renderDateTimeInput(Map model, Map attrs) {
+ private CharSequence renderDateTimeInput(Map model, Map attrs) {
attrs.precision = model.type in [java.sql.Time, LocalDateTime] ? 'minute' : 'day'
if (!model.required) {
attrs.noSelection = ['': '']
@@ -787,7 +787,7 @@ class FormFieldsTagLib {
return g.datePicker(attrs)
}
- CharSequence renderStringInput(Map model, Map attrs) {
+ private CharSequence renderStringInput(Map model, Map attrs) {
Constrained constrained = (Constrained) model.constraints
if (!attrs.type) {
@@ -819,7 +819,7 @@ class FormFieldsTagLib {
return g.field(attrs)
}
- CharSequence renderNumericInput(BeanPropertyAccessor propertyAccessor, Map model, Map attrs) {
+ private CharSequence renderNumericInput(BeanPropertyAccessor propertyAccessor, Map model, Map attrs) {
Constrained constrained = (Constrained) model.constraints
if (!attrs.type && constrained?.inList) {
@@ -845,12 +845,12 @@ class FormFieldsTagLib {
}
@CompileStatic
- protected NumberFormat getNumberFormatter() {
+ private NumberFormat getNumberFormatter() {
NumberFormat.getInstance(getLocale())
}
@CompileStatic
- protected Locale getLocale() {
+ private Locale getLocale() {
def locale
def request = GrailsWebRequest.lookup()?.currentRequest
if (request instanceof HttpServletRequest) {
@@ -863,7 +863,7 @@ class FormFieldsTagLib {
}
@CompileStatic
- protected String getDefaultNumberType(Map model) {
+ private String getDefaultNumberType(Map model) {
Class modelType = (Class) model.type
def typeName = modelType.simpleName.toLowerCase()
@@ -992,7 +992,7 @@ class FormFieldsTagLib {
buffer << render(template: '/templates/_fields/list', model: [domainClass: domainClass, domainProperties: properties]) { prop ->
def propertyAccessor = resolveProperty(bean, prop.name)
def model = buildModel(propertyAccessor, attrs)
- out << raw(renderDisplayWidget(propertyAccessor, model, attrs, templatesFolder, theme))
+ out << renderDisplayWidget(propertyAccessor, model, attrs, templatesFolder, theme)?.encodeAsRaw()
}
buffer.buffer
}
diff --git a/grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPage.java b/grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPage.java
index 095f5a098c3..49f3068538e 100644
--- a/grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPage.java
+++ b/grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPage.java
@@ -49,6 +49,8 @@
import org.grails.taglib.GroovyPageAttributes;
import org.grails.taglib.TagBodyClosure;
import org.grails.taglib.TagLibraryLookup;
+import org.grails.taglib.TagMethodContext;
+import org.grails.taglib.TagMethodInvoker;
import org.grails.taglib.TagOutput;
import org.grails.taglib.encoder.OutputContext;
import org.grails.taglib.encoder.OutputEncodingStack;
@@ -381,10 +383,13 @@ public final void invokeTag(String tagName, String tagNamespace, int lineNumber,
if (tagLib != null || (gspTagLibraryLookup != null && gspTagLibraryLookup.hasNamespace(tagNamespace))) {
if (tagLib != null) {
boolean returnsObject = gspTagLibraryLookup.doesTagReturnObject(tagNamespace, tagName);
- Object tagLibClosure = tagLib.getProperty(tagName);
+ Object tagLibClosure = TagMethodInvoker.getClosureTagProperty(tagLib, tagName);
if (tagLibClosure instanceof Closure) {
Map encodeAsForTag = gspTagLibraryLookup.getEncodeAsForTag(tagNamespace, tagName);
invokeTagLibClosure(tagName, tagNamespace, (Closure) tagLibClosure, attrs, body, returnsObject, encodeAsForTag);
+ } else if (TagMethodInvoker.hasInvokableTagMethod(tagLib, tagName)) {
+ Map encodeAsForTag = gspTagLibraryLookup.getEncodeAsForTag(tagNamespace, tagName);
+ invokeTagLibMethod(tagName, tagNamespace, tagLib, attrs, body, returnsObject, encodeAsForTag);
} else {
throw new GrailsTagException("Tag [" + tagName + "] does not exist in tag library [" + tagLib.getClass().getName() + "]", getGroovyPageFileName(), lineNumber);
}
@@ -475,6 +480,29 @@ private void invokeTagLibClosure(String tagName, String tagNamespace, Closure>
}
}
+ private void invokeTagLibMethod(String tagName, String tagNamespace, GroovyObject tagLib, Map, ?> attrs, Closure> body,
+ boolean returnsObject, Map defaultEncodeAs) {
+ if (!(attrs instanceof GroovyPageAttributes)) {
+ attrs = new GroovyPageAttributes(attrs);
+ }
+ ((GroovyPageAttributes) attrs).setGspTagSyntaxCall(true);
+ boolean encodeAsPushedToStack = false;
+ try {
+ Map codecSettings = TagOutput.createCodecSettings(tagNamespace, tagName, attrs, defaultEncodeAs);
+ if (codecSettings != null) {
+ outputStack.push(WithCodecHelper.createOutputStackAttributesBuilder(codecSettings, outputContext.getGrailsApplication()).build());
+ encodeAsPushedToStack = true;
+ }
+ Closure> actualBody = body != null ? body : TagOutput.EMPTY_BODY_CLOSURE;
+ TagMethodContext.push(attrs, actualBody);
+ Object tagResult = TagMethodInvoker.invokeTagMethod(tagLib, tagName, attrs, actualBody);
+ outputTagResult(returnsObject, tagResult);
+ } finally {
+ TagMethodContext.pop();
+ if (encodeAsPushedToStack) outputStack.pop();
+ }
+ }
+
private void outputTagResult(boolean returnsObject, Object tagresult) {
if (returnsObject && tagresult != null && !(tagresult instanceof Writer)) {
if (tagresult instanceof String && isHtmlPart((String) tagresult)) {
diff --git a/grails-gsp/grails-taglib/src/main/groovy/org/grails/core/gsp/DefaultGrailsTagLibClass.java b/grails-gsp/grails-taglib/src/main/groovy/org/grails/core/gsp/DefaultGrailsTagLibClass.java
index 9719da62d93..68a42f2c5c1 100644
--- a/grails-gsp/grails-taglib/src/main/groovy/org/grails/core/gsp/DefaultGrailsTagLibClass.java
+++ b/grails-gsp/grails-taglib/src/main/groovy/org/grails/core/gsp/DefaultGrailsTagLibClass.java
@@ -33,6 +33,7 @@
import grails.core.gsp.GrailsTagLibClass;
import org.grails.core.AbstractInjectableGrailsClass;
import org.grails.core.artefact.gsp.TagLibArtefactHandler;
+import org.grails.taglib.TagMethodInvoker;
/**
* Default implementation of a tag lib class.
@@ -69,6 +70,7 @@ public DefaultGrailsTagLibClass(Class> clazz) {
tags.add(prop.getName());
}
}
+ tags.addAll(TagMethodInvoker.getInvokableTagMethodNames(clazz));
String ns = getStaticPropertyValue(NAMESPACE_FIELD_NAME, String.class);
if (ns != null && !"".equals(ns.trim())) {
diff --git a/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagLibraryMetaUtils.groovy b/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagLibraryMetaUtils.groovy
index 5937aac2e7d..bbcb4db8d47 100644
--- a/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagLibraryMetaUtils.groovy
+++ b/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagLibraryMetaUtils.groovy
@@ -44,10 +44,26 @@ class TagLibraryMetaUtils {
@CompileStatic
static void enhanceTagLibMetaClass(MetaClass mc, TagLibraryLookup gspTagLibraryLookup, String namespace) {
+ registerTagMethodContextMetaProperties(mc)
registerTagMetaMethods(mc, gspTagLibraryLookup, namespace)
registerNamespaceMetaProperties(mc, gspTagLibraryLookup)
}
+ @CompileStatic
+ static void registerTagMethodContextMetaProperties(MetaClass metaClass) {
+ GroovyObject mc = (GroovyObject) metaClass
+ if (!metaClass.hasProperty('attrs') && !doesMethodExist(metaClass, 'getAttrs', [] as Class[])) {
+ mc.setProperty('getAttrs') { ->
+ TagMethodContext.currentAttrs()
+ }
+ }
+ if (!metaClass.hasProperty('body') && !doesMethodExist(metaClass, 'getBody', [] as Class[])) {
+ mc.setProperty('getBody') { ->
+ TagMethodContext.currentBody()
+ }
+ }
+ }
+
@CompileStatic
static void registerNamespaceMetaProperties(MetaClass mc, TagLibraryLookup gspTagLibraryLookup) {
for (String ns : gspTagLibraryLookup.getAvailableNamespaces()) {
@@ -57,9 +73,7 @@ class TagLibraryMetaUtils {
@CompileStatic
static void registerNamespaceMetaProperty(MetaClass metaClass, TagLibraryLookup gspTagLibraryLookup, String namespace) {
- if (!metaClass.hasProperty(namespace) && !doesMethodExist(metaClass, GrailsClassUtils.getGetterName(namespace), [] as Class[])) {
- registerPropertyMissingForTag(metaClass, namespace, gspTagLibraryLookup.lookupNamespaceDispatcher(namespace))
- }
+ registerPropertyMissingForTag(metaClass, namespace, gspTagLibraryLookup.lookupNamespaceDispatcher(namespace))
}
@CompileStatic
@@ -68,33 +82,39 @@ class TagLibraryMetaUtils {
if (overrideMethods || !doesMethodExist(metaClass, name, [Map, Closure] as Class[])) {
mc.setProperty(name) { Map attrs, Closure body ->
- TagOutput.captureTagOutput(gspTagLibraryLookup, namespace, name, attrs, body, OutputContextLookupHelper.lookupOutputContext())
+ captureTagOutputForMethodCall(gspTagLibraryLookup, namespace, name, attrs, body)
}
}
if (overrideMethods || !doesMethodExist(metaClass, name, [Map, CharSequence] as Class[])) {
mc.setProperty(name) { Map attrs, CharSequence body ->
- TagOutput.captureTagOutput(gspTagLibraryLookup, namespace, name, attrs, new TagOutput.ConstantClosure(body), OutputContextLookupHelper.lookupOutputContext())
+ captureTagOutputForMethodCall(gspTagLibraryLookup, namespace, name, attrs, new TagOutput.ConstantClosure(body))
}
}
if (overrideMethods || !doesMethodExist(metaClass, name, [Map] as Class[])) {
mc.setProperty(name) { Map attrs ->
- TagOutput.captureTagOutput(gspTagLibraryLookup, namespace, name, attrs, null, OutputContextLookupHelper.lookupOutputContext())
+ captureTagOutputForMethodCall(gspTagLibraryLookup, namespace, name, attrs, null)
}
}
if (addAll) {
if (overrideMethods || !doesMethodExist(metaClass, name, [Closure] as Class[])) {
mc.setProperty(name) { Closure body ->
- TagOutput.captureTagOutput(gspTagLibraryLookup, namespace, name, [:], body, OutputContextLookupHelper.lookupOutputContext())
+ captureTagOutputForMethodCall(gspTagLibraryLookup, namespace, name, [:], body)
}
}
if (overrideMethods || !doesMethodExist(metaClass, name, [] as Class[])) {
mc.setProperty(name) { ->
- TagOutput.captureTagOutput(gspTagLibraryLookup, namespace, name, [:], null, OutputContextLookupHelper.lookupOutputContext())
+ captureTagOutputForMethodCall(gspTagLibraryLookup, namespace, name, [:], null)
}
}
}
}
+ @CompileStatic
+ private static Object captureTagOutputForMethodCall(TagLibraryLookup gspTagLibraryLookup, String namespace, String name, Map attrs, Object body) {
+ Object output = TagOutput.captureTagOutput(gspTagLibraryLookup, namespace, name, attrs, body, OutputContextLookupHelper.lookupOutputContext())
+ return output
+ }
+
static registerMethodMissingForTags(MetaClass mc, ApplicationContext ctx,
GrailsTagLibClass tagLibraryClass, String name) {
TagLibraryLookup gspTagLibraryLookup = ctx.getBean('gspTagLibraryLookup')
@@ -109,13 +129,13 @@ class TagLibraryMetaUtils {
}
@CompileStatic
- static void registerTagMetaMethods(MetaClass emc, TagLibraryLookup lookup, String namespace) {
+ static void registerTagMetaMethods(MetaClass emc, TagLibraryLookup lookup, String namespace, boolean overrideMethods = true) {
for (String tagName : lookup.getAvailableTags(namespace)) {
boolean addAll = !(namespace == TagOutput.DEFAULT_NAMESPACE && tagName == 'hasErrors')
- registerMethodMissingForTags(emc, lookup, namespace, tagName, addAll, false)
+ registerMethodMissingForTags(emc, lookup, namespace, tagName, addAll, overrideMethods)
}
if (namespace != TagOutput.DEFAULT_NAMESPACE) {
- registerTagMetaMethods(emc, lookup, TagOutput.DEFAULT_NAMESPACE)
+ registerTagMetaMethods(emc, lookup, TagOutput.DEFAULT_NAMESPACE, false)
}
}
@@ -149,6 +169,34 @@ class TagLibraryMetaUtils {
Object[] args = makeObjectArray(argsParam)
final GroovyObject tagBean = gspTagLibraryLookup.lookupTagLibrary(namespace, name)
if (tagBean != null) {
+ Object tagLibProp = TagMethodInvoker.getClosureTagProperty(tagBean, name)
+ if (tagLibProp instanceof Closure || TagMethodInvoker.hasInvokableTagMethod(tagBean, name)) {
+ Map attrs = [:]
+ Object body = null
+ switch (args.length) {
+ case 0:
+ break
+ case 1:
+ if (args[0] instanceof Map) {
+ attrs = (Map) args[0]
+ } else if (args[0] instanceof Closure || args[0] instanceof CharSequence) {
+ body = args[0]
+ } else {
+ attrs = [(name): args[0]]
+ }
+ break
+ case 2:
+ if (args[0] instanceof Map) {
+ attrs = (Map) args[0]
+ body = args[1]
+ }
+ break
+ }
+ if (addMethodsToMetaClass) {
+ registerMethodMissingForTags(mc, gspTagLibraryLookup, namespace, name)
+ }
+ return captureTagOutputForMethodCall(gspTagLibraryLookup, namespace, name, attrs, body)
+ }
MetaClass tagBeanMc = tagBean.getMetaClass()
final MetaMethod method = tagBeanMc.respondsTo(tagBean, name, args).find { it }
if (method != null) {
diff --git a/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagMethodContext.java b/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagMethodContext.java
new file mode 100644
index 00000000000..f521c56d2d6
--- /dev/null
+++ b/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagMethodContext.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.grails.taglib;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Map;
+
+import groovy.lang.Closure;
+
+public final class TagMethodContext {
+
+ private static final ThreadLocal> CONTEXT_STACK = ThreadLocal.withInitial(ArrayDeque::new);
+
+ private TagMethodContext() {
+ }
+
+ public static void push(Map, ?> attrs, Closure> body) {
+ CONTEXT_STACK.get().push(new TagMethodContextEntry(attrs, body));
+ }
+
+ public static void pop() {
+ Deque stack = CONTEXT_STACK.get();
+ if (!stack.isEmpty()) {
+ stack.pop();
+ }
+ if (stack.isEmpty()) {
+ CONTEXT_STACK.remove();
+ }
+ }
+
+ public static Map, ?> currentAttrs() {
+ Deque stack = CONTEXT_STACK.get();
+ return stack.isEmpty() ? null : stack.peek().attrs();
+ }
+
+ public static Closure> currentBody() {
+ Deque stack = CONTEXT_STACK.get();
+ return stack.isEmpty() ? null : stack.peek().body();
+ }
+
+ private record TagMethodContextEntry(Map, ?> attrs, Closure> body) { }
+}
diff --git a/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagMethodInvoker.java b/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagMethodInvoker.java
new file mode 100644
index 00000000000..27b5720faef
--- /dev/null
+++ b/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagMethodInvoker.java
@@ -0,0 +1,218 @@
+/*
+ * 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.taglib;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Parameter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import groovy.lang.Closure;
+import groovy.lang.GroovyObject;
+import groovy.lang.MissingMethodException;
+
+public final class TagMethodInvoker {
+
+ /**
+ * Method names inherited from framework traits and interfaces that must never
+ * be treated as tag methods. These come from {@code TagLibrary},
+ * {@code TagLibraryInvoker}, {@code WebAttributes}, {@code ServletAttributes},
+ * and related Spring interfaces.
+ */
+ private static final Set FRAMEWORK_METHOD_NAMES = Set.of(
+ "initializeTagLibrary",
+ "raw",
+ "throwTagError",
+ "withCodec",
+ "currentRequestAttributes"
+ );
+
+ private static final ClassValue