void performInjectionOnAnnotatedClass()

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)
            }
        }
    }