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.
Constant | Description |
---|---|
CrmResourceRef.STATUS_DRAFT |
Content is in draft state and only accessible by authenticated users |
CrmResourceRef.STATUS_ARCHIVED |
Content is archived and only accessible by authenticated users |
CrmResourceRef.STATUS_PUBLISHED |
Content is active/current but only accessible by authenticated users |
CrmResourceRef.STATUS_RESTRICTED |
Content is only accessible by a restricted group of people (application specific) |
CrmResourceRef.STATUS_SHARED |
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.
Constant | Description |
---|---|
CrmFileResource.NO_ENCRYPTION |
Content is not encrypted (default) |
CrmFileResource.AES_ENCRYPTION |
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 |
---|---|---|
/s/$t/$domain/$id/$file |
Content attached to a domain instance |
|
/r/$t/$uri** |
Content stored in a folder |
|
/f/$t/$uri** |
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 |
---|---|
inputStream |
The input stream to read content from |
filename |
Name of content, this is later used when accessing this content |
length |
Content length in bytes |
contentType |
MIME content type |
reference |
a domain instance or a reference identifier to attach the content to |
params |
optional parameters like |
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
response.setContentType(metadata.contentType)
response.setContentLength(metadata.bytes.intValue())
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 |
---|---|
uri |
the provider specific URI for the content |
contentType |
MIME content type |
bytes |
length in bytes |
size |
formatted length |
icon |
name of icon that best describes the content |
created |
Date instance when content was created |
modified |
Date instance when content was last updated |
hash |
MD5 hash of the content |
encrypted |
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()
ref.writeTo(result)
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>
<div class="row-fluid">
<crm:render template="web/front/intro.html" parser="gsp"/> (1)
</div>
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()}"/>
</g:link>
</crm:attachments>
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
crm.content.encryption.algorithm
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.
grails.plugins.crm.content.CrmFileResource.AES_ENCRYPTION
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
- 2.4.6
-
Content-Disposition filename is now URL encoded. Content router can be replaced with custom impl to store content in other locations.
- 2.4.5
-
Make it easier to use multiple storage providers in the same application
- 2.4.4
-
withInputStream now returns what the closure returns (was void).
- 2.4.3
-
Fix for threading issues when loading FreeMarker templates from different tenants.
- 2.4.2
-
You can now specify destination root folder when importing content with CrmContentImportService
- 2.4.1
-
Fix for template rendering with specific tenant
- 2.4.0
-
Compatible with Grails 2.4.4
- 2.0.4
-
Fixed class reloading bug caused by missing method
addControllerMethods()
in plugin descriptor. - 2.0.3
-
Tag
attachments
added to thecrm
tag library - 2.0.2
-
Grails tags are now supported when using the
crm:render
tag with optionparser="gsp"
.
Improved handling of illegal characters in file names.
CrmContentImportService#importFiles(…) now works on Windows. - 2.0.1
-
Updated dependency on crm-core to version 2.0.2
- 2.0.0
-
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.