/grails-plugin-gsp/src/main/groovy/org/codehaus/groovy/grails/plugins/web/taglib/ApplicationTagLib.groovy

https://github.com/continiouslearning/grails-core · Groovy · 448 lines · 307 code · 30 blank · 111 comment · 25 complexity · 3fbb6cb95dbd137f5467929f42a45825 MD5 · raw file

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