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 * <g:include controller="foo" action="test"></g:include><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 * <g:applyLayout name="myLayout">some text</g:applyLayout><br/> 102 * <g:applyLayout name="myLayout" template="mytemplate" /><br/> 103 * <g:applyLayout name="myLayout" url="http://www.google.com" /><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 * <g:pageProperty default="defaultValue" name="body.onload" /><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 * <g:ifPageProperty name="meta.index">body to invoke</g:ifPageProperty><br/> 231 * 232 * or it equals a certain value:<br/> 233 * 234 * <g:ifPageProperty name="meta.index" equals="blah">body to invoke</g:ifPageProperty> 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 * <g:layoutTitle default="The Default title" /> 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 * <g:layoutBody /> 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 * <g:layoutHead /> 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 * <g:paginate total="${Account.count()}" /><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 * <g:sortableColumn property="title" title="Title" /><br/> 434 * <g:sortableColumn property="title" title="Title" style="width: 200px" /><br/> 435 * <g:sortableColumn property="title" titleKey="book.title" /><br/> 436 * <g:sortableColumn property="releaseDate" defaultOrder="desc" title="Release Date" /><br/> 437 * <g:sortableColumn property="releaseDate" defaultOrder="desc" title="Release Date" titleKey="book.releaseDate" /><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 * <g:render template="atemplate" collection="${users}" /><br/> 511 * <g:render template="atemplate" model="[user:user,company:company]" /><br/> 512 * <g:render template="atemplate" bean="${user}" /><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