in grails-datamapping-core/src/main/groovy/org/grails/compiler/gorm/DirtyCheckingTransformer.groovy [91:299]
void performInjectionOnAnnotatedClass(SourceUnit source, ClassNode classNode, Class traitToInject = DirtyCheckable) {
// First add a local field that will store the change tracking state. The field is a simple list of property names that have changed
// the field is only added to root clauses that extend from java.lang.Object
final ClassNode changeTrackableClassNode = new ClassNode(traitToInject).getPlainNodeReference()
if (traitToInject != DirtyCheckable) {
changeTrackableClassNode.setSuperClass(new ClassNode(DirtyCheckable).getPlainNodeReference())
}
final MethodNode markDirtyMethodNode = changeTrackableClassNode.getMethod(METHOD_NAME_MARK_DIRTY, new Parameter(ClassHelper.STRING_TYPE, "propertyName"), new Parameter(ClassHelper.OBJECT_TYPE, "newValue"))
ClassNode superClass = classNode.getSuperClass()
boolean shouldWeave = superClass.equals(OBJECT_CLASS_NODE)
ClassNode dirtyCheckableTrait = ClassHelper.make(traitToInject).getPlainNodeReference()
if (traitToInject != DirtyCheckable) {
dirtyCheckableTrait.setSuperClass(new ClassNode(DirtyCheckable).getPlainNodeReference())
}
while(!shouldWeave) {
if(isDomainClass(superClass) || !superClass.getAnnotations(DIRTY_CHECK_CLASS_NODE).isEmpty()) {
break
}
superClass = superClass.getSuperClass()
if(superClass == null || superClass.equals(OBJECT_CLASS_NODE)) {
shouldWeave = true
break
}
}
if(shouldWeave ) {
classNode.addInterface(dirtyCheckableTrait)
if(compilationUnit != null) {
org.codehaus.groovy.transform.trait.TraitComposer.doExtendTraits(classNode, source, compilationUnit);
}
}
PropertyNode transientPropertyNode = classNode.getProperty("transients")
// Now we go through all the properties, if the property is a persistent property and change tracking has been initiated then we add to the setter of the property
// code that will mark the property as dirty. Note that if the property has no getter we have to add one, since only adding the setter results in a read-only property
final propertyNodes = classNode.getProperties()
def staticCompilationVisitor = new StaticCompilationVisitor(source, classNode)
LinkedHashMap<String, GetterAndSetter> gettersAndSetters = [:]
boolean isJavaValidateable = false
for (MethodNode mn in classNode.methods) {
final methodName = mn.name
if(!mn.isPublic() || mn.isStatic() || mn.isSynthetic() || mn.isAbstract()) continue
if (isSetter(methodName, mn)) {
String propertyName = NameUtils.getPropertyNameForGetterOrSetter(methodName)
GetterAndSetter getterAndSetter = getGetterAndSetterForPropertyName(gettersAndSetters, propertyName)
getterAndSetter.setter = mn
} else if (isGetter(methodName, mn)) {
String propertyName = NameUtils.getPropertyNameForGetterOrSetter(methodName)
// if there are any jakarta.validation constraints present
def annotationNodes = mn.annotations
if(!isJavaValidateable && isAnnotatedWithJavaValidationApi(annotationNodes)) {
addAnnotationIfNecessary(classNode, Validated)
isJavaValidateable = true
}
GetterAndSetter getterAndSetter = getGetterAndSetterForPropertyName(gettersAndSetters, propertyName)
getterAndSetter.getter = mn
}
}
boolean hasVersion = false
for (PropertyNode pn in propertyNodes) {
final propertyName = pn.name
if (!pn.isStatic() && pn.isPublic() && !NameUtils.isConfigurational(propertyName)) {
if(isTransient(pn.modifiers) || isDefinedInTransientsNode(propertyName, transientPropertyNode) || isFinal(pn.modifiers)) continue
// don't dirty check id or version
if(propertyName == GormProperties.IDENTITY) {
continue
}
else if(propertyName == GormProperties.VERSION) {
hasVersion = true
continue
}
final GetterAndSetter getterAndSetter = gettersAndSetters[propertyName]
final FieldNode propertyField = pn.getField()
final List<AnnotationNode> allAnnotationNodes = pn.annotations + propertyField.annotations
if(getterAndSetter?.getter != null) {
allAnnotationNodes.addAll(getterAndSetter.getter.annotations)
}
if(hasAnnotation(allAnnotationNodes, GormEntityTransformation.JPA_ID_ANNOTATION_NODE)) {
if(!propertyName.equals(GormProperties.IDENTITY) ) {
// if the property is a JPA @Id but the property name is not id add a transient getter to retrieve the id called getId
if(classNode.getField(GormProperties.IDENTITY) == null && gettersAndSetters[GormProperties.IDENTITY] == null) {
def getIdMethod = new MethodNode(
"getId",
Modifier.PUBLIC,
pn.type.plainNodeReference,
ZERO_PARAMETERS,
null,
GeneralUtils.returnS(GeneralUtils.varX(propertyField))
)
classNode.addMethod(getIdMethod)
getIdMethod.addAnnotation(GormEntityTransformation.JPA_TRANSIENT_ANNOTATION_NODE)
}
}
// skip dirty checking for JPA @Id
continue
}
if(hasAnnotation( allAnnotationNodes, GormEntityTransformation.JPA_VERSION_ANNOTATION_NODE)) {
hasVersion = true
// if the property is a JPA @Version but the property name is not version add a transient getter to retrieve the version called getVersion
if(classNode.getField(GormProperties.VERSION) == null && gettersAndSetters[GormProperties.VERSION] == null) {
def getVersionMethod = new MethodNode(
"getVersion",
Modifier.PUBLIC,
pn.type.plainNodeReference,
ZERO_PARAMETERS,
null,
GeneralUtils.returnS(GeneralUtils.varX(propertyField))
)
classNode.addMethod(getVersionMethod)
getVersionMethod.addAnnotation(GormEntityTransformation.JPA_TRANSIENT_ANNOTATION_NODE)
}
// skip dirty checking for JPA @Version
continue
}
// if there is no explicit getter and setter then one will be generated by Groovy, so we must add these to track changes
if(getterAndSetter == null) {
if(!isJavaValidateable && isAnnotatedWithJavaValidationApi(allAnnotationNodes)) {
addAnnotationIfNecessary(classNode, Validated)
isJavaValidateable = true
}
// first add the getter
ClassNode returnType = resolvePropertyReturnType(pn, classNode)
boolean booleanProperty = ClassHelper.boolean_TYPE.getName().equals(returnType.getName()) || ClassHelper.Boolean_TYPE.getName().equals(returnType.getName())
String fieldName = propertyField.getName()
String getterName = NameUtils.getGetterName(propertyName, false)
MethodNode getter = classNode.getMethod(getterName, ZERO_PARAMETERS)
if(getter == null) {
getter = classNode.addMethod(getterName, PUBLIC, returnType, ZERO_PARAMETERS, null, returnS(varX(fieldName)))
getter.addAnnotation(DIRTY_CHECKED_PROPERTY_ANNOTATION_NODE)
staticCompilationVisitor.visitMethod(
getter
)
if(booleanProperty) {
classNode.addMethod(NameUtils.getGetterName(propertyName, true), PUBLIC, returnType, ZERO_PARAMETERS, null, returnS(varX(fieldName)))
}
}
// now add the setter that tracks changes. Each setters becomes:
// void setFoo(String foo) { markDirty("foo", foo); this.foo = foo }
addDirtyCheckingSetter(classNode, propertyName, fieldName, returnType, markDirtyMethodNode, staticCompilationVisitor)
}
else if(getterAndSetter.hasBoth()) {
// if both a setter and getter are present, we get hold of the setter and weave the markDirty method call into it
weaveIntoExistingSetter(propertyName, getterAndSetter, markDirtyMethodNode)
gettersAndSetters.remove(propertyName)
}
else {
if(getterAndSetter.setter != null) {
weaveIntoExistingSetter(propertyName, getterAndSetter, markDirtyMethodNode)
// there isn't both a getter and a setter then this is not a candidate for persistence, so we eliminate it from change tracking
gettersAndSetters.remove(propertyName)
}
else if(getterAndSetter.getter != null) {
String fieldName = propertyField.getName()
ClassNode returnType = resolvePropertyReturnType(pn, classNode)
addDirtyCheckingSetter(classNode, propertyName, fieldName, returnType, markDirtyMethodNode, staticCompilationVisitor)
}
else {
gettersAndSetters.remove(propertyName)
}
}
}
}
if(!hasVersion && ClassUtils.isPresent("grails.artefact.Artefact") && !classNode.getAnnotations(GormEntityTransformation.JPA_ENTITY_CLASS_NODE).isEmpty()) {
// if the entity is a JPA and has no version property then add a transient one as a stub, this is more to satisfy Grails
def getVersionMethod = new MethodNode(
"getVersion",
Modifier.PUBLIC,
ClassHelper.make(Long),
ZERO_PARAMETERS,
null,
GeneralUtils.returnS(GeneralUtils.constX(0))
)
classNode.addMethod(getVersionMethod)
getVersionMethod.addAnnotation(GormEntityTransformation.JPA_TRANSIENT_ANNOTATION_NODE)
}
// We also need to search properties that are represented as getters with setters. This requires going through all the methods and finding getter/setter pairs that are public
gettersAndSetters.each { String propertyName, GetterAndSetter getterAndSetter ->
if(!NameUtils.isConfigurational(propertyName) && getterAndSetter.hasBoth()) {
weaveIntoExistingSetter(propertyName, getterAndSetter, markDirtyMethodNode)
}
}
}