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. A GR8 CRM plugin have minimal dependencies on other GR8 CRM plugins. However there are some common features that most plugins need. Like working with date/time, caching and multi-tenancy features. Such common features are provided by the crm-core plugin. GR8 CRM plugins are allowed to have compile-time dependency on the crm-core plugin but should avoid dependency on other GR8 CRM plugins if possible.
2. Multitenancy
Multitenancy refers to a principle in software architecture where a single instance of the software runs on a server, serving multiple client organizations (tenants). Multitenancy is contrasted with a multi-instance architecture where separate software instances (or hardware systems) are set up for different client organizations. With a multitenant architecture, a software application is designed to virtually partition its data and configuration, and each client organization works with a customized virtual application instance. Multitenancy is also regarded as one of the essential attributes of cloud computing. Wikipedia
2.1. GR8 CRM has multitenancy built-in
GR8 CRM implements a very simple approach to multitenancy. All domain classes that are tenant-specific
must have a tenantId
property of type Long
. All code that work with tenant specific domain instances
must handle the tenantId property "manually". With a few exceptions there is no automatic filtering going on behind the scenes.
The reason for not having automatic filtering (for example using Hibernate Filters) is that in GR8 CRM a user can access more than one tenant at a given time. Users can have roles and permissions that span multiple tenants. There is still a notion of current tenant but certain features can access other tenants that the user have access to. A calender view is a typical example where a user can see items from different tenants in the same calendar view. Another example is a sales manager that want to view a forecast from all regions in a country where each region is a separate tenant. Therefore tenant filtering is done by business logic in services and controllers. It’s a freedom that brings with it great deal of responsibility and it requires serious test coverage. One missing tenant filter somewhere can result in fatal security issues.
You as a developer are responsible for tenant filtering. Never forget that. And always write tests! |
2.2. Tenant-aware domain class
To make a domain class tenant-aware you add the TenantEntity
annotation to the class.
@TenantEntity
class Person {
String name
}
This annotation trigger an AST transformation that adds a Long tenantId
property to the domain class.
The property is set to TenantUtils.getTenant()
when the domain class is instantiated.
2.3. Current Tenant
GR8 CRM resolves the current executing tenant from the HTTP request or the HTTP session.
The tenant resolver sets the current tenant in a ThreadLocal variable and you use the TenantUtils.getTenant()
to access it.
2.4. Tenant aware query
def joe = Person.findByTenantIdAndName(TenantUtils.tenant, "Joe Average")
3. Security
GR8 CRM is designed to work with different security implementations. A core plugin called crm-security manages generic application security and it delegates implementation specific calls to a sub-plugin. One such sub-plugin is crm-securiy-shiro that uses Apache Shiro for user, role and permission management.
The next security sub-plugin will be based on Spring Security and we look forward to contributions. ;-) |
4. CRM Core Plugin
The crm-core
plugin does not provide much visual functionality. It contains common features that other GR8 CRM plugins use.
Normally you don’t include crm-core
in your application’s BuildConfig.groovy
, instead you include dependencies on other
high-level GR8 CRM plugins that transitively depends on crm-core. However if you are a GR8 CRM plugin developer you may depend on crm-core directly.
5. Services
The crm-core plugin provide a few services with common functionality used by other GR8 CRM plugins.
5.1. CrmCoreService
String getReferenceIdentifier(Object object)
Return a string representation of a domain instance. This string contains both the domain type and the primary key.
The format is "domainName@primaryKey", for example: "customerOrder@42".
This makes it possible to store the reference in a String
property and re-create the domain instance later.
Because the domain type is stored in a readable form it is also possible to query the property to find
all objects of the same type or query for an exact match on a specific domain instance.
SomeDomain.findAllByReferenceLike("customerOrder%") // Find all objects that contains a customer order
SomeDomain.findByReference("customerOrder@42") // Find the object that contains customer order with id 42.
def getReference(String identifier)
The method getReference
is the opposite of getReferenceIdentifier()
.
Given a reference identifier the method return a re-constructed domain instance.
def order = crmCoreService.getReference("customerOrder@42")
5.2. CrmPluginService
void registerView(final String controller, final String action, final String location, final Map params)
Inject a custom GSP view in an existing GSP view.
List getViews(final String controller, final String action, final String location)
Return a list of custom views injected in a GSP view with registerView()
.
6. Utilities
6.1. TenantUtils
This utility class is the most used utility class in GR8 CRM. It’s used to set and get the current executing tenant in a multi-tenant environment.
Every plugin in the GR8 CRM suite is multi-tenant aware, this means that multiple users can work in the same database but they will only see their own information. Every user work in a safe watertight compartment. But multi-tenancy in GR8 CRM is not implemented at the database (Hibernate) layer. It’s implemented in application logic. This means that the developer is responsible for retrieving information about the current executing tenant and restrict queries to a tenant.
The reason for this design is that the multi-tenancy support in GR8 CRM extends beyond simple one-one relationship between a user and a tenant. One user can have access to multiple tenants simultaneously. A user always execute in one tenant, but the user may have permission to view information in other tenants. For example in a calendar view appointments/tasks from multiple tenants could be overlaid on top of each other. Statistic reports and other "management" type of queries may span multiple tenants. Therefore it’s up to the developer of the application or plugin to decide how a query should be restricted.
public static Long getTenant()
Return the ID of current executing tenant.
public static Object withTenant(Long tenantId, Closure work)
Execute some work on behalf of a tenant. The tenant will be saved in a ThreadLocal variable. The previous tenant will be restored after this method completes. The return value is the return value of the Closure passed to the method.
If the Closure spawns a new thread, the tenant ID must be passed to the new thread and the new thread must call TenantUtils.withTenant(). Otherwise the new thread will not execute in a tenant. |
HTTP requests to Grails controller actions will automatically execute in a tenant because CrmTenantFilters will intercept the request and set the correct tenant, based on information stored in the user’s HTTP session. So you don’t need to use TenantUtils.withTenant() in normal controller/service code but for tasks executing outside of a HTTP request you must, for example in Quartz background jobs.
6.2. DateUtils
static Date parseDate(String input, TimeZone tz = UTC)
Parse a date string and return a date instance.
static String formatDate(final Date date, TimeZone tz = UTC)
Format a date instance as a String.
6.3. SearchUtils
static String wildcard(String q)
Replaces asterisk (*) in the input string with '%'.
6.4. WebUtils
static void shortCache(final HttpServletResponse response)
Cache a HTTP response for 2 minutes.
static void defaultCache(final HttpServletResponse response)
Cache a HTTP response for 10 minutes.
static String bytesFormatted(final Number b)
Returns a human friendly representation of number of bytes.
-
0-1024 is presented as is
-
1025-10240000 is presented as kB
-
> 10240000 is presented as MB
7. Abstract Domain Classes
The crm-core plugin provides some useful domain classes that are abstract and can be extended in applications or plugins.
7.1. CrmLookupEntity
Lookup entities are domain classes that hold reference/lookup information. For example Customer Type or Project Category. All lookup entities in GR8 CRM plugins extend CrmLookupEntity and get the following properties:
int orderIndex // Used for sorting list
boolean enabled // False means disabled/do-not-use
String name // 80 chars
String param // 20 chars
String icon // 100 chars
String description // 2000 chars
8. Exceptions
8.1. CrmValidationException
This exception is thrown by services when data binding fails. The exception instance can carry one or more domain instances involved in the data binding. The constructor takes a message key and a variable number of domain instance arguments. The controller that called the service can pick up domain instances from the exception and render validation errors in the browser.
MyDomain save(Map params) {
...
if(! myDomain.save()) {
throw new CrmValidationException('myDomain.validation.error', myDomain)
}
myDomain
}
def saveAction() {
MyDomain domainInstance
try {
domainInstance = myService.save(params)
} catch(CrmValidationException e) {
domainInstance = e.domainInstance
}
if(domainInstance.hasErrors()) {
render view: "create", model: [bean: domainInstance]
} else {
redirect action: "show", id: domainInstance.ident()
}
}
9. AST Transformations
The crm-core plugin provides a collection of useful AST Transformations. Most of the transformations are related to domain classes.
9.1. @TenantEntity
This transformation adds a Long tenantId
property to the domain class.
The property is set to TenantUtils.getTenant()
when the domain class is instantiated.
import grails.plugins.crm.core.TenantEntity
@TenantEntity
class Customer {
String name
String toString() {
"#$tenantId $name"
}
}
9.2. @AuditEntity
This transformation adds dateCreated
and lastUpdated
Date properties to the domain class.
import grails.plugins.crm.core.AuditEntity
@AuditEntity
class Author {
String name
String toString() {
"Author $name last updated ${lastUpdated ?: dateCreated}"
}
}
9.3. @UuidEntity
This transformation adds a String guid
property to the domain class.
The property is set to UUID.randomUUID().toString()
when the domain class is instantiated.
A database index can be created on the guid
column by speficying index = true
in the annotation.
import grails.plugins.crm.core.UuidEntity
@UuidEntity(index = true)
class MyEvent {
String toString() {
"[$guid]"
}
}
10. Changes
- 2.4.4
-
Increased length of address columns → db migration!
- 2.4.3
-
Added method
SearchUtils#dateQuery()
andSearchUtils#getPaginationSize()
. - 2.4.2
-
Added interfaces for working with address entities, CrmAddressInformation and CrmMutableAddressInformation.
- 2.4.1
-
Added support for database index in
UuidEntity
domain classes (thanks aberbenni). - 2.4.0
-
First version compatible with Grails 2.4.4.
- 2.0.2
-
New exception type CrmValidationException to use in services that persist domain instances.
- 2.0.1
-
Interface
CrmContactInformation
changed. New getter methodLong getCompanyId()
. - 2.0.0
-
First public release.
11. License
This plugin is licensed with Apache License version 2.0
12. Source Code
The source code for this plugin is available at https://github.com/goeh/grails-crm-core
13. Contributing
Please report issues or suggestions.
Want to improve the plugin: Fork the repository and send a pull request.