Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

God mode plus shared objects features have been added #2

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 53 additions & 17 deletions MultiTenantCoreGrailsPlugin.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@ import grails.plugin.multitenant.core.DomainNameDatabaseTenantResolver
import grails.plugin.multitenant.core.DomainNamePropertyTenantResolver
import grails.plugin.multitenant.core.CurrentTenantThreadLocal

import grails.plugin.multitenant.CurrentTenantWithMasterMode
import grails.plugin.multitenant.TenantEventHandlerWithMasterMode

class MultiTenantCoreGrailsPlugin {
def version = "1.0.0"
def version = "1.0.1"
def grailsVersion = "1.3.0 > *"
def dependsOn = [falconeUtil: "1.0"]
def author = "Eric Martineau, Scott Ryan"
Expand Down Expand Up @@ -62,14 +65,21 @@ class MultiTenantCoreGrailsPlugin {
}

} else {

//This registers hibernate events that force filtering on domain classes
//In single tenant mode, the records are automatically filtered by different
//data sources.
tenantEventHandler(TenantEventHandler) {
sessionFactory = ref("sessionFactory")
currentTenant = ref("currentTenant")
}
if(ConfigurationHolder.config.tenant.withMasterMode){
tenantEventHandler(TenantEventHandlerWithMasterMode) {
sessionFactory = ref("sessionFactory")
currentTenant = ref("currentTenant")
}
}else {
tenantEventHandler(TenantEventHandler) {
sessionFactory = ref("sessionFactory")
currentTenant = ref("currentTenant")
}
}
}
//Bean container for all multi-tenant beans
tenantBeanContainer(TenantBeanContainer) {
Expand All @@ -82,9 +92,15 @@ class MultiTenantCoreGrailsPlugin {
def resolverType = ConfigHelper.get("request") {it.tenant.resolver.type}
if (resolverType == "request") {
//This implementation
currentTenant(CurrentTenantThreadLocal) {
eventBroker = ref("eventBroker")
}
if(ConfigurationHolder.config.tenant.withMasterMode){
currentTenant(CurrentTenantWithMasterMode) {
eventBroker = ref("eventBroker")
}
} else {
currentTenant(CurrentTenantThreadLocal) {
eventBroker = ref("eventBroker")
}
}

def requestResolverType = ConfigHelper.get("config") {it.tenant.resolver.request.dns.type}
if (requestResolverType == "config") {
Expand All @@ -110,16 +126,33 @@ class MultiTenantCoreGrailsPlugin {
//Listen for criteria created events
hibernate.criteriaCreated("tenantFilter") {
CriteriaContext context ->

if(ConfigurationHolder.config.tenant.withMasterMode && ctx.currentTenant.isMasterMode()){
return
}

boolean hasAnnotation = TenantUtils.isAnnotated(context.entityClass)
if (context.entityClass == null || hasAnnotation) {
boolean hasSharedAnnotation = TenantUtils.isAnnotatedAsShared(context.entityClass)
if (context.entityClass == null || (hasAnnotation || hasSharedAnnotation)) {
final Integer tenant = ctx.currentTenant.get();
context.criteria.add(Expression.eq("tenantId", tenant));
if(hasAnnotation){
context.criteria.add(Expression.eq("tenantId", tenant));
} else if(hasSharedAnnotation){
if(!context.criteria.iterateSubcriteria().toList().find{it.path == "tenants"}){
context.criteria.createCriteria("tenants").add(Expression.eq("tenantId", tenant))
}
}
}
}

//Listen for query created events
hibernate.queryCreated("tenantFilter") {
Query query ->
hibernate.queryCreated("tenantFilter") { Query query ->

if(ConfigurationHolder.config.tenant.withMasterMode && ctx.currentTenant.isMasterMode()){
return
}


for (String param: query.getNamedParameters()) {
if ("tenantId".equals(param)) {
query.setParameter("tenantId", ctx.currentTenant.get(), new IntegerType());
Expand Down Expand Up @@ -194,12 +227,15 @@ class MultiTenantCoreGrailsPlugin {
def doWithDynamicMethods = {ctx ->

if (ConfigurationHolder.config.tenant.mode != "singleTenant") {

//Add a nullable contraint for tenantId.
application.domainClasses.each {DefaultGrailsDomainClass domainClass ->
domainClass.constraints?.get("tenantId")?.applyConstraint(ConstrainedProperty.NULLABLE_CONSTRAINT, true);
domainClass.clazz.metaClass.beforeInsert = {
if (tenantId == null) tenantId = 0
}
if(domainClass.clazz.metaClass.properties.find {p -> p.name == "tenantId"}){
domainClass.constraints?.get("tenantId")?.applyConstraint(ConstrainedProperty.NULLABLE_CONSTRAINT, true);
domainClass.clazz.metaClass.beforeInsert = {
if (tenantId == null) tenantId = 0
}
}
}
}

Expand Down
9 changes: 9 additions & 0 deletions grails-app/domain/grails/plugin/multitenant/TenantId.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package grails.plugin.multitenant

class TenantId {

Integer tenantId

static constraints = {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package grails.plugin.multitenant

import grails.plugin.multitenant.core.CurrentTenantThreadLocal

class CurrentTenantWithMasterMode extends CurrentTenantThreadLocal {

static ThreadLocal<Boolean> masterMode = new ThreadLocal<Boolean>()

def isMasterMode(){
if(masterMode.get() == null){
masterMode.set(false)
}
masterMode.get()
}

def setMasterMode(gm) {
masterMode.set(gm as Boolean)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package grails.plugin.multitenant

import grails.plugin.multitenant.core.hibernate.TenantEventHandler
import org.hibernate.event.*
import org.hibernate.event.LoadEventListener.LoadType
import org.hibernate.tuple.StandardProperty

import org.hibernate.SessionFactory
import util.hibernate.HibernateEventUtil
import org.hibernate.event.InitializeCollectionEventListener
import grails.plugin.multitenant.TenantId
import grails.plugin.multitenant.core.util.TenantUtils

import org.codehaus.groovy.grails.commons.ConfigurationHolder

class TenantEventHandlerWithMasterMode extends TenantEventHandler{


private Map<String, Class> reflectedCache = [:]

public TenantEventHandlerWithMasterMode() {
}

public boolean onPreInsert(PreInsertEvent preInsertEvent) {
def shouldFail = false
boolean hasAnnotation = TenantUtils.isAnnotated(preInsertEvent.getEntity().getClass())
boolean hasSharedAnnotation = false
if (ConfigurationHolder.config.tenant.mode != "singleTenant") {
hasSharedAnnotation = TenantUtils.isAnnotatedAsShared(preInsertEvent.getEntity().getClass())
}

def setTenantId

def findTenantIdIndex = { where ->
int result = -1
for(def property in where) {
result++
if (property.getName() == "tenantId") {
break
}
}
result
}

if(hasAnnotation){
setTenantId = preInsertEvent.getEntity().tenantId

if (setTenantId == 0 || setTenantId == null) {
preInsertEvent.getEntity().tenantId = currentTenant.get()
StandardProperty[] properties = preInsertEvent.getPersister().getEntityMetamodel().getProperties()
int tenandIdIndex = findTenantIdIndex(properties)
if (tenandIdIndex > -1) {
preInsertEvent.getState()[tenandIdIndex] = currentTenant.get()
}
} else {
if (setTenantId != currentTenant.get()) {
shouldFail = true
return shouldFail
}
}
} else if(hasSharedAnnotation){
setTenantId = preInsertEvent.getEntity().tenants*.tenantId
if(!setTenantId){
def tenantToAdd = TenantId.findByTenantId(currentTenant.get()) ?: new TenantId(tenantId: currentTenant.get())
preInsertEvent.getEntity().addToTenants(tenantToAdd)

} else {
if (!(currentTenant.get() in setTenantId)) {
shouldFail = true
return shouldFail
}
}
}
return shouldFail;
}



public void onLoad(LoadEvent event, LoadType loadType) {
if (ConfigurationHolder.config.tenant.withMasterMode && !currentTenant.isMasterMode() && annotated(event.getEntityClassName())) {
Object result = event.getResult()
if (result != null) {
int currentTenant = currentTenant.get()
def violates = attemptingTenantViolation(event.getEntityClassName(), result, currentTenant)
if (violates && !event.isAssociationFetch()) {
println "Trying to load record from a different app (should be ${currentTenant} but was ...)"
event.setResult null
} else if(!event.isAssociationFetch()){
event.setResult result
}
}
}
}


/**
* Checks before deleting a record that the record is for the current tenant. THrows an exception otherwise
*/
public boolean onPreDelete(PreDeleteEvent event) {
boolean shouldFail = attemptingTenantViolation(event.getEntity().getClass().getName(), event.getEntity(), currentTenant.get())
if(shouldFail){
println "Failed Delete Because TenantId Doesn't Match"
}
return shouldFail;
}

public boolean onPreUpdate(PreUpdateEvent preUpdateEvent) {
boolean shouldFail = attemptingTenantViolation(preUpdateEvent.getEntity().getClass().getName(), preUpdateEvent.getEntity(), currentTenant.get())
if(shouldFail){
println "Failed Update Because TenantId Doesn't Match"
}
return shouldFail;
}


private Class getClassFromName(String className) {
if (!reflectedCache.containsKey(className)) {
Class aClass = this.class.classLoader.loadClass("${className}")
reflectedCache.put(className, aClass)
}
return reflectedCache.get(className)
}

private annotated(entityClassName){
boolean hasAnnotation = TenantUtils.isAnnotated(getClassFromName(entityClassName))
boolean hasSharedAnnotation = false
if (ConfigurationHolder.config.tenant.mode != "singleTenant") {
hasSharedAnnotation = TenantUtils.isAnnotatedAsShared(getClassFromName(entityClassName))
}

hasAnnotation || hasSharedAnnotation
}

private attemptingTenantViolation(entityClassName, entity, currentTenantId){
if (ConfigurationHolder.config.tenant.withMasterMode && !currentTenant.isMasterMode() && annotated(entityClassName)) {
def loaded
def hasAnnotation = TenantUtils.isAnnotated(getClassFromName(entityClassName))
if(hasAnnotation){
loaded = [entity.tenantId]
} else {
loaded = entity.tenants*.tenantId
}
return !(currentTenantId in loaded)
}
false
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package grails.plugin.multitenant.core.groovy.compiler;

import org.codehaus.groovy.transform.GroovyASTTransformationClass;

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Retention;

/**
* Annotation used to mark domain classes that should be converted to multi-tenant.
*
* Currently, this annotation will add a tenantId property to the class.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@GroovyASTTransformationClass("grails.plugin.multitenant.core.groovy.compiler.SharedTenantASTTransformation")
public @interface MultiTenantShared {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package grails.plugin.multitenant.core.groovy.compiler;

import org.codehaus.groovy.transform.ASTTransformation;
import org.codehaus.groovy.transform.GroovyASTTransformation;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.grails.compiler.injection.GrailsASTUtils;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;
import org.codehaus.groovy.ast.expr.ArrayExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MapExpression;
import org.codehaus.groovy.ast.expr.MapEntryExpression;
import org.codehaus.groovy.ast.expr.ClassExpression;
import org.codehaus.groovy.ast.PropertyNode;

import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.Set;

import java.lang.reflect.Modifier;

import grails.plugin.multitenant.TenantId;

/**
* Performs an ast transformation on a class - adds a tenantId property to the subject class.
*/
@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION )
public class SharedTenantASTTransformation implements ASTTransformation {
// ========================================================================================================================
// Static Fields
// ========================================================================================================================

private static final Log LOG = LogFactory.getLog(TenantASTTransformation.class);
private static final String key = "tenants";

// ========================================================================================================================
// Public Instance Methods
// ========================================================================================================================

public void visit(ASTNode[] astNodes, SourceUnit sourceUnit) {
for (ASTNode astNode : astNodes) {
if (astNode instanceof ClassNode) {
ClassNode classNode = (ClassNode) astNode;
final boolean hasTenants = GrailsASTUtils.hasProperty(classNode, key);
final boolean hasHasMany = GrailsASTUtils.hasProperty(classNode, "hasMany");
if(!hasTenants){
if(!hasHasMany){
MapExpression me = new MapExpression();
me.addMapEntryExpression(new ConstantExpression(key), new ClassExpression(new ClassNode(TenantId.class)));
classNode.addProperty("hasMany", Modifier.PUBLIC | Modifier.STATIC, new ClassNode(java.util.Map.class), me, null, null);
} else {
PropertyNode hm = classNode.getProperty("hasMany");
MapExpression me = (MapExpression)hm.getInitialExpression();
me.addMapEntryExpression(new ConstantExpression(key), new ClassExpression(new ClassNode(TenantId.class)));
}
classNode.addProperty(new PropertyNode(key, Modifier.PUBLIC, new ClassNode(Set.class), classNode, null, null, null));
}
}
}
}
}
Loading