Groovy/Java API
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
Global Variables
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. |
|
|
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. |
|
|
siteContext
|
The current SiteContext
|
Examples
The following examples shows you how to use the global variables in your scripts. For more information on available interfaces for your Groovy scripts, see the CrafterCMS JavaDocs.
Make a Query for Content Based on Structure
The following code examples use the Site Item Service in Crafter Engine to get content. You can find the interface for this service here at Site Item Service JavaDoc
1def topNavItems = [:]
2def siteDir = siteItemService.getSiteTree("/site/website", 2)
3
4if (siteDir) {
5 def dirs = siteDir.childItems
6 dirs.each { dir ->
7 def dirName = dir.getStoreName()
8 def dirItem = siteItemService.getSiteItem("/site/website/${dirName}/index.xml")
9 if (dirItem != null) {
10 def dirDisplayName = dirItem.queryValue('internal-name')
11 topNavItems.put(dirName, dirDisplayName)
12 }
13 }
14}
15
16return topNavItems
Make a Query for Content Based on Structure with Filter
The following code examples use the Site Item Service in Crafter Engine to get content. In the example we build on the Site Item Service of getting objects under a specific tree in the repository by supplying a filter that will be applied to each object first to determine if it should be part of the result. Filters can make their determination based on the path or the content or even “outside” influence.
You can find the interface for this service here at Site Item Service JavaDoc
Note in the example below we define our own filter based on the ItemFilter interface found at ItemFilter JavaDoc
However, you may use out of the box filters as well if they meet your needs. These are found here at the Filter Package Summary
Finally be aware that for simple filename patterns, methods for this already exist in the Site Item Service and no filter is required (but they make for an simple to understand example.)
1import org.craftercms.core.service.ItemFilter
2import org.craftercms.core.service.Item
3import java.util.List
4
5
6def result = [:]
7def navItems = [:]
8def siteDir = siteItemService.getSiteTree("/site/website", 2, new StartsWithAItemFilter(), null)
9
10if (siteDir) {
11 def dirs = siteDir.childItems
12 dirs.each { dir ->
13 def dirName = dir.getStoreName()
14 def dirItem = siteItemService.getSiteItem("/site/website/${dirName}/index.xml")
15 if (dirItem != null) {
16 def dirDisplayName = dirItem.queryValue('internal-name')
17 navItems.put(dirName, dirDisplayName)
18 }
19 }
20}
21result.navItems = navItems
22
23return result
24
25/**
26 * Define a filter that returns only items that have a name that starts with "A" or "a"
27 */
28class StartsWithAItemFilter implements ItemFilter {
29
30 public boolean runBeforeProcessing() {
31 return true
32 }
33
34 public boolean runAfterProcessing() {
35 return false
36 }
37
38 public boolean accepts(Item item, List acceptedItems, List rejectedItems, boolean runBeforeProcessing) {
39
40 if (item.getName().toLowerCase().startsWith("a")) {
41 return true
42 }
43
44 return false
45 }
46 }
Make a Query Against Fields in a Content Object
The following code examples use the Site Item Service in Crafter Engine to get content. You can find the interface for this service at Site Item Service JavaDoc
1def result = [:]
2def segment = "a segment value" // could come from profile, query param etc
3
4// load a specific content object
5def itemDom = siteItemService.getSiteItem("/site/components/sliders/default.xml")
6
7// query specific values from the object
8result.header = itemDom.queryValue("/component/targetedSlide//segment[contains(.,'" + segment + "')]../label")
9result.image = itemDom.queryValue("/component/targetedSlide//segment[contains(.,'" + segment + "')]/../image")
10
11return result
Other Variables
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
|
|
pathVars |
The path variable values for the
current request
|
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
andauthentication
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 Configure 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
Java API
CrafterCMS provides the following Java libraries for managing your projects:
Crafter Commons |
|
Crafter Core |
|
Crafter Deployer |
|
Crafter Engine |
|
Crafter Profile |
|
Crafter Search |
|
Crafter Social |
|
Crafter Studio |
The Groovy examples listed above uses interfaces from the Java libraries in the table.
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]
Using Path Variables
Path variables allows you to pass a value as part of the URL. It’s a part of the path of the document to be accessed during the API call. In a REST controller the system can automatically pick out those portions of the URL and feed them to you with the name you supplied.
Use of path variables is done by allowing you to have a folder in {NAME}
format under scripts/rest/
and these folders become templates for parameterized URLs. Values added in these /
locations are added to the
params map with the key of the name inside the {..}
. The pathVars
variable is available for accessing the
value passed in the API call.
To specify path variables for a REST API, simply name the folder as the variables, for example, for the following API
call http://mysite/api/1/services//bar/{model}/{make}/{year}/foo
, to create the path variables {model}
,
{make}
and {year}
, your folder structure under /scripts/rest
should look something like this:
/bar/{model}/{make}/{year}/foo
. To access the values passed to the path variables, use the pathVars
variable
e.g. to access the value passed to {make}
in the API call, use pathVars.make
, for {model}
use
pathVars.model
and for {year}
use pathVars.year}
.
Let’s take a look at an example of creating a REST API http://mysite/api/1/services/foo/{version}/helloworld.json
.
First, we’ll set up the folder structure for the API. Under scripts
> rest
, add a folder named foo
, then
under the foo
folder, add a folder named {version}
which will be the path variable.
Next we’ll add the script. Under scripts
> rest
> foo
> {version}
, create the script helloworld.get.groovy
def version = pathVars.version
return "Hello world version ${version}!"
Here’s how the folders and files should look like for our example:
1scripts/
2 rest/
3 foo/
4 {version}/
5 helloworld.json
When we make a call to http://mysite/api/1/services/foo/1/helloworld.json, the output will be:
"Hello world version 1!"
Similarly, a call to http://mysite/api/1/services/foo/2/helloworld.json will output:
"Hello world version 2!"
Custom HTTP Responses
Rest scripts will return the 404
page when a script is not found.
Developers will still be able to return custom 404
responses from rest scripts. e.g.:
response.setStatus(404)
return 'This is the custom message'
If desired, they could even conditionally send the default response page as well by using sendError
instead:
response.sendError(404)
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
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:
Designate a folder for all test related files
Write your unit test code
Setup your unit test to run with Gradle
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:
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
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}
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}
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}
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.
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.
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
$ 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
Other Topics
Studio Content Write
Since 4.1.6To write content from an input stream and notify subscribers (including the preview indexing) about the ContentEvent
in Studio only, use the method writeContentAndNotify
:
boolean writeContentAndNotify(String site, String path, InputStream content) throws ServiceLayerException;
This method can be used from any Groovy script in Studio, this includes for example REST scripts in Studio plugins and
content type controllers. Remember to get the contentService
bean when using the method like below:
def documentStream = ContentUtils.convertDocumentToStream(document, "UTF-8")
def contentService = applicationContext.getBean("cstudioContentService")
contentService.writeContentAndNotify(site, path, documentStream)