Last active
September 18, 2019 14:47
-
-
Save passionfruit18/d0f78e1cc4aea41a0e4ab736df842ab3 to your computer and use it in GitHub Desktop.
Grails Join Class Generator– generates something with structure similar to the UserRole class generated by Spring Security Core Plugin 2.0.0
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Generates something with structure similar to the UserRole class generated by Spring Security Core Plugin | |
* Except "removeAll" methods, which were overloaded, have been replaced by separately named methods for each field | |
* representing a Joined Class | |
*/ | |
class GrailsJoinClassGenerator { | |
static String joinClassCode(JoinClassRep joinClassRep) { | |
String classInnerBlock = | |
[serialStuff(), | |
fields(joinClassRep.joinedClassRepList), | |
constructor(joinClassRep), | |
equalsAndHashCode(joinClassRep), | |
getExistsCriteria(joinClassRep), | |
create(joinClassRep), | |
removeMethods(joinClassRep), | |
constraints(joinClassRep), | |
mapping(joinClassRep)].join('\n\n') | |
List<String> imports = ["grails.gorm.DetachedCriteria", | |
"groovy.transform.ToString", | |
"org.apache.commons.lang.builder.HashCodeBuilder"].collect {"import ${it}"} | |
nestedBlock( | |
[imports.join('\n') + '\n\n' + | |
"@ToString(cache=true, includeNames=true, includePackage=false)", | |
"class ${joinClassRep.joinClassName} implements Serializable {"].join('\n'), | |
classInnerBlock, | |
"}" | |
) | |
} | |
static String serialStuff() { | |
"\nprivate static final long serialVersionUID = 1" | |
} | |
/** | |
* Joined Classes as fields of Join Class | |
* @param joinedClassRepList | |
* @return | |
*/ | |
static String fields(List<JoinedClassRep> joinedClassRepList) { | |
joinedClassRepList | |
.collect {"${it.joinedClassName} ${it.joinedClassFieldName}"} | |
.join('\n') | |
} | |
/** | |
* Constructor using Joined Class Fields | |
* @param joinClassRep | |
* @return | |
*/ | |
static String constructor(JoinClassRep joinClassRep) { | |
String arguments = joinClassRep.joinedClassRepList | |
.collect {"${it.joinedClassName} ${it.joinedClassAbbrev}" } | |
.join(', ') | |
String assignment = "this()\n" + joinClassRep.joinedClassRepList | |
.collect {"${it.joinedClassFieldName} = ${it.joinedClassAbbrev}" } | |
.join('\n') | |
nestedBlock( | |
"${joinClassRep.joinClassName}(${arguments}) {", | |
assignment, | |
"}" | |
) | |
} | |
/** | |
* Equals & Hashcode based on ids of Joined Classes | |
* @param joinClassRep | |
* @return | |
*/ | |
static String equalsAndHashCode(JoinClassRep joinClassRep) { | |
equals(joinClassRep) + "\n\n" + hashCode(joinClassRep) | |
} | |
static String equals(JoinClassRep joinClassRep) { | |
String instanceofComparison = nestedBlock( | |
"if (!(other instanceof ${joinClassRep.joinClassName})) {", | |
"return false", | |
"}" | |
) | |
String idComparison = joinClassRep.joinedClassRepList.collect { | |
"other.${it.joinedClassFieldName}?.id == ${it.joinedClassFieldName}?.id" | |
}.join(" && ") | |
String comparisonCode = instanceofComparison + "\n\n" + idComparison | |
nestedBlock( | |
["@Override", | |
"boolean equals(other) {"].join('\n'), | |
comparisonCode, | |
"}" | |
) | |
} | |
static String hashCode(JoinClassRep joinClassRep) { | |
String hashBuildingCode = "def builder = new HashCodeBuilder()\n" + | |
joinClassRep.joinedClassRepList.collect { | |
"if (${it.joinedClassFieldName}) builder.append(${it.joinedClassFieldName}.id)" | |
}.join('\n') + | |
"\nbuilder.toHashCode()" | |
nestedBlock( | |
["@Override", | |
"int hashCode() {"].join('\n'), | |
hashBuildingCode, | |
"}" | |
) | |
} | |
/** | |
* Define the "get", "exists", and "criteriaFor" methods | |
* @param joinClassRep | |
* @return | |
*/ | |
static String getExistsCriteria(JoinClassRep joinClassRep) { | |
String idArguments = idArguments(joinClassRep) | |
String idArgumentsPassedIn = joinClassRep.joinedClassRepList.collect { | |
"${it.joinedClassFieldNameIdForm}" | |
}.join(', ') | |
String getMethod = nestedBlock( | |
"static ${joinClassRep.joinClassName} get(${idArguments}) {", | |
"criteriaFor(${idArgumentsPassedIn}).get()", | |
"}") | |
String existsMethod = nestedBlock( | |
"static boolean exists(${idArguments}) {", | |
"criteriaFor(${idArgumentsPassedIn}).count()", | |
"}" | |
) | |
getMethod + "\n\n" + existsMethod + "\n\n" + criteriaForMethod(joinClassRep) | |
} | |
static String idArguments(JoinClassRep joinClassRep) { | |
joinClassRep.joinedClassRepList.collect { | |
"long ${it.joinedClassFieldNameIdForm}" | |
}.join(', ') | |
} | |
static String criteriaForMethod(JoinClassRep joinClassRep) { | |
String idArguments = idArguments(joinClassRep) | |
String equalityStatements = joinClassRep.joinedClassRepList.collect { | |
"${it.joinedClassFieldName} == ${it.joinedClassName}.load(${it.joinedClassFieldNameIdForm})" | |
}.join(' &&\n') | |
String criteriaDeclaration = nestedBlock( | |
"${joinClassRep.joinClassName}.where {", | |
equalityStatements, | |
"}" | |
) | |
nestedBlock( | |
"private static DetachedCriteria criteriaFor(${idArguments}) {", | |
criteriaDeclaration, | |
"}" | |
) | |
} | |
static String create(JoinClassRep joinClassRep) { | |
String createArgs = joinClassRep.joinedClassRepList.collect { | |
"${it.joinedClassName} ${it.joinedClassFieldName}" | |
}.join(', ') + booleanFlushFalse | |
String constructorArgs = joinClassRep.joinedClassRepList.collect { | |
"${it.joinedClassFieldName}: ${it.joinedClassFieldName}" | |
}.join(', ') | |
String newInstanceCode =[ | |
"def instance = new ${joinClassRep.joinClassName}(${constructorArgs})", | |
"instance.save(flush: flush, insert: true)", | |
"instance" | |
].join('\n') | |
nestedBlock( | |
"static ${joinClassRep.joinClassName} create(${createArgs}) {", | |
newInstanceCode, | |
"}" | |
) | |
} | |
static String booleanFlushFalse = ", boolean flush = false" | |
static String removeMethods(JoinClassRep joinClassRep) { | |
removeMethod(joinClassRep) + "\n\n" + joinClassRep.joinedClassRepList.collect{ | |
removeAllFor(it, joinClassRep) | |
}.join("\n\n") | |
} | |
/** | |
* Method to remove instance of Join Class | |
* @param joinClassRep | |
* @return | |
*/ | |
static String removeMethod(JoinClassRep joinClassRep) { | |
String removeArgs = joinClassRep.joinedClassRepList.collect { | |
"${it.joinedClassName} ${it.joinedClassAbbrev}" | |
}.join(", ") + booleanFlushFalse | |
String anyNull = joinClassRep.joinedClassRepList.collect { | |
"${it.joinedClassAbbrev} == null" | |
}.join(" || ") | |
String criteriaMatching = joinClassRep.joinedClassRepList.collect { | |
"${it.joinedClassFieldName} == ${it.joinedClassAbbrev}" | |
}.join(" && ") | |
String removeCode = [ | |
"if (${anyNull}) return false", | |
"int rowCount = ${joinClassRep.joinClassName}.where { ${criteriaMatching} }.deleteAll()", | |
"if (flush) { ${joinClassRep.joinClassName}.withSession { it.flush() } }", | |
"rowCount" | |
].join('\n\n') | |
nestedBlock( | |
"static boolean remove(${removeArgs}) {", | |
removeCode, | |
"}" | |
) | |
} | |
/** | |
* Method to remove all instance of Join Class with one particular field | |
* @param joinedClassRep | |
* @return | |
*/ | |
static String removeAllFor(JoinedClassRep joinedClassRep, JoinClassRep joinClassRep) { | |
String removeAllCode = [ | |
"if (${joinedClassRep.joinedClassAbbrev} == null) return", | |
"${joinClassRep.joinClassName}.where { ${joinedClassRep.joinedClassFieldName} == ${joinedClassRep.joinedClassAbbrev} }.deleteAll()", | |
"if (flush) { ${joinClassRep.joinClassName}.withSession { it.flush() } }" | |
].join('\n\n') | |
nestedBlock( | |
"static void removeAllFor${joinedClassRep.joinedClassFieldName.capitalize()}(" + | |
"${joinedClassRep.joinedClassName} ${joinedClassRep.joinedClassAbbrev}${booleanFlushFalse}) {", | |
removeAllCode, | |
"}" | |
) | |
} | |
static String constraints(JoinClassRep joinClassRep) { | |
String jcVar = joinClassRep.joinClassAbbrev | |
/** | |
* The first joined class is used as the field to which the validator is attached | |
*/ | |
JoinedClassRep firstJoinedClass = joinClassRep.joinedClassRepList[0] | |
/** | |
* The rest of the joined classes | |
*/ | |
List<JoinedClassRep> theRest = joinClassRep.joinedClassRepList.drop(1) | |
String nullCheck = theRest.collect { | |
String field = it.joinedClassFieldName | |
"${jcVar}.${field} == null || ${jcVar}.${field}.id == null" | |
}.join(" || ") | |
String ids = (["${firstJoinedClass.joinedClassAbbrev}.id"] + theRest.collect { | |
"${jcVar}.${it.joinedClassFieldName}.id" | |
}).join(', ') | |
String validationBlock = [ | |
"if (${nullCheck}) return", | |
"boolean existing = false", | |
nestedBlock( | |
"${joinClassRep.joinClassName}.withNewSession {", | |
"existing = ${joinClassRep.joinClassName}.exists(${ids})", | |
"}" | |
), | |
nestedBlock( | |
"if (existing) {", | |
"return '${joinClassRep.joinClassName.uncapitalize()}.exists'", | |
"}" | |
) | |
].join('\n') | |
String validatorBlock = nestedBlock( | |
"${firstJoinedClass.joinedClassFieldName} validator: " + | |
"{${firstJoinedClass.joinedClassName} ${firstJoinedClass.joinedClassAbbrev}, " + | |
"${joinClassRep.joinClassName} ${jcVar} ->", | |
validationBlock, | |
"}" | |
) | |
nestedBlock( | |
"static constraints = {", | |
validatorBlock, | |
"}" | |
) | |
} | |
static String mapping(JoinClassRep joinClassRep) { | |
String fields = joinClassRep.joinedClassRepList.collect { | |
"'${it.joinedClassFieldName}'" | |
}.join(', ') | |
nestedBlock( | |
"static mapping = {", | |
"id composite: [${fields}]\n" + | |
"version false", | |
"}" | |
) | |
} | |
/** | |
* Returns string s with n tabs preceding it | |
* @param s | |
* @param n | |
* @return | |
*/ | |
static String tabbedN(String s, int n) { | |
return '\t' * n + s | |
} | |
/** | |
* Pre-tabs each line of string s, n times | |
* @param s | |
* @param n | |
* @return | |
*/ | |
static String linesTabbedN(String s, int n) { | |
return s.split('\n').collect{tabbedN(it, n)}.join('\n') | |
} | |
/** | |
* Pre-tabs each line of string s | |
* @param s | |
* @param n | |
* @return | |
*/ | |
static String linesTabbed(String s) { | |
return s.split('\n').collect{'\t' + it}.join('\n') | |
} | |
/** | |
* Prints "begin", then "block" at one tab deeper, and then "end". | |
* @param begin | |
* @param block | |
* @param end | |
* @return | |
*/ | |
static String nestedBlock(String begin, String block, String end) { | |
begin + '\n' + linesTabbed(block) + '\n' + end | |
} | |
} | |
/** | |
* Representation of the JoinClass to be generated | |
*/ | |
class JoinClassRep { | |
/** | |
* Name of joinClass, e.g. "UserRole" | |
*/ | |
String joinClassName | |
/** | |
* Abbreviated name of joinClass, to be used as variable name, e.g. "ur" | |
*/ | |
String joinClassAbbrev | |
/** | |
* List of classes to be joined. | |
* This should have two or more entries, or else it doesn't really make sense to have a join class. | |
* Code generation is undefined for 0 or 1 entries. | |
*/ | |
List<JoinedClassRep> joinedClassRepList | |
JoinClassRep(String joinClassName, String joinClassAbbrev, List<JoinedClassRep> joinedClassRepList) { | |
this.joinClassName = joinClassName | |
this.joinClassAbbrev = joinClassAbbrev | |
this.joinedClassRepList = joinedClassRepList | |
checkConsistency() | |
} | |
boolean checkConsistency() { | |
if (joinedClassRepList.size() < 2) { | |
throw new Exception("Joined Classes number less than 2– nonsense!") | |
} | |
Set<String> abbrevNames = [joinClassAbbrev] | |
abbrevNames.addAll(joinedClassRepList*.joinedClassAbbrev) | |
if (abbrevNames.size() < joinedClassRepList.size() + 1) { | |
throw new Exception("There is a duplicate abbreviated name among JoinClassRep & JoinedClassReps!") | |
} | |
Set<String> fieldNames = [] | |
fieldNames.addAll(joinedClassRepList*.joinedClassFieldName) | |
if (fieldNames.size() < joinedClassRepList.size() ) { | |
throw new Exception("There is a duplicate field name among JoinedClassReps!") | |
} | |
} | |
} | |
/** | |
* Representation of class to be joined | |
*/ | |
class JoinedClassRep { | |
/** | |
* e.g. "User" | |
*/ | |
String joinedClassName | |
/** | |
* e.g. "u"– to be used as a variable name in some cases | |
*/ | |
String joinedClassAbbrev | |
/** | |
* Field name for the joined class | |
* e.g. "user" | |
* If null, the uncapitalized version of joinedClassName will be used | |
* This can be used to define a field name in case e.g. the Join Class joins two instance of the same Joined Class. | |
* e.g. "user1" and "user2" | |
*/ | |
String joinedClassFieldName = null | |
/** | |
* e.g. "user" | |
* @return | |
*/ | |
String getJoinedClassFieldName() { | |
if (joinedClassFieldName) { | |
joinedClassFieldName | |
} | |
else { | |
joinedClassName.uncapitalize() | |
} | |
} | |
/** | |
* e.g. "userId" | |
* @return | |
*/ | |
String getJoinedClassFieldNameIdForm() { | |
getJoinedClassFieldName() + "Id" | |
} | |
JoinedClassRep(String joinedClassName, String joinedClassAbbrev, String joinedClassFieldName = null) { | |
this.joinedClassName = joinedClassName | |
this.joinedClassAbbrev = joinedClassAbbrev | |
this.joinedClassFieldName = joinedClassFieldName | |
} | |
} | |
class Main { | |
static void run() { | |
roleHierarchy() | |
} | |
static void roleHierarchy() { | |
JoinClassRep roleHierarchyRep = new JoinClassRep( | |
"RoleHierarchy", | |
"rh", | |
[ | |
new JoinedClassRep("Role", "r1", "role1"), | |
new JoinedClassRep("Role", "r2", "role2") | |
] | |
) | |
String roleHierarchyCode = GrailsJoinClassGenerator.joinClassCode(roleHierarchyRep) | |
println roleHierarchyCode | |
} | |
static void userRole() { | |
JoinClassRep userRoleJCRep = new JoinClassRep( | |
"UserRole", | |
"ur", | |
[ | |
new JoinedClassRep("User", "u"), | |
// new JoinedClassRep("User", "u1", "user1"), | |
// new JoinedClassRep("User", "u2", "user2"), | |
new JoinedClassRep("Role", "r") | |
] | |
) | |
String userRoleCode = GrailsJoinClassGenerator.joinClassCode(userRoleJCRep) | |
println userRoleCode | |
} | |
} | |
Main.run() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment