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 */
15package org.codehaus.groovy.grails.plugins.web.taglib
16
17import org.springframework.web.servlet.support.RequestContextUtils as RCU
18
19import com.opensymphony.module.sitemesh.Factory
20import com.opensymphony.module.sitemesh.RequestConstants
21
22import grails.util.Environment
23import grails.util.GrailsNameUtils
24
25import groovy.text.Template
26
27import java.util.concurrent.ConcurrentHashMap
28import javax.servlet.ServletConfig
29
30import org.codehaus.groovy.grails.plugins.GrailsPluginManager
31import org.codehaus.groovy.grails.commons.GrailsDomainClass
32import org.codehaus.groovy.grails.scaffolding.GrailsTemplateGenerator
33import org.codehaus.groovy.grails.web.mapping.ForwardUrlMappingInfo
34import org.codehaus.groovy.grails.web.metaclass.ControllerDynamicMethods
35import org.codehaus.groovy.grails.web.pages.GroovyPage
36import org.codehaus.groovy.grails.web.pages.GroovyPageMetaInfo
37import org.codehaus.groovy.grails.web.pages.GroovyPagesTemplateEngine
38import org.codehaus.groovy.grails.web.servlet.GrailsApplicationAttributes
39import org.codehaus.groovy.grails.web.servlet.mvc.GrailsWebRequest
40import org.codehaus.groovy.grails.web.sitemesh.FactoryHolder
41import org.codehaus.groovy.grails.web.sitemesh.GSPSitemeshPage
42import org.codehaus.groovy.grails.web.sitemesh.GrailsPageFilter
43import org.codehaus.groovy.grails.web.util.StreamCharBuffer
44import org.codehaus.groovy.grails.web.util.WebUtils
45
46/**
47 * Tags to help rendering of views and layouts.
48 *
49 * @author Graeme Rocher
50 */
51class RenderTagLib implements RequestConstants {
52
53    def out // to facilitate testing
54
55    ServletConfig servletConfig
56    GroovyPagesTemplateEngine groovyPagesTemplateEngine
57    GrailsPluginManager pluginManager
58    GrailsTemplateGenerator scaffoldingTemplateGenerator
59    Map scaffoldedActionMap
60    Map controllerToScaffoldedDomainClassMap
61
62    static Map TEMPLATE_CACHE = new ConcurrentHashMap()
63
64    protected getPage() {
65        return request[PAGE]
66    }
67
68    /**
69     * Includes another controller/action within the current response.<br/>
70     *
71     * &lt;g:include controller="foo" action="test"&gt;&lt;/g:include&gt;<br/>
72     *
73     * @attr controller The name of the controller
74     * @attr action The name of the action
75     * @attr id The identifier
76     * @attr params Any parameters
77     * @attr view The name of the view. Cannot be specified in combination with controller/action/id
78     * @attr model A model to pass onto the included controller in the request
79     */
80    def include = { attrs, body ->
81        if (attrs.action && !attrs.controller) {
82            def controller = request?.getAttribute(GrailsApplicationAttributes.CONTROLLER)
83            def controllerName = controller?.getProperty(ControllerDynamicMethods.CONTROLLER_NAME_PROPERTY)
84            attrs.controller = controllerName
85        }
86
87        if (attrs.controller || attrs.view) {
88            def mapping = new ForwardUrlMappingInfo(controller: attrs.controller,
89                                                    action: attrs.action,
90                                                    view: attrs.view,
91                                                    id: attrs.id,
92                                                    params: attrs.params)
93
94            out << WebUtils.includeForUrlMappingInfo(request, response, mapping, attrs.model ?: [:])?.content
95        }
96    }
97
98    /**
99     * Apply a layout to a particular block of text or to the given view or template.<br/>
100     *
101     * &lt;g:applyLayout name="myLayout"&gt;some text&lt;/g:applyLayout&gt;<br/>
102     * &lt;g:applyLayout name="myLayout" template="mytemplate" /&gt;<br/>
103     * &lt;g:applyLayout name="myLayout" url="http://www.google.com" /&gt;<br/>
104     *
105     * @attr name The name of the layout
106     * @attr template Optional. The template to apply the layout to
107     * @attr url Optional. The URL to retrieve the content from and apply a layout to
108     * @attr contentType Optional. The content type to use, default is "text/html"
109     * @attr encoding Optional. The encoding to use
110     * @attr params Optiona. The params to pass onto the page object
111     */
112    def applyLayout = { attrs, body ->
113        if (!groovyPagesTemplateEngine) throw new IllegalStateException("Property [groovyPagesTemplateEngine] must be set!")
114        def oldPage = getPage()
115        def contentType = attrs.contentType ? attrs.contentType : "text/html"
116
117        def content = ""
118        GSPSitemeshPage gspSiteMeshPage = null
119        if (attrs.url) {
120            content = new URL(attrs.url).text
121        }
122        else {
123            def oldGspSiteMeshPage = request.getAttribute(GrailsPageFilter.GSP_SITEMESH_PAGE)
124            try {
125                gspSiteMeshPage = new GSPSitemeshPage()
126                request.setAttribute(GrailsPageFilter.GSP_SITEMESH_PAGE, gspSiteMeshPage)
127                if (attrs.view || attrs.template) {
128                    content = render(attrs)
129                }
130                else {
131                    def bodyClosure = GroovyPage.createOutputCapturingClosure(this, body, webRequest, true)
132                    content = bodyClosure()
133                }
134                if (content instanceof StreamCharBuffer) {
135                    gspSiteMeshPage.setPageBuffer(content)
136                }
137                else if (content != null) {
138                    def buf = new StreamCharBuffer()
139                    buf.writer.write(content)
140                    gspSiteMeshPage.setPageBuffer(buf)
141                }
142            }
143            finally {
144                request.setAttribute(GrailsPageFilter.GSP_SITEMESH_PAGE, oldGspSiteMeshPage)
145            }
146        }
147
148        def page = null
149        if (gspSiteMeshPage != null && gspSiteMeshPage.isUsed()) {
150            page = gspSiteMeshPage
151        }
152        else {
153            def parser = getFactory().getPageParser(contentType)
154            page = parser.parse(content.toCharArray())
155        }
156
157        attrs.params.each { k, v->
158            page.addProperty(k, v?.toString())
159        }
160        def decoratorMapper = getFactory().getDecoratorMapper()
161
162        if (decoratorMapper) {
163            def d = decoratorMapper.getNamedDecorator(request, attrs.name)
164            if (d && d.page) {
165                try {
166                    request[PAGE] = page
167                    def t = groovyPagesTemplateEngine.createTemplate(d.getPage())
168                    def w = t.make()
169                    w.writeTo(out)
170                }
171                finally {
172                    request[PAGE] = oldPage
173                }
174            }
175        }
176    }
177
178    private Factory getFactory() {
179        return FactoryHolder.getFactory()
180    }
181
182    /**
183     * Used to retrieve a property of the decorated page.<br/>
184     *
185     * &lt;g:pageProperty default="defaultValue" name="body.onload" /&gt;<br/>
186     *
187     * @attr REQUIRED name the property name
188     * @attr default the default value to use if the property is null
189     * @attr writeEntireProperty if true, writes the property in the form 'foo = "bar"', otherwise renders 'bar'
190     */
191    def pageProperty = { attrs ->
192        if (!attrs.name) {
193            throwTagError("Tag [pageProperty] is missing required attribute [name]")
194        }
195
196        def propertyName = attrs.name
197        def htmlPage = getPage()
198        def propertyValue
199
200        if (htmlPage instanceof GSPSitemeshPage) {
201            // check if there is an component content buffer
202            propertyValue = htmlPage.getContentBuffer(propertyName)
203        }
204
205        if (!propertyValue) {
206            propertyValue = htmlPage.getProperty(propertyName)
207        }
208
209        if (!propertyValue) {
210            propertyValue = attrs.'default'
211        }
212
213        if (propertyValue) {
214            if (attrs.writeEntireProperty) {
215                out << ' '
216                out << propertyName.substring(propertyName.lastIndexOf('.') + 1)
217                out << "=\""
218                out << propertyValue
219                out << "\""
220            }
221            else {
222                out << propertyValue
223            }
224        }
225    }
226
227    /**
228     * Invokes the body of this tag if the page property exists:<br/>
229     *
230     * &lt;g:ifPageProperty name="meta.index"&gt;body to invoke&lt;/g:ifPageProperty&gt;<br/>
231     *
232     * or it equals a certain value:<br/>
233     *
234     * &lt;g:ifPageProperty name="meta.index" equals="blah"&gt;body to invoke&lt;/g:ifPageProperty&gt;
235     *
236     * @attr name REQUIRED the property name
237     * @attr equals optional value to test against
238     */
239    def ifPageProperty = { attrs, body ->
240        if (!attrs.name) {
241            return
242        }
243
244        def htmlPage = getPage()
245        def names = ((attrs.name instanceof List) ? attrs.name : [attrs.name])
246
247        def invokeBody = true
248        for (i in 0..<names.size()) {
249            String propertyValue = htmlPage.getProperty(names[i])
250            if (propertyValue) {
251                if (attrs.equals instanceof List) {
252                    invokeBody = attrs.equals[i] == propertyValue
253                }
254                else if (attrs.equals instanceof String) {
255                    invokeBody = attrs.equals == propertyValue
256                }
257            }
258            else {
259                invokeBody = false
260                break
261            }
262        }
263        if (invokeBody) {
264            out << body()
265        }
266    }
267
268    /**
269     * Used in layouts to render the page title from the SiteMesh page.<br/>
270     *
271     * &lt;g:layoutTitle default="The Default title" /&gt;
272     *
273     * @attr default the value to use if the title isn't specified in the GSP
274     */
275    def layoutTitle = { attrs ->
276        String title = page.title
277        if (!title && attrs.'default') title = attrs.'default'
278        if (title) out << title
279    }
280
281    /**
282     * Used in layouts to render the body of a SiteMesh layout.<br/>
283     *
284     * &lt;g:layoutBody /&gt;
285     */
286    def layoutBody = { attrs ->
287        getPage().writeBody(out)
288    }
289
290    /**
291     * Used in layouts to render the head of a SiteMesh layout.<br/>
292     *
293     * &lt;g:layoutHead /&gt;
294     */
295    def layoutHead = { attrs ->
296        getPage().writeHead(out)
297    }
298
299    /**
300     * Creates next/previous links to support pagination for the current controller.<br/>
301     *
302     * &lt;g:paginate total="${Account.count()}" /&gt;<br/>
303     *
304     * @attr total REQUIRED The total number of results to paginate
305     * @attr action the name of the action to use in the link, if not specified the default action will be linked
306     * @attr controller the name of the controller to use in the link, if not specified the current controller will be linked
307     * @attr id The id to use in the link
308     * @attr params A map containing request parameters
309     * @attr prev The text to display for the previous link (defaults to "Previous" as defined by default.paginate.prev property in I18n messages.properties)
310     * @attr next The text to display for the next link (defaults to "Next" as defined by default.paginate.next property in I18n messages.properties)
311     * @attr max The number of records displayed per page (defaults to 10). Used ONLY if params.max is empty
312     * @attr maxsteps The number of steps displayed for pagination (defaults to 10). Used ONLY if params.maxsteps is empty
313     * @attr offset Used only if params.offset is empty
314     * @attr fragment The link fragment (often called anchor tag) to use
315     */
316    def paginate = { attrs ->
317        def writer = out
318        if (attrs.total == null) {
319            throwTagError("Tag [paginate] is missing required attribute [total]")
320        }
321
322        def messageSource = grailsAttributes.messageSource
323        def locale = RCU.getLocale(request)
324
325        def total = attrs.int('total') ?: 0
326        def action = (attrs.action ? attrs.action : (params.action ? params.action : "list"))
327        def offset = params.int('offset') ?: 0
328        def max = params.int('max')
329        def maxsteps = (attrs.int('maxsteps') ?: 10)
330
331        if (!offset) offset = (attrs.int('offset') ?: 0)
332        if (!max) max = (attrs.int('max') ?: 10)
333
334        def linkParams = [:]
335        if (attrs.params) linkParams.putAll(attrs.params)
336        linkParams.offset = offset - max
337        linkParams.max = max
338        if (params.sort) linkParams.sort = params.sort
339        if (params.order) linkParams.order = params.order
340
341        def linkTagAttrs = [action:action]
342        if (attrs.controller) {
343            linkTagAttrs.controller = attrs.controller
344        }
345        if (attrs.id != null) {
346            linkTagAttrs.id = attrs.id
347        }
348        if (attrs.fragment != null) {
349            linkTagAttrs.fragment = attrs.fragment
350        }
351        linkTagAttrs.params = linkParams
352
353        // determine paging variables
354        def steps = maxsteps > 0
355        int currentstep = (offset / max) + 1
356        int firststep = 1
357        int laststep = Math.round(Math.ceil(total / max))
358
359        // display previous link when not on firststep
360        if (currentstep > firststep) {
361            linkTagAttrs.class = 'prevLink'
362            linkParams.offset = offset - max
363            writer << link(linkTagAttrs.clone()) {
364                (attrs.prev ?: messageSource.getMessage('paginate.prev', null, messageSource.getMessage('default.paginate.prev', null, 'Previous', locale), locale))
365            }
366        }
367
368        // display steps when steps are enabled and laststep is not firststep
369        if (steps && laststep > firststep) {
370            linkTagAttrs.class = 'step'
371
372            // determine begin and endstep paging variables
373            int beginstep = currentstep - Math.round(maxsteps / 2) + (maxsteps % 2)
374            int endstep = currentstep + Math.round(maxsteps / 2) - 1
375
376            if (beginstep < firststep) {
377                beginstep = firststep
378                endstep = maxsteps
379            }
380            if (endstep > laststep) {
381                beginstep = laststep - maxsteps + 1
382                if (beginstep < firststep) {
383                    beginstep = firststep
384                }
385                endstep = laststep
386            }
387
388            // display firststep link when beginstep is not firststep
389            if (beginstep > firststep) {
390                linkParams.offset = 0
391                writer << link(linkTagAttrs.clone()) {firststep.toString()}
392                writer << '<span class="step">..</span>'
393            }
394
395            // display paginate steps
396            (beginstep..endstep).each { i ->
397                if (currentstep == i) {
398                    writer << "<span class=\"currentStep\">${i}</span>"
399                }
400                else {
401                    linkParams.offset = (i - 1) * max
402                    writer << link(linkTagAttrs.clone()) {i.toString()}
403                }
404            }
405
406            // display laststep link when endstep is not laststep
407            if (endstep < laststep) {
408                writer << '<span class="step">..</span>'
409                linkParams.offset = (laststep -1) * max
410                writer << link(linkTagAttrs.clone()) { laststep.toString() }
411            }
412        }
413
414        // display next link when not on laststep
415        if (currentstep < laststep) {
416            linkTagAttrs.class = 'nextLink'
417            linkParams.offset = offset + max
418            writer << link(linkTagAttrs.clone()) {
419                (attrs.next ? attrs.next : messageSource.getMessage('paginate.next', null, messageSource.getMessage('default.paginate.next', null, 'Next', locale), locale))
420            }
421        }
422    }
423
424    /**
425     * Renders a sortable column to support sorting in list views.<br/>
426     *
427     * Attribute title or titleKey is required. When both attributes are specified then titleKey takes precedence,
428     * resulting in the title caption to be resolved against the message source. In case when the message could
429     * not be resolved, the title will be used as title caption.<br/>
430     *
431     * Examples:<br/>
432     *
433     * &lt;g:sortableColumn property="title" title="Title" /&gt;<br/>
434     * &lt;g:sortableColumn property="title" title="Title" style="width: 200px" /&gt;<br/>
435     * &lt;g:sortableColumn property="title" titleKey="book.title" /&gt;<br/>
436     * &lt;g:sortableColumn property="releaseDate" defaultOrder="desc" title="Release Date" /&gt;<br/>
437     * &lt;g:sortableColumn property="releaseDate" defaultOrder="desc" title="Release Date" titleKey="book.releaseDate" /&gt;<br/>
438     *
439     * @attr property - name of the property relating to the field
440     * @attr defaultOrder default order for the property; choose between asc (default if not provided) and desc
441     * @attr title title caption for the column
442     * @attr titleKey title key to use for the column, resolved against the message source
443     * @attr params a map containing request parameters
444     * @attr action the name of the action to use in the link, if not specified the list action will be linked
445     * @attr params A map containing URL query parameters
446     * @attr class CSS class name
447     */
448    def sortableColumn = { attrs ->
449        def writer = out
450        if (!attrs.property) {
451            throwTagError("Tag [sortableColumn] is missing required attribute [property]")
452        }
453
454        if (!attrs.title && !attrs.titleKey) {
455            throwTagError("Tag [sortableColumn] is missing required attribute [title] or [titleKey]")
456        }
457
458        def property = attrs.remove("property")
459        def action = attrs.action ? attrs.remove("action") : (actionName ?: "list")
460
461        def defaultOrder = attrs.remove("defaultOrder")
462        if (defaultOrder != "desc") defaultOrder = "asc"
463
464        // current sorting property and order
465        def sort = params.sort
466        def order = params.order
467
468        // add sorting property and params to link params
469        def linkParams = [:]
470        if (params.id) linkParams.put("id",params.id)
471        if (attrs.params) linkParams.putAll(attrs.remove("params"))
472        linkParams.sort = property
473
474        // determine and add sorting order for this column to link params
475        attrs.class = (attrs.class ? "${attrs.class} sortable" : "sortable")
476        if (property == sort) {
477            attrs.class = attrs.class + " sorted " + order
478            if (order == "asc") {
479                linkParams.order = "desc"
480            }
481            else {
482                linkParams.order = "asc"
483            }
484        }
485        else {
486            linkParams.order = defaultOrder
487        }
488
489        // determine column title
490        def title = attrs.remove("title")
491        def titleKey = attrs.remove("titleKey")
492        if (titleKey) {
493            if (!title) title = titleKey
494            def messageSource = grailsAttributes.messageSource
495            def locale = RCU.getLocale(request)
496            title = messageSource.getMessage(titleKey, null, title, locale)
497        }
498
499        writer << "<th "
500        // process remaining attributes
501        attrs.each { k, v ->
502            writer << "${k}=\"${v.encodeAsHTML()}\" "
503        }
504        writer << ">${link(action:action, params:linkParams) { title }}</th>"
505    }
506
507    /**
508     * Renders a template inside views for collections, models and beans. Examples:<br/>
509     *
510     * &lt;g:render template="atemplate" collection="${users}" /&gt;<br/>
511     * &lt;g:render template="atemplate" model="[user:user,company:company]" /&gt;<br/>
512     * &lt;g:render template="atemplate" bean="${user}" /&gt;<br/>
513     *
514     * @attr template REQUIRED The name of the template to apply
515     * @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.
516     * @attr bean The bean to apply the template against
517     * @attr model The model to apply the template against as a java.util.Map
518     * @attr collection A collection of model objects to apply the template to
519     * @attr var The variable name of the bean to be referenced in the template
520     * @attr plugin The plugin to look for the template in
521     */
522    def render = { attrs, body ->
523        if (!groovyPagesTemplateEngine) {
524            throw new IllegalStateException("Property [groovyPagesTemplateEngine] must be set!")
525        }
526
527        if (!attrs.template) {
528            throwTagError("Tag [render] is missing required attribute [template]")
529        }
530
531        def engine = groovyPagesTemplateEngine
532        def uri = grailsAttributes.getTemplateUri(attrs.template, request)
533        def var = attrs.var
534
535        Template t
536
537        def contextPath = attrs.contextPath ? attrs.contextPath : null
538        def pluginName = attrs.plugin
539        def pluginContextFromPagescope = false
540        if (pluginName) {
541            contextPath = pluginManager?.getPluginPath(pluginName) ?: ''
542        }
543        else if (contextPath == null) {
544            if (uri.startsWith('/plugins/')) {
545                contextPath = ''
546            }
547            else {
548                contextPath = pageScope.pluginContextPath ?: ''
549                pluginContextFromPagescope = true
550            }
551        }
552
553        def templatePath = "${contextPath}${uri}".toString()
554        def cacheKey = "${templatePath}:${pluginContextFromPagescope}".toString()
555
556        def cached = TEMPLATE_CACHE[cacheKey]
557        if (cached instanceof Template) {
558            t = cached
559        }
560        else {
561            if (cached != null && System.currentTimeMillis() - cached.timestamp < GroovyPageMetaInfo.LASTMODIFIED_CHECK_INTERVAL) {
562                t = cached.template
563            }
564            else {
565                def templateResolveOrder
566                def templateInContextPath = "${contextPath}/grails-app/views${uri}"
567                if (pluginName) {
568                    templateResolveOrder = [templatePath, templateInContextPath]
569                }
570                else {
571                    templateResolveOrder = [uri, templatePath, templateInContextPath]
572                }
573                t = engine.createTemplateForUri(templateResolveOrder as String[])
574                if (!t && scaffoldingTemplateGenerator) {
575                    GrailsWebRequest webRequest = WebUtils.retrieveGrailsWebRequest()
576                    def controllerActions = scaffoldedActionMap[webRequest.controllerName]
577                    if (controllerActions?.contains(webRequest.actionName)) {
578                        GrailsDomainClass domainClass = controllerToScaffoldedDomainClassMap[webRequest.controllerName]
579                        if (domainClass) {
580                            int i = uri.lastIndexOf('/')
581                            String templateName = i > -1 ? uri.substring(i) : uri
582                            if (templateName.toLowerCase().endsWith('.gsp')) {
583                                templateName = templateName[0..-5]
584                            }
585                            def sw = new StringWriter()
586                            scaffoldingTemplateGenerator.generateView domainClass, templateName, sw
587                            t = engine.createTemplate(sw.toString(), uri)
588                        }
589                    }
590                }
591                if (t) {
592                    if (!engine.isReloadEnabled()) {
593                        def prevt = TEMPLATE_CACHE.putIfAbsent(cacheKey, t)
594                        if (prevt) {
595                            t = prevt
596                        }
597                    }
598                    else if (!Environment.isDevelopmentMode()) {
599                        TEMPLATE_CACHE.put(cacheKey, [timestamp: System.currentTimeMillis(), template: t])
600                    }
601                }
602            }
603        }
604
605        if (!t) {
606            throwTagError("Template not found for name [$attrs.template] and path [$uri]")
607        }
608
609        if (attrs.containsKey('bean')) {
610            def b = [body: body]
611            if (attrs.model instanceof Map) {
612                b += attrs.model
613            }
614            if (var) {
615                b.put(var, attrs.bean)
616            }
617            else {
618                b.put('it', attrs.bean)
619            }
620            t.make(b).writeTo(out)
621        }
622        else if (attrs.containsKey('collection')) {
623            def collection = attrs.collection
624            def key = 'it'
625            if (collection) {
626                def first = collection.iterator().next()
627                key = first ? GrailsNameUtils.getPropertyName(first.getClass()) : 'it'
628            }
629            collection.each {
630                def b = [body:body]
631                if (attrs.model instanceof Map) {
632                    b += attrs.model
633                }
634                if (var) {
635                    b.put(var, it)
636                }
637                else {
638                    b.put('it', it)
639                    b.put(key, it)
640                }
641                t.make(b).writeTo(out)
642            }
643        }
644        else if (attrs.model instanceof Map) {
645            t.make([body:body] + attrs.model).writeTo(out)
646        }
647        else if (attrs.template) {
648            t.make([body:body]).writeTo(out)
649        }
650    }
651}
652