/
ApplicationTagLib.groovy
458 lines (410 loc) · 17.4 KB
/
ApplicationTagLib.groovy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
/*
* Copyright 2004-2005 the original author or authors.
*
* 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
*
* http://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.codehaus.groovy.grails.plugins.web.taglib
import grails.artefact.Artefact
import grails.util.GrailsUtil
import grails.util.Metadata
import groovy.transform.CompileStatic
import org.apache.commons.io.FilenameUtils
import org.codehaus.groovy.grails.commons.GrailsApplication
import org.codehaus.groovy.grails.plugins.GrailsPluginManager
import org.codehaus.groovy.grails.plugins.support.aware.GrailsApplicationAware
import org.codehaus.groovy.grails.web.mapping.LinkGenerator
import org.codehaus.groovy.grails.web.mapping.UrlMapping
import org.codehaus.groovy.grails.web.mapping.UrlMappingsHolder
import org.codehaus.groovy.grails.web.servlet.mvc.GrailsWebRequest
import org.codehaus.groovy.runtime.InvokerHelper
import org.springframework.beans.factory.InitializingBean
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.ApplicationContextAware
import org.springframework.web.servlet.support.RequestDataValueProcessor
/**
* The base application tag library for Grails many of which take inspiration from Rails helpers (thanks guys! :)
* This tag library tends to get extended by others as tags within here can be re-used in said libraries
*
* @author Graeme Rocher
*/
@Artefact("TagLibrary")
class ApplicationTagLib implements ApplicationContextAware, InitializingBean, GrailsApplicationAware {
static returnObjectForTags = ['createLink', 'resource', 'createLinkTo', 'cookie', 'header', 'img', 'join', 'meta', 'set', 'applyCodec']
ApplicationContext applicationContext
GrailsPluginManager pluginManager
GrailsApplication grailsApplication
UrlMappingsHolder grailsUrlMappingsHolder
@Autowired
LinkGenerator linkGenerator
RequestDataValueProcessor requestDataValueProcessor
static final SCOPES = [page: 'pageScope',
application: 'servletContext',
request:'request',
session:'session',
flash:'flash']
boolean useJsessionId = false
boolean hasResourceProcessor = false
void afterPropertiesSet() {
def config = grailsApplication.config
if (config.grails.views.enable.jsessionid instanceof Boolean) {
useJsessionId = config.grails.views.enable.jsessionid
}
hasResourceProcessor = applicationContext.containsBean('grailsResourceProcessor')
if (applicationContext.containsBean('requestDataValueProcessor')) {
requestDataValueProcessor = applicationContext.getBean('requestDataValueProcessor', RequestDataValueProcessor)
}
}
/**
* Obtains the value of a cookie.
*
* @emptyTag
*
* @attr name REQUIRED the cookie name
*/
Closure cookie = { attrs ->
request.cookies.find { it.name == attrs.name }?.value
}
/**
* Renders the specified request header value.
*
* @emptyTag
*
* @attr name REQUIRED the header name
*/
Closure header = { attrs ->
attrs.name ? request.getHeader(attrs.name) : null
}
/**
* Sets a variable in the pageContext or the specified scope.
* The value can be specified directly or can be a bean retrieved from the applicationContext.
*
* @attr var REQUIRED the variable name
* @attr value the variable value; if not specified uses the rendered body
* @attr bean the name or the type of a bean in the applicationContext; the type can be an interface or superclass
* @attr scope the scope name; defaults to pageScope
*/
Closure set = { attrs, body ->
def var = attrs.var
if (!var) throw new IllegalArgumentException("[var] attribute must be specified to for <g:set>!")
def scope = attrs.scope ? SCOPES[attrs.scope] : 'pageScope'
if (!scope) throw new IllegalArgumentException("Invalid [scope] attribute for tag <g:set>!")
def value
if (attrs.bean) {
value = applicationContext.getBean(attrs.bean)
} else {
value = attrs.value
def containsValue = attrs.containsKey('value')
if (!containsValue && body) value = body()
}
this."$scope"."$var" = value
null
}
/**
* Creates a link to a resource, generally used as a method rather than a tag.<br/>
*
* eg. <link type="text/css" href="${createLinkTo(dir:'css',file:'main.css')}" />
*
* @emptyTag
*/
Closure createLinkTo = { attrs ->
GrailsUtil.deprecated "Tag [createLinkTo] is deprecated please use [resource] instead"
return resource(attrs)
}
/**
* Creates a link to a resource, generally used as a method rather than a tag.<br/>
*
* eg. <link type="text/css" href="${resource(dir:'css',file:'main.css')}" />
*
* @emptyTag
*
* @attr base Sets the prefix to be added to the link target address, typically an absolute server URL. This overrides the behaviour of the absolute property, if both are specified.x≈
* @attr contextPath the context path to use (relative to the application context path). Defaults to "" or path to the plugin for a plugin view or template.
* @attr dir the name of the directory within the grails app to link to
* @attr file the name of the file within the grails app to link to
* @attr absolute If set to "true" will prefix the link target address with the value of the grails.serverURL property from Config, or http://localhost:<port> if no value in Config and not running in production.
* @attr plugin The plugin to look for the resource in
*/
Closure resource = { attrs ->
if (!attrs.pluginContextPath && pageScope.pluginContextPath) {
attrs.pluginContextPath = pageScope.pluginContextPath
}
// Use resources plugin if present, but only if file is specified - resources require files
// But users often need to link to a folder just using dir
def url = (hasResourceProcessor && attrs.file) ? r.resource(attrs) : linkGenerator.resource(attrs)
return url ? processedUrl("$url", request) : url
}
/**
* Render an img tag with src set to a static resource
* @attr dir Optional name of resource directory, defaults to "images"
* @attr file Name of resource file (optional if uri specified)
* @attr plugin Optional the name of the grails plugin if the resource is not part of the application
* @attr uri Optional app-relative URI path of the resource if not using dir/file attributes - only if Resources plugin is in use
*/
Closure img = { attrs ->
if (!attrs.uri && !attrs.dir) {
attrs.dir = "images"
}
if (hasResourceProcessor) {
return r.img(attrs)
}
def uri = attrs.uri ? processedUrl(attrs.uri, request) : resource(attrs)
def excludes = ['dir', 'uri', 'file', 'plugin']
def attrsAsString = attrsToString(attrs.findAll { !(it.key in excludes) })
def imgSrc = uri.encodeAsHTML()
return "<img src=\"${imgSrc}\"${attrsAsString} />"
}
/**
* General linking to controllers, actions etc. Examples:<br/>
*
* <g:link action="myaction">link 1</gr:link><br/>
* <g:link controller="myctrl" action="myaction">link 2</gr:link><br/>
*
* @attr controller The name of the controller to use in the link, if not specified the current controller will be linked
* @attr action The name of the action to use in the link, if not specified the default action will be linked
* @attr uri relative URI
* @attr url A map containing the action,controller,id etc.
* @attr base Sets the prefix to be added to the link target address, typically an absolute server URL. This overrides the behaviour of the absolute property, if both are specified.
* @attr absolute If set to "true" will prefix the link target address with the value of the grails.serverURL property from Config, or http://localhost:<port> if no value in Config and not running in production.
* @attr id The id to use in the link
* @attr fragment The link fragment (often called anchor tag) to use
* @attr params A map containing URL query parameters
* @attr mapping The named URL mapping to use to rewrite the link
* @attr event Webflow _eventId parameter
* @attr elementId DOM element id
*/
Closure link = { attrs, body ->
def writer = getOut()
def elementId = attrs.remove('elementId')
def linkAttrs
if (attrs.params instanceof Map && attrs.params.containsKey('attrs')) {
linkAttrs = attrs.params.remove('attrs').clone()
}
else {
linkAttrs = [:]
}
writer << '<a href=\"'
writer << createLink(attrs).encodeAsHTML()
writer << '"'
if (elementId) {
writer << " id=\"${elementId}\""
}
attrs.remove(UrlMapping.PLUGIN)
attrs.remove(UrlMapping.NAMESPACE)
def remainingKeys = attrs.keySet() - LinkGenerator.LINK_ATTRIBUTES
for (key in remainingKeys) {
writer << " " << key << "=\"" << attrs[key]?.encodeAsHTML() << "\""
}
for (entry in linkAttrs) {
writer << " " << entry.key << "=\"" << entry.value?.encodeAsHTML() << "\""
}
writer << '>'
writer << body()
writer << '</a>'
}
@CompileStatic
static String attrsToString(Map attrs) {
// Output any remaining user-specified attributes
StringBuilder sb=new StringBuilder()
// For some strange reason Groovy creates ClassCastExceptions internally in PogoMetaMethodSite.checkCall without this hack
for (Iterator i = InvokerHelper.asIterator(attrs); i.hasNext();) {
Map.Entry e = (Map.Entry)i.next()
if (e.value != null) {
sb.append(' ')
sb.append(e.key)
sb.append('="')
sb.append(InvokerHelper.invokeMethod(String.valueOf(e.value), "encodeAsHTML", null))
sb.append('"')
}
}
return sb.toString()
}
static LINK_WRITERS = [
js: { url, constants, attrs ->
return "<script src=\"${url}\"${getAttributesToRender(constants, attrs)}></script>"
},
link: { url, constants, attrs ->
return "<link href=\"${url}\"${getAttributesToRender(constants, attrs)}/>"
}
]
static getAttributesToRender(constants, attrs) {
StringBuilder sb = new StringBuilder()
if (constants) {
sb.append(attrsToString(constants))
}
if (attrs) {
sb.append(attrsToString(attrs))
}
return sb.toString()
}
static SUPPORTED_TYPES = [
css:[type:"text/css", rel:'stylesheet', media:'screen, projector'],
js:[type:'text/javascript', writer:'js'],
gif:[rel:'shortcut icon'],
jpg:[rel:'shortcut icon'],
png:[rel:'shortcut icon'],
ico:[rel:'shortcut icon'],
appleicon:[rel:'apple-touch-icon']
// @todo add feed link types here too
]
/**
* Render the appropriate kind of external link for use in <head> based on the type of the URI.
* For JS will render <script> tags, for CSS will render <link> with the correct rel, and so on for icons.
* @attr uri
* @attr dir
* @attr file
* @attr plugin
* @attr type
*/
Closure external = { attrs ->
if (!attrs.uri) {
attrs.uri = resource(attrs).toString()
}
renderResourceLink(attrs)
}
/**
*
* @attr uri
* @attr type
*/
protected renderResourceLink(attrs) {
def uri = attrs.remove('uri')
def type = attrs.remove('type')
if (!type) {
type = FilenameUtils.getExtension(uri)
}
def typeInfo = SUPPORTED_TYPES[type]?.clone()
if (!typeInfo) {
throwTagError "I can't work out the type of ${uri} with type [${type}]. Please check the URL, resource definition or specify [type] attribute"
}
def writerName = typeInfo.remove('writer')
def writer = LINK_WRITERS[writerName ?: 'link']
// Allow attrs to overwrite any constants
attrs.each { typeInfo.remove(it.key) }
out << writer(processedUrl(uri, request), typeInfo, attrs)
out << "\r\n"
}
/**
* Creates a grails application link from a set of attributes. This
* link can then be included in links, ajax calls etc. Generally used as a method call
* rather than a tag eg.<br/>
*
* <a href="${createLink(action:'list')}">List</a>
*
* @emptyTag
*
* @attr controller The name of the controller to use in the link, if not specified the current controller will be linked
* @attr action The name of the action to use in the link, if not specified the default action will be linked
* @attr uri relative URI
* @attr url A map containing the action,controller,id etc.
* @attr base Sets the prefix to be added to the link target address, typically an absolute server URL. This overrides the behaviour of the absolute property, if both are specified.
* @attr absolute If set to "true" will prefix the link target address with the value of the grails.serverURL property from Config, or http://localhost:<port> if no value in Config and not running in production.
* @attr id The id to use in the link
* @attr fragment The link fragment (often called anchor tag) to use
* @attr params A map containing URL query parameters
* @attr mapping The named URL mapping to use to rewrite the link
* @attr event Webflow _eventId parameter
*/
Closure createLink = { attrs ->
def urlAttrs = attrs
if (attrs.url instanceof Map) {
urlAttrs = attrs.url
}
def params = urlAttrs.params && urlAttrs.params instanceof Map ? urlAttrs.params : [:]
if (request.flowExecutionKey) {
params.execution = request.flowExecutionKey
urlAttrs.params = params
if (attrs.controller == null && attrs.action == null && attrs.url == null && attrs.uri == null) {
urlAttrs[LinkGenerator.ATTRIBUTE_ACTION] = GrailsWebRequest.lookup().actionName
}
}
if (urlAttrs.event) {
params."_eventId" = urlAttrs.remove('event')
urlAttrs.params = params
}
String generatedLink = linkGenerator.link(urlAttrs, request.characterEncoding)
generatedLink = processedUrl(generatedLink, request)
return useJsessionId ? response.encodeURL(generatedLink) : generatedLink
}
/**
* Helper method for creating tags called like:<br/>
* <pre>
* withTag(name:'script',attrs:[type:'text/javascript']) {
* ...
* }
* </pre>
* @attr name REQUIRED the tag name
* @attr attrs tag attributes
*/
Closure withTag = { attrs, body ->
def writer = out
writer << "<${attrs.name}"
attrs.attrs?.each { k,v ->
if (!v) return
if (v instanceof Closure) {
writer << " $k=\""
v()
writer << '"'
}
else {
writer << " $k=\"$v\""
}
}
writer << '>'
writer << body()
writer << "</${attrs.name}>"
}
/**
* Uses the Groovy JDK join method to concatenate the toString() representation of each item
* in this collection with the given separator.
*
* @emptyTag
*
* @attr REQUIRED in The collection to iterate over
* @attr delimiter The value of the delimiter to use during the join. If no delimiter is specified then ", " (a comma followed by a space) will be used as the delimiter.
*/
Closure join = { attrs ->
def collection = attrs.'in'
if (collection == null) {
throwTagError('Tag ["join"] missing required attribute ["in"]')
}
def delimiter = attrs.delimiter == null ? ', ' : attrs.delimiter
return collection.join(delimiter)
}
/**
* Output application metadata that is loaded from application.properties.
*
* @emptyTag
*
* @attr name REQUIRED the metadata key
*/
Closure meta = { attrs ->
if (!attrs.name) {
throwTagError('Tag ["meta"] missing required attribute ["name"]')
}
return Metadata.current[attrs.name]
}
/**
* Filters the url through the RequestDataValueProcessor bean if it is registered.
*/
String processedUrl(String link, request) {
if (requestDataValueProcessor == null) {
return link
}
return requestDataValueProcessor.processUrl(request, link)
}
Closure applyCodec = { Map attrs, Closure body ->
// encoding is handled in GroovyPage.invokeTag and GroovyPage.captureTagOutput
body()
}
}