1. Introduction

GR8 CRM is a set of Grails Web Application Framework plugins that makes it easy to develop web applications with CRM functionality.

You can find more information about GR8 CRM on the main documentation site http://gr8crm.github.io.

1.1. Customer Relationship Management

Customer relationship management (CRM) is a system for managing a company’s interactions with current and future customers. It involves using technology to organize, automate and synchronize sales, marketing, customer service, and technical support. Wikipedia

The GR8 CRM "Ecosystem" currently contains over 40 Grails plugins. For a complete list of plugins see http://gr8crm.github.io.

Each GR8 CRM plugin defines a Bounded Context that focus on one specific domain, for example contact, project or document.

2. Content Management Plugin

This plugin provide storage and services for managing content in GR8 CRM applications. Content can be any type of media like plain text, Microsoft Word, PDF, and images. Content can be stored in folders or attached to domain instances. Content can be shared with users of the application or shared publicly to the world.

Note that this plugin does not contain any user interface components. This plugin contains domain classes and services only. The plugin crm-content-ui provides a Twitter Bootstrap based user interface for managing files, folders and attachments. crm-content-ui depends on crm-content so you only need to include crm-content-ui in your BuildConfig.groovy if you want end-user content management features.

The Content Management plugin is very generic and flexible. It can be used to provide content in any form. Here are some real-wold examples on what type of content that can be stored and managed by this plugin.

  • System HTML email templates that can be edited by an administrator

  • Attach documents to customer records, like contracts, tenders or spreadsheets

  • User guides in PDF or other eReader formats

  • HTML page fragments to be included in Groovy Server Pages

  • Complete HTML pages to be rendered as part of a web site

3. Storage

The crm-content plugin has configurable storage providers. The default storage provider store content in the web server filesystem. Files on disc can optionally be encrypted for increased security. Custom storage providers can be developed to integrate with content services like Amazon S3, Google Drive or Dropbox. One application can have multiple storage providers and select the best storage provider based on some business logic. Maybe HTML files should be stored in a S3 bucket and customer contracts encrypted on a local disc.

The crm-content-aws plugin is a storage provider that store content in Amazon S3 buckets. This is a must-have plugin in cloud environments.

4. Attachments

Content is usually stored in folders but content can also be attached to domain instances. In fact when content is stored in a folder, it is actually "attached" to a CrmResourceFolder domain instance. So there is no technical difference between files stored in folders and files attached to domain instances.

A CrmResourceRef instance is used to attach content to a domain instance. The ref property references the domain instance using a Resource Identifier and the res property contains the provider specific URI to the content. This is nothing that you need to care about, the framework takes care about setting these properties when you create and attach content.

5. Security

Content be sensitive information, therefore security is a very important in GR8 CRM applications. All access to content is checked to make sure the user accessing content has the right privileges. Content can optionally be encrypted before it is stored on disc or sent over a network for increased security.

5.1. Access Control

An instance of the domain class CrmResourceRef is always involved when working with content. It represents a "handle" to the content. It has properties like filename, title and description. Another important property is status that specifies the status and access rights for the content. The class CrmResourceRef has one static int constant for each possible status a content instance can have. The constants are listed below.

Table 1. Content Status
Constant Description


Content is in draft state and only accessible by authenticated users


Content is archived and only accessible by authenticated users


Content is active/current but only accessible by authenticated users


Content is only accessible by a restricted group of people (application specific)


Content is accessible by anyone, no authentication needed = public access if URL is known!

5.2. Encryption

Content can be encrypted before it’s stored in the server filesystem.

The class CrmFileResource has static int constants for all supported encryption algorithms.

Table 2. Content Encryption
Constant Description


Content is not encrypted (default)


Content is encrypted with AES-128 encryption

You configure the application wide encryption key in Config.groovy. The encryption key must be 16 bytes long.

crm.content.encryption.password = "1234567890123456"

The current implementation of CrmFileResource encrypts all content if crm.content.encryption.password is set.

6. Web Access

Content can be accessed via a URL and the crm-content plugin configures a set of URL mappings for this purpose. Note that access control restrictions apply.

URL Pattern Description Example


Content attached to a domain instance



Content stored in a folder



List all files in a folder


t → Tenant ID
domain → Domain name in short (property name) format
id → ID of domain instance
file → filename
uri → any path

7. CrmContentService

This is the main service that you use to create, find, edit and delete files and folders with.

7.1. createResource

CrmResourceRef createResource(InputStream inputStream, String filename, Long length, String contentType, Object reference, Map params = [:])

Create a new file from an InputStream.

Parameter Description


The input stream to read content from


Name of content, this is later used when accessing this content


Content length in bytes


MIME content type


a domain instance or a reference identifier to attach the content to


optional parameters like status, title and description for the content

If the content creation succeeds an instance of CrmResourceRef is returned. This is an active "handle" to the content.

The resource property on CrmResourceRef return a URI instance. This URI is used by other service methods, for example when reading and writing content.

The following code copies (moves) a PDF file from the server to a /presentations folder in GR8 CRM.

def folder = crmContentService.createFolder(null, "presentations")
def serverFile = new File("presentation.pdf")
def pdf = serverFile.withInputStream{inputStream->
    crmContentService.createResource(inputStream, serverFile.name, serverFile.length(), "application/pdf", folder)
serverFile.delete() (1)

assert pdf.name == "presentation.pdf"
1 The server file is copied into, and managed by GR8 CRM so it’s not needed anymore.

7.2. withInputStream

def withInputStream(URI uri, Closure work)

For content referenced by a URI create a new InputStream and pass it into a closure. This method ensures the stream is closed after the closure returns.

def content = crmContentService.getContentByPath("/presentations/2014/gr8conf/eu/goeh-feature-plugins.pdf")
crmContentService.withInputStream(content.resource) { inputStream ->
    new File("/tmp/feature-plugins.pdf").withOutputStream{ outputStream ->
        outputStream << inputStream

7.3. writeTo

long writeTo(URI uri, OutputStream out)

Write content to an OutputStream.

def show(Long id) {
    def content = crmContentService.getResourceRef(id) (1)
    def metadata = content.metadata
    crmContentService.writeTo(content.resource, response.outputStream) (2)
1 Lookup content by ID
2 Render content to the response stream. This line can be shortened to: content.writeTo(response.outputStream)

7.4. getMetadata

Map<String, Object> getMetadata(URI resource)

Get metadata for the content specified by resource. The metadata Map contains the following keys:

Key Description


the provider specific URI for the content


MIME content type


length in bytes


formatted length


name of icon that best describes the content


Date instance when content was created


Date instance when content was last updated


MD5 hash of the content


type of encrypted storage (0 = no encryption)

7.5. updateResource

long updateResource(CrmResourceRef resource, InputStream inputStream, String contentType = null)

Update/overwrite existing content.

def folder = crmContentService.createFolder(null, "test")
def bytes = "This is a test".getBytes()
def inputStream = new ByteArrayInputStream(bytes)
def ref = crmContentService.createResource(inputStream, "test1.txt", bytes.length, "text/plain", folder) (1)
bytes = "This is an updated test".getBytes()
inputStream = new ByteArrayInputStream(bytes)
crmContentService.updateResource(ref, inputStream) (2)
def result = new ByteArrayOutputStream()
def s = new String(result.toByteArray())
assert s == "This is an updated test"
1 Create a file with content "This is a test"
2 Update the content to "This is an updated test"

8. CrmFreeMarkerService

The FreeMarker service is used when you want to store FreeMarker templates with the crm-content plugin. You can use FreeMarker templates when you send email or render HTML pages. If used together with the crm-content-ui plugin you can let administrators edit templates with an HTML editor.

8.1. process

void process(String templatePath, Map binding, Writer out)

Let FreeMarker parse the template located at templatePath in the current tenant. Values in binding can be referenced from the template. The output is written to out.

void process(Long tenant, String templatePath, Map binding, Writer out)

Same as above but a tenant can be specified from which templates will be retrieved.

void process(CrmResourceRef ref, Map binding, Writer out)

Same as above but an instance of CrmResourceRef will be used as template.

9. Events

You can also send an asynchronous event that results in a template being parsed.

9.1. parseTemplate

def reply = event(namespace: 'crm', topic: 'parseTemplate', data: [template: '/templates/hello.txt', greet: 'Groovy'])
assert reply.value == 'Hello Groovy World'

10. Code Samples

10.1. Create a folder

def rootFolder = crmContentService.createFolder(null, "templates")
def subFolder = crmContentService.createFolder(rootFolder, "powerpoint")

10.2. Create a file

def bytes = "Hello World".getBytes()
def inputStream = new ByteArrayInputStream(bytes) (1)
def folder = crmContentService.createFolder(null, "files")
def doc = crmContentService.createResource(inputStream, "hello.txt", bytes.length, "text/plain", folder)
assert doc.title == "test1"
assert doc.name == "test1.txt"
assert doc.text == "Hello World"
1 The stream is closed by createResource(…​)
You can look at the source code for the integration tests to find more code examples.

10.3. Save photos to Amazon S3

The bean crmContentRouter is responsible for routing content from/to a CrmContentProvider when reading and writing content. The following example replaces the default content router with an implementation that looks at the content type and size. All large images attached to contacts (CrmContact) are stored in Amazon S3 and all other content are stored by the default content provider (the local file system). The awsContentProvider bean is provided by crm-content-aws plugin.

import grails.plugins.crm.contact.CrmContact

beans = {
    crmContentRouter(grails.plugins.crm.content.PatternContentRouter, ref("crmCoreService")) { bean ->
        bean.autowire = 'byName'

        pattern = /.*\.(jpg|jpeg|png)$/
        minLength = 131072
        referenceClass = CrmContact
        defaultProvider = ref("crmFileContentProvider")
        provider = ref("awsContentProvider")

11. Tag Library

11.1. render

The render tag renders content in the browser.

<div class="row-fluid">
    <crm:render template="web/front/banner.html"/>
<div class="row-fluid">
    <crm:render template="web/front/intro.html" parser="gsp"/> (1)
1 The content can optionally be parsed with gsp or freemarker.

11.2. image

The image tag generates markup to display a resource instance as an image.

class ImageController {
    def crmContentService

    def index(Long id) {
        [file: crmContentService.getResourceRef(id)]
<crm:image resource="${file}" class="img-polaroid" width="640"/>

11.3. attachments

With the attachments tag you can iterate over resources attached to a domain instance.

The following example displays a photo album of all images attached to a project that are tagged as favorite. The project domain instance is referenced with the project variable.

<crm:attachments bean="${project}" var="file" type="image" tags="favorite"> (1) (2)
    <g:link controller="crmContent" action="open" id="${file.id}" title="${file.title.encodeAsHTML()}" target="_blank">
      <crm:image resource="${file}" width="64" class="img-polaroid" alt="${file.name.encodeAsHTML()}"/>
1 The type attribute can be any file extension, or image that is a shorthand for (jpg, png, gif).
2 The tags attribute can be used to only include attachments that are tagged with a specific value

12. Configuration


This property defines what encryption algorithm to use when storing files. File are by default stored in the filesystem on the application server. One of the following algorithms can be used:

grails.plugins.crm.content.CrmFileResource.NO_ENCRYPTION (default)

Files are not encrypted, they are stored in original form.


Files are encrypted with AES encryption

crm.content.encryption.password = "1234567890123456"

Encryption key. Must be 16 bytes!

crm.content.cache.expires = 60 * 10

Browser cache expiration (in seconds) for public content.

crm.content.include.tenant = 1L

Default tenant for content rendered with the render tag.

crm.content.include.path = '/templates'

Default path for content rendered with the render tag.

crm.content.include.parser = 'freemarker'

Default parser for content rendered with the render tag.

crm.content.freemarker.template.updateDelay = 60

The FreeMarker service checks if templates has been updated with this interval (in seconds).

13. Changes


Content-Disposition filename is now URL encoded. Content router can be replaced with custom impl to store content in other locations.


Make it easier to use multiple storage providers in the same application


withInputStream now returns what the closure returns (was void).


Fix for threading issues when loading FreeMarker templates from different tenants.


You can now specify destination root folder when importing content with CrmContentImportService


Fix for template rendering with specific tenant


Compatible with Grails 2.4.4


Fixed class reloading bug caused by missing method addControllerMethods() in plugin descriptor.


Tag attachments added to the crm tag library


Grails tags are now supported when using the crm:render tag with option parser="gsp".
Improved handling of illegal characters in file names.
CrmContentImportService#importFiles(…​) now works on Windows.


Updated dependency on crm-core to version 2.0.2


First public release

14. License

This plugin is licensed with Apache License version 2.0

15. Source Code

The source code for this plugin is available at https://github.com/technipelago/grails-crm-content

16. Contributing

Please report issues or suggestions.

Want to improve the plugin: Fork the repository and send a pull request.