Loading...

Monday, March 5, 2012

Grails Goodness: Render GSP Views And Templates Outside Controllers

In a Grails application we use Groovy Server Pages (GSP) views and templates to create content we want to display in the web browser. Since Grails 2 we can use the groovyPageRenderer instance to render GSP views and templates outside a controller. For example we can render content in a service or other classes outside the scope of a web request.

The groovyPageRenderer instance is of type grails.gsp.PageRenderer and has a render() method like we use in a controller. We can pass a view or template name together with a model of data to be used in the GSP. The result is a String value.
The class also contains a renderTo() method that can be used to generate output to a Writer object. For example we can generate files with the output of the GSP rendering by passing a FileWriter instance to this method.

In the following sample we create a GSP view confirm.gsp and a GSP template _welcome.gsp. We also create a Grails service with two methods to use the GSP view and template.

%{-- File: grails-app/views/email/_welcome.gsp --}%
Hi, ${username}
%{-- File: grails-app/views/email/confirm.gsp --}%
<!doctype html>
<head>
    <title>Confirmation</title>
</head>

<body>

<h2><g:render template="/email/welcome" model="[username: username]"/></h2>

<p>
    Thank you for your registration.
</p>

</body>
</html>
// File: grails-app/services/page/renderer/RenderService.groovy
package page.renderer

import grails.gsp.PageRenderer

class RenderService {

    /**
     * Use the variable name groovyPageRenderer to enable
     * automatic Spring bean binding. Notice the variable
     * starts with groovy..., could be cause of confusing because
     * the type is PageRenderer without prefix Groovy....
     */
    PageRenderer groovyPageRenderer

    String createConfirmMessage() {
        groovyPageRenderer.render view: '/email/confirm', model: [username: findUsername()]
    }

    String createWelcomeMessage() {
        groovyPageRenderer.render template: '/email/welcome', model: [username: findUsername()]
    }

    private String findUsername() {
        // Lookup username, for this example we return a
        // simple String value.
        'mrhaki'
    }
}

Next we create a integration test to see if our content is rendered correctly:

// File: test/integration/page/renderer/RenderOutputTests.groovy
package page.renderer

import org.junit.Assert
import org.junit.Test

class RenderOutputTests {

    RenderService renderService

    @Test
    void welcomeMessage() {
        Assert.assertEquals 'Hi, mrhaki', renderService.createWelcomeMessage().trim()
    }

    @Test
    void confirmMessage() {
        final String expectedOutput = '''
        <!doctype html>
        <head>
            <title>Confirmation</title>
        </head>

        <body>

        <h2>
        Hi, mrhaki</h2>

        <p>
            Thank you for your registration.
        </p>

        </body>
        </html>'''

        Assert.assertEquals expectedOutput.stripIndent(), renderService.createConfirmMessage()
    }
}

We can run our test and everything is okay:

$ grails test-app RenderOutputTests
| Completed 2 integration tests, 0 failed in 1051ms
| Tests PASSED - view reports in target/test-reports

We can use tags from tag libraries in our GSP views and templates. The Sitemesh layouts cannot be used. The PageRenderer works outside of the request scope, which is necessary for the Sitemesh layouts. Because we are outside of the web request scope we cannot generate absolute links in the GSP view directly. If we change the confirm GSP view and add a tag to create an absolute link we get an UnsupportedOperationExeption with the message You cannot read the server port in non-request rendering operations.

%{-- File: grails-app/views/email/confirm.gsp --}%
<!doctype html>
<head>
    <title>Confirmation</title>
</head>

<body>

<h2><g:render template="/email/welcome" model="[username: username]"/></h2>

<p>
    Thank you for your registration.
</p>

<p>
    To use the application can you directly go to the
    <g:link absolute="true" controller="index">home page</g:link>.
</p>

</body>
</html>

But we can simply workaround this issue. Remember that since Grails 2 we can use the grailsLinkGenerator to generate links in for example a service. So we create our absolute link with the grailsLinkGenerator and pass it as a model attribute to our GPS view.

package page.renderer

import grails.gsp.PageRenderer
import org.codehaus.groovy.grails.web.mapping.LinkGenerator

class RenderService {

    PageRenderer groovyPageRenderer

    LinkGenerator grailsLinkGenerator

    String createConfirmMessage() {
        final String homePageLink = grailsLinkGenerator.link(controller: 'index', absolute: true)
        groovyPageRenderer.render(view: '/email/confirm', model: [link: homePageLink, username: findUsername()])
    }

    String createWelcomeMessage() {
        groovyPageRenderer.render template: '/email/welcome', model: [username: findUsername()]
    }

    private String findUsername() {
        // Lookup username, for this example we return a
        // simple String value.
        'mrhaki'
    }
}
%{-- File: grails-app/views/email/confirm.gsp --}%
<!doctype html>
<head>
    <title>Confirmation</title>
</head>

<body>

<h2><g:render template="/email/welcome" model="[username: username]"/></h2>

<p>
    Thank you for your registration.
</p>

<p>
    To use the application can you directly go to the
    <a href="${link}">home page</a>.
</p>

</body>
</html>

4 comments:

Anonymous said...

I'm getting an error "Cannot invoke method findViewByPath() on null object. Stacktrace follows:" when running this code. I end up in PageRenderer.class and I can see that the groovyPageLocator object is null in method renderViewToWriter(). Am I missing a step?

Anonymous said...

Does not work with Tomcat6. Requires servlet 3.0 and above, which wasn't used until Tomcat7. Calling grails.gsp.PageRenderer.render throws ClassNotFoundException for javax.servlet.http.Part

Stephane Rainville said...

Very nice thanks !
How do I change the locale for the render? (i.e. personalized eMail)

Most methods change it at session level
def newLocale = new Locale(lang)
RCU.getLocaleResolver(request).setLocale(request, response, newLocale)

Sérgio Michels said...

@Stephane Rainville

Pass the desired locale in the model and use that you obtain your messages.

Post a Comment