• Document Up to Date
  • Updated On 4.1.0

Groovy Development

CrafterCMS supports server-side development with Groovy. By using Groovy, you can create RESTful services, MVC controllers, code that runs before a page or component is rendered, servlet filters, scheduled jobs, and entire backend applications.

Groovy API

CrafterCMS provides a number of useful global variables that can be used in all the different types of scripts available:

Name

Description

Type

siteItemService
Allows access to the site content.
urlTransformationService


Service for transforming URLs, like
transforming the content URL of a
page to the web or render URL.
search


Service that can be used to execute
search queries against
OpenSearch.
applicationContext



Provides access to the Crafter
Engine’s Spring beans and site beans
defined in
config/spring/application-context.xml
globalProperties


Provides access to global
configuration properties defined in
server-config.properties. |
navBreadcrumbBuilder


Helper class that returns the list of
path components in an URL, to create
navigation breadcrumbs. |
navTreeBuilder

Helper class that creates navigation
trees to facilitate rendering
tenantsResolver


Can be used to retrieve the
Profile tenants associated to the
current site. |
profileService

Provides access to the Crafter
Profile API for profiles. |
tenantService

Provides access to the Crafter
Profile API for tenants. |
authenticationService

Provides access to the Crafter
Profile API for authentication. |
authenticationManager

Manages Crafter Security Provider
based authentications. |
textEncryptor

Utility class for encrypting/
decrypting text with AES. |
See TextEncryptor under org.springframework.security.crypto.encrypt in the Spring Security apidocs
modePreview



Flag that indicates that Engine is
being executed in preview mode
(also the value of the
crafter.engine.preview property)
Boolean



crafterEnv


Indicates the value of the
crafter.engine.environment
property
String


logger
The GroovyUtils SLF4J logger
siteConfig

The current site Configuration,
loaded from /config/site.xml. |
See XMLConfiguration under org.apache.commons.configuration2 in the Apache Commons apidocs
siteContext
The current SiteContext

There are also several other variables available to scripts that are executed during the scope of a request (REST scripts, controller scripts, page/component scripts and filter scripts):

Name

Description

API

request

The current request

response

The current response

params

The parameter values for the current
request

headers

The header values for the current
request

cookies

The cookie values for the current
request

session

The current session, if it has been
created

locale

The current locale for the current
user

authToken

The current authentication (if the
user has logged in), created by
Spring Security

The following variables are provided for backward compatibility when using Crafter Profile, should be replaced with authToken if possible:

Name

Description

API

authentication

The current authentication (if the
user has logged in), created by the
Crafter Security Provider

profile

The current profile (if the user
has logged in), created by the
Crafter Security Provider

Note

The variables profile and authentication listed above will be null in most cases and should not be used anymore

The following variables are restricted by default, to use them see Configuration Related to Building Custom Services

Name

Description

API

application

The servlet context

All scripts are executed in a sandbox to prevent insecure code from running, to change the configuration see Groovy Sandbox Configuration

To create unit tests for your groovy code, see Unit Testing CrafterCMS Groovy Code

Types of Scripts

There are different types of scripts you can create, depending on the subfolder under Scripts where they’re placed. The following are the ones currently supported:

REST Scripts

REST scripts function just like RESTful services. They just need to return the object to serialize back to the caller. REST scripts must be placed in any folder under Scripts > rest.

A REST script URL has the following format: it starts with /api/1/services, then contains all the folders that are part of the hierarchy for the particular script, and ends with the script name, the HTTP method and the .groovy extension. So, a script file at Scripts > rest > myfolder > myscript.get.groovy will respond to GET method calls at http://mysite/api/1/services/myfolder/myscript.json.

The following is a very simple sample script that returns a date attribute saved in the session. If no attribute is set yet, the current date is set as the attribute. Assume that the REST script exists under Scripts > rest > session_date.get.groovy

 1import java.util.Date
 2
 3if (!session) {
 4    session = request.getSession(true)
 5}
 6
 7def date = session.getAttribute("date")
 8if (!date) {
 9    date = new Date()
10
11    session.setAttribute("date", date)
12}
13
14return ["date": date]

Controller Scripts

Controller scripts are very similar to REST scripts. They have the same variables available, but instead of returning an object, they return a string with the view to render. Most of the time, this is just the template path, like in the following snippet:

return "/templates/web/registration.ftl"

Controller scripts basically work like a controller in the MVC pattern. They should be put under Scripts > controllers, and similarly to REST scripts, the URL is made up of the directory hierarchy, the script name and the HTTP method. For example, a script at Scripts > controllers > myfolder > mycontroller.get.groovy will respond to GET calls at http://mysite/myfolder/mycontroller.

The following is a very simple example script that will do the sum of 2 parameters, put the result in the templateModel and return the path of the FTL template that will render the result:

templateModel.result = Integer.parseInt(params.num1) + Integer.parseInt(params.num2)

return "/templates/web/sum.ftl"

One very common controller script is the sitemap.groovy. A sitemap is used by search engines to have a better idea of how to “crawl” a website. A sitemap is an XML with references to most of the site’s pages, and basically looks like this:

 1<?xml version="1.0" encoding="UTF-8"?>
 2<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
 3    <url>
 4        <loc>http://www.domain.com /</loc>
 5        <lastmod>2008-01-01</lastmod>
 6        <changefreq>weekly</changefreq>
 7        <priority>0.8</priority>
 8    </url>
 9    <url>
10        <loc>http://www.domain.com/catalog?item=vacation_hawaii</loc>
11        <changefreq>weekly</changefreq>
12    </url>
13    <url>
14        <loc>http://www.domain.com/catalog?item=vacation_new_zealand</loc>
15        <lastmod>2008-12-23</lastmod>
16        <changefreq>weekly</changefreq>
17    </url>
18    <url>
19        <loc>http://www.domain.com/catalog?item=vacation_newfoundland</loc>
20        <lastmod>2008-12-23T18:00:15+00:00</lastmod>
21        <priority>0.3</priority>
22    </url>
23    <url>
24        <loc>http://www.domain.com/catalog?item=vacation_usa</loc>
25        <lastmod>2008-11-23</lastmod>
26    </url>
27</urlset>

Search engines look for the sitemap just after the domain, so a sitemap URL would look like www.domain.com/sitemap. The sitemap controller then must be placed in Scripts > controllers > sitemap.groovy. The code would be similar to this:

 1import groovy.xml.MarkupBuilder
 2import groovy.xml.MarkupBuilderHelper
 3
 4def sitemap = []
 5def excludeContentTypes = ['/component/level-descriptor']
 6
 7parseSiteItem = { siteItem ->
 8    if (siteItem.isFolder()) {
 9        def children = siteItem.childItems;
10        children.each { child ->
11            parseSiteItem(child);
12        }
13    } else {
14        def contentType = siteItem.queryValue('content-type')
15        if (!excludeContentTypes.contains(contentType)) {
16            def storeUrl = siteItem.getStoreUrl();
17            def location = urlTransformationService.transform('storeUrlToFullRenderUrl', storeUrl);
18            sitemap.add(location);
19        }
20    }
21}
22
23def siteTree = siteItemService.getSiteTree("/site/website", -1)
24if (siteTree) {
25    def items = siteTree.childItems;
26    items.each { siteItem ->
27        parseSiteItem(siteItem);
28    }
29}
30
31response.setContentType("application/xml;charset=UTF-8")
32
33def writer = response.getWriter()
34def xml = new MarkupBuilder(writer)
35def xmlHelper = new MarkupBuilderHelper(xml)
36
37xmlHelper.xmlDeclaration(version:"1.0", encoding:"UTF-8")
38
39xml.urlset(xmlns:"http://www.sitemaps.org/schemas/sitemap/0.9") {
40    sitemap.each { location ->
41        url {
42            loc(location)
43            changefreq("daily")
44        }
45    }
46}
47
48response.flushBuffer()
49
50return null

Page and Component Scripts

Crafter page and components can have their own controller scripts too, that are executed before the page or component is rendered, and that can contribute to the model of the template. These scripts, besides the common variables, have the following model related variables:

Model Related Variable
Description
contentModel

The XML descriptor content
It is an instance of the SiteItem class
templateModel

The actual map model of the template
It is an instance of the AllHttpScopesAndAppContextHashModel class

As mentioned in the table above, the templateModel is the actual map model of the template, and any variable put in it will be accessible directly in the template, eg. if the script has the line templateModel.var = 5, then in the template the var’s value can be printed with ${var}. The scripts don’t have to return any result, just populate the templateModel.

There are 2 ways in which you can “bind” a script to a page or component:

  1. Put the script under Scripts > pages or Scripts > components, and name it after the page or component content type.

  2. When creating the content type for the page or component, add an Item Selector with the variable name scripts. Later when creating

    a page or component of that type, you can select multiple scripts that will be associated to the page or component.

The following is an example of a component script. The component content type is /component/upcoming-events. We can then place the script in Scripts > components > upcoming-events.groovy so that it is executed for all components of that type.

scripts/components/upcoming-events.groovy
 1 import org.craftercms.engine.service.context.SiteContext
 2
 3 import utils.DateUtils
 4
 5 def now = DateUtils.formatDateAsIso(new Date())
 6 def queryStr = "crafterSite:\"${siteContext.siteName}\" AND content-type:\"/component/event\" AND disabled:\"false\" AND date_dt:[${now} TO *]"
 7 def start = 0
 8 def rows = 1000
 9 def sort = "date_dt asc"
10 def query = searchService.createQuery()
11
12 query.setQuery(queryStr)
13 query.setStart(start)
14 query.setRows(rows)
15 query.addParam("sort", sort)
16 query.addParam("fl", "localId")
17
18 def events = []
19 def searchResults = searchService.search(query)
20 if (searchResults.response) {
21   searchResults.response.documents.each {
22     def event = [:]
23     def item = siteItemService.getSiteItem(it.localId)
24
25     event.image = item.image.text
26     event.title = item.title_s.text
27     event.date = DateUtils.parseModelValue(item.date_dt.text)
28     event.summary = item.summary_html.text
29
30     events.add(event)
31   }
32 }
33
34 templateModel.events = events

In the above example, you will see that we’re importing a utils.DateUtils class. This class is not part of CrafterCMS, but instead it is a site specific class written in Groovy. The class is/needs to be located at the following path scripts > classes > utils and is placed in a file called DateUtils.groovy, like below:

scripts/classes/utils/DateUtils.groovy
 1 package utils
 2
 3 import java.text.SimpleDateFormat
 4
 5 class DateUtils {
 6
 7   static def parseModelValue(value){
 8     def dateFormat = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss")
 9     dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"))
10     return dateFormat.parse(value)
11   }
12
13   static def formatDateAsIso(date) {
14     def dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
15     dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"))
16     eturn dateFormat.format(date)
17   }
18
19 }

Filter Scripts

Crafter Engine can handle special Groovy filters that work basically in the same way as Servlet filters. These are scripts very similar to page scripts, and have basically the same variables available (except the templateModel and contentModel variables), but instead of updating the template model, they call filterChain.doFilter (request, response), just like in Java Servlet filters, to continue with the filter chain. You can even stop the request filtering and return the response directly, like in this example:

1if (!authentication) {
2    response.sendError(400, "You're not a subscriber")
3} else {
4    filterChain.doFilter(request, response)
5}

All this filter scripts should be put under Scripts > filters, and their mappings should be defined in Config > site.xml. The order in which the mappings appear is the order in which the filters will be applied.

 1<filters>
 2    <filter>
 3        <script>/scripts/filters/testFilter1.groovy</script>
 4        <mapping>
 5            <include>/**</include>
 6        </mapping>
 7    </filter>
 8    <filter>
 9        <script>/scripts/filters/testFilter2.groovy</script>
10        <mapping>
11            <include>/**</include>
12        </mapping>
13    </filter>
14    <filter>
15        <script>/scripts/filters/testFilter3.groovy</script>
16        <mapping>
17            <include>/**</include>
18            <exclude>/static-assets/**</exclude>
19        </mapping>
20    </filter>
21</filters>

The following is an example script that adds a display name attribute to the current profile if the attribute doesn’t exist yet. Assume that the script is placed in Scripts > filters > addDisplayName.groovy.

 1if (profile) {
 2    def displayName = profile.getAttribute("displayName")
 3    if (!displayName) {
 4        def id = profile.id.toString()
 5        def firstName = profile.getAttribute("firstName")
 6        def lastName = profile.getAttribute("lastName")
 7        def newAttributes = [:]
 8
 9        newAttributes["displayName"] = "${firstName} ${lastName}".toString()
10
11        profileService.updateAttributes(id, newAttributes)
12
13        logger.info("Display name added to profile '${id}'")
14    }
15}
16
17filterChain.doFilter(request, response)

To enable this filter, we need to configure its mapping in Config > site.xml. The mapping would look as the following.

 1<?xml version="1.0" encoding="UTF-8"?>
 2<site>
 3    <filters>
 4        <filter>
 5            <script>/scripts/filters/addDisplayName.groovy</script>
 6            <mapping>
 7                <include>/**</include>
 8            </mapping>
 9        </filter>
10    </filters>
11</site>

Scheduled Script Jobs

Scripts can also be scheduled as jobs in Crafter Engine. These scripts only have the common global variables and the logger variable. They don’t need to return any result. Engine allows 3 different ways to configure script jobs:

  • By placing the scripts under one of the following folders in Scripts > jobs: hourly, daily, weekly and monthly. As the names imply, scripts under these folders will be scheduled to run every hour (hourly), at 12:00 am every day (daily), at 12:00 am every Monday (weekly), or at 12:00 am every first day of the month (monthly).

  • By adding one or more <jobFolder> configuration elements under <jobs> in Config > site.xml. Under <jobFolder> you can specify a <path> and a <cronExpression>, and every script under that folder will be scheduled using the cron expression.

    1<jobs>
    2    <jobFolder>
    3        <path>/scripts/jobs/morejobs</path>
    4        <cronExpression>0 0/15 * * * ?</cronExpression>
    5    </jobFolder>
    6</jobs>
    
  • By adding one or more <job> configuration elements under <jobs> in Config > site.xml. With the <path> and <cronExpression> elements, you specify the job script path and the cron expression for scheduling.

    1<jobs>
    2    <job>
    3        <path>/scripts/jobs/testJob.groovy</path>
    4        <cronExpression>0 0/15 * * * ?</cronExpression>
    5    </job>
    6</jobs>
    

Unit Testing CrafterCMS Groovy Code

For larger sites with complex services implemented in Groovy, it is very helpful to have a way to include unit tests in a way that can be easily integrated with CI/CD systems.

This section details how to create unit tests for CrafterCMS Groovy code with Gradle.

For more information on the classes of the variables that can be mocked for unit testing, see above

Steps for Creating Groovy Unit Test

To create a unit test:

  1. Designate a folder for all test related files

  2. Write your unit test code

  3. Setup your unit test to run with Gradle

  4. Execute your unit test

Write Your Unit Test Code

There are no restrictions or requirements for the unit test code, developers can choose any testing framework supported by the build tool. Examples: spring-test, junit, testng, spock

Remember when writing unit test code, developers will be responsible for:

  • Choosing & configuring the testing framework

  • Making sure all required dependencies are included (for example external jars)

  • Mocking all Crafter Engine classes used by the classes under testing

Setup Your Unit Test to Run With Gradle

To use Gradle the only requirement is to add a build.gradle in the root folder of the site repository and execute the test task. Example:

CRAFTER_HOME/data/repos/sites/SITENAME/sandbox/build.gradle
 1// Enable Gradle’s Groovy plugin
 2plugins {
 3  id 'groovy'
 4}
 5
 6sourceSets {
 7  // Add the site Groovy classes that will be tested
 8  main {
 9    groovy {
10      srcDir 'scripts/classes'
11    }
12  }
13
14  // Add the Groovy classes & resources to perform the tests
15  test {
16    groovy {
17      srcDir 'scripts/test/classes'
18    }
19    resources {
20      srcDir 'scripts/test/resources'
21    }
22  }
23}
24
25// Enable the testing framework of choice
26test {
27  useJUnit()
28}
29
30repositories {
31  mavenCentral()
32}
33
34// Include the required dependencies
35dependencies {
36  // This dependency is required for two reasons:
37  // 1. Make Engine’s classes available for compilation & testing
38  // 2. Include the Groovy dependencies required by Gradle
39  implementation 'org.craftercms:crafter-engine:4.1.1:classes'
40
41  // Include the chosen testing dependencies
42  testImplementation 'junit:junit:4.13.2'
43  testImplementation 'org.mockito:mockito-core:4.11.0'
44}

Execute Your Unit Test

Given the previous example the tests can be executed using a single command:

gradle test

Example

Let’s take a look at an example of a groovy unit test in a site created using the empty blueprint with a custom groovy script, MyService

Unit Testing Groovy - Sample Service

CRAFTER_HOME/data/repos/sites/SITENAME/sandbox/scripts/classes/org/company/site/api/MyService.groovy
 1package org.company.site.api
 2
 3/*
 4  Custom service that will be invoked from other controllers like scheduled jobs, page or REST scripts
 5 */
 6
 7interface MyService {
 8
 9    def saveFormSubmission(def title, def author, def message)
10
11    def getPostViews(def postId)
12
13    def getPostsSummary(def date)
14
15}

CRAFTER_HOME/data/repos/sites/SITENAME/sandbox/scripts/classes/org/company/site/api/MySearchService.groovy
 1package org.company.site.api
 2
 3/*
 4  Service that wraps search to hide queries & result mapping from other services
 5 */
 6
 7interface MySearchService {
 8
 9    def getPostsForDate(def date)
10
11}

CRAFTER_HOME/data/repos/sites/SITENAME/sandbox/scripts/classes/org/company/site/api/ExternalApi.groovy
 1package org.company.site.api
 2
 3/*
 4  External service to store & retrieve data, in real life this could be using an external REST API or DB
 5 */
 6
 7interface ExternalApi {
 8
 9    def saveFormSubmission(def formId, def title, def author, def message)
10
11    def getPostViews(def postId)
12
13}

CRAFTER_HOME/data/repos/sites/SITENAME/sandbox/scripts/classes/org/company/site/impl/MyServiceImpl.groovy
 1package org.company.site.impl
 2
 3import org.company.site.api.MySearchService
 4import org.craftercms.core.service.CachingOptions
 5import org.craftercms.core.util.cache.CacheTemplate
 6import org.company.site.api.ExternalApi
 7import org.company.site.api.MyService
 8import org.craftercms.engine.service.context.SiteContext
 9
10/*
11  Implementation of the custom service
12 */
13
14class MyServiceImpl implements MyService {
15
16    protected ExternalApi externalApi
17
18    protected CacheTemplate cacheTemplate
19
20    protected MySearchService searchService
21
22    protected def formId
23
24    protected def refreshFrequency
25
26    def saveFormSubmission(def title, def author, def message) {
27        return externalApi.saveFormSubmission(formId, title, author, message)
28    }
29
30    def getPostViews(def postId) {
31        def context = SiteContext.current.context
32        def options = new CachingOptions()
33        options.refreshFrequency = refreshFrequency
34        return cacheTemplate.getObject(context, options, { externalApi.getPostViews(postId) }, "post-views-", postId)
35    }
36
37    def getPostsSummary(def date) {
38        def posts = searchService.getPostsForDate(date)
39        return posts.collect { post ->
40            [
41                    id: post.id,
42                    views: getPostViews(post.id)
43            ]
44        }
45    }
46
47}

Let’s begin creating our unit test for MyService

Designate Folder for All Test Related Files

The first thing we need to do is to designate a folder for all test related files. We’ll designate the /scripts/test folder to be used for all test related files.

Write Your Unit Test Code

Next, we’ll write the unit test code.

Unit Testing Groovy - Sample Service

CRAFTER_HOME/data/repos/sites/SITENAME/sandbox/scripts/test/classes/org/company/impl/MyServiceImplTest.groovy
  1package org.company.site.impl
  2
  3import org.company.site.api.ExternalApi
  4import org.company.site.api.MySearchService
  5import org.craftercms.core.service.Context
  6import org.craftercms.core.util.cache.CacheTemplate
  7import org.craftercms.engine.service.context.SiteContext
  8import org.junit.Before
  9import org.junit.Test
 10import org.junit.runner.RunWith
 11import org.mockito.InjectMocks
 12import org.mockito.Mock
 13import org.mockito.Spy
 14import org.mockito.junit.MockitoJUnitRunner
 15
 16import static org.mockito.ArgumentMatchers.any
 17import static org.mockito.ArgumentMatchers.eq
 18import static org.mockito.Mockito.verify
 19import static org.mockito.Mockito.when
 20
 21/*
 22  Tests for the custom service
 23 */
 24
 25@RunWith(MockitoJUnitRunner)
 26class MyServiceImplTest {
 27
 28    static final def FORM_ID = "myFormId"
 29
 30    static final def POST_ID_1 = "myPostId1"
 31
 32    static final def POST_ID_2 = "myPostId2"
 33
 34    static final def POST_VIEWS_1 = 42
 35
 36    static final def POST_VIEWS_2 = 0
 37
 38    static final def REFRESH_FREQ = 5
 39
 40    @Spy
 41    SiteContext siteContext
 42
 43    @Mock
 44    Context context
 45
 46    @Mock
 47    CacheTemplate cacheTemplate
 48
 49    @Mock
 50    MySearchService searchService
 51
 52    @Mock
 53    ExternalApi externalApi
 54
 55    @InjectMocks
 56    MyServiceImpl myServiceImpl
 57
 58    @Before
 59    void setUp() {
 60        myServiceImpl.formId = FORM_ID
 61        myServiceImpl.refreshFrequency = REFRESH_FREQ
 62
 63        siteContext.context = context
 64        SiteContext.current = siteContext
 65
 66        when(externalApi.saveFormSubmission(eq(FORM_ID), any(), any(), any())).thenReturn(true)
 67        when(externalApi.getPostViews(eq(POST_ID_1))).thenReturn(POST_VIEWS_1)
 68        when(externalApi.getPostViews(eq(POST_ID_2))).thenReturn(POST_VIEWS_2)
 69
 70        when(cacheTemplate.getObject(eq(context), any(), any(), any())).then({ i -> i.getArgument(2).execute() })
 71
 72        when(searchService.getPostsForDate(any())).thenReturn([
 73                [ id: POST_ID_1 ],
 74                [ id: POST_ID_2]
 75        ])
 76    }
 77
 78    @Test
 79    void testFormSubmission() {
 80        def title = "My New Post"
 81        def author = "John Doe"
 82        def message = "Is this working?"
 83
 84        assert myServiceImpl.saveFormSubmission(title, author, message) == true
 85        verify(externalApi).saveFormSubmission(eq(FORM_ID), eq(title), eq(author), eq(message))
 86    }
 87
 88    @Test
 89    void testGetPostViews() {
 90        assert myServiceImpl.getPostViews(POST_ID_1) == 42
 91    }
 92
 93    @Test
 94    void testGetPostsSummary() {
 95        def result = myServiceImpl.getPostsSummary(new Date())
 96
 97        assert result.size() == 2
 98        assert result[0].views == POST_VIEWS_1
 99        assert result[1].views == POST_VIEWS_2
100    }
101
102}

Setup Your Unit Test to Run With Gradle

We’ll now setup our unit test to run with Gradle, by adding a build.gradle file in the root folder of the site repository and execute the test task.

CRAFTER_HOME/data/repos/sites/SITENAME/sandbox/build.gradle
 1plugins {
 2    id 'groovy'
 3}
 4
 5sourceSets {
 6    main {
 7        groovy {
 8            srcDir 'scripts/classes'
 9        }
10    }
11
12    test {
13        groovy {
14            srcDir 'scripts/test/classes'
15        }
16
17        resources {
18            srcDir 'scripts/test/resources'
19        }
20    }
21}
22
23test {
24    useJUnit()
25}
26
27repositories {
28    mavenCentral()
29}
30
31dependencies {
32    implementation ('org.craftercms:crafter-engine:4.1.1:classes')
33    compile 'javax.servlet:servlet-api:4.0.1'
34
35    testImplementation 'junit:junit:4.13.2'
36    testImplementation 'org.mockito:mockito-core:4.11.0'
37}

Execute Your Unit Test

Finally, we can run our unit test by running gradle test

Output when running unit test
$ gradle test

BUILD SUCCESSFUL in 4s
3 actionable tasks: 3 up-to-date

Let’s take a look at the result of our unit test which can be found here: CRAFTER_HOME/data/repos/sites/SITENAME/sandbox/build/reports/tests/test/index.html

Unit Testing Groovy - Unit test  build report

See Also