io.micronaut.ast.groovy.InjectVisitor.groovy Maven / Gradle / Ivy
/*
* Copyright 2017-2021 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.ast.groovy
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import groovy.transform.PackageScope
import io.micronaut.aop.Adapter
import io.micronaut.aop.Around
import io.micronaut.aop.Interceptor
import io.micronaut.aop.InterceptorBinding
import io.micronaut.aop.InterceptorKind
import io.micronaut.aop.Introduction
import io.micronaut.aop.internal.intercepted.InterceptedMethodUtil
import io.micronaut.aop.writer.AopProxyWriter
import io.micronaut.ast.groovy.annotation.GroovyAnnotationMetadataBuilder
import io.micronaut.ast.groovy.utils.AstAnnotationUtils
import io.micronaut.ast.groovy.utils.AstGenericUtils
import io.micronaut.ast.groovy.utils.AstMessageUtils
import io.micronaut.ast.groovy.utils.PublicAbstractMethodVisitor
import io.micronaut.ast.groovy.utils.PublicMethodVisitor
import io.micronaut.ast.groovy.visitor.GroovyElementFactory
import io.micronaut.ast.groovy.visitor.GroovyVisitorContext
import io.micronaut.context.RequiresCondition
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.ConfigurationBuilder
import io.micronaut.context.annotation.ConfigurationInject
import io.micronaut.context.annotation.ConfigurationReader
import io.micronaut.context.annotation.DefaultScope
import io.micronaut.context.annotation.Executable
import io.micronaut.context.annotation.Factory
import io.micronaut.context.annotation.Property
import io.micronaut.context.annotation.Requires
import io.micronaut.context.annotation.Value
import io.micronaut.core.annotation.AccessorsStyle
import io.micronaut.core.annotation.AnnotationClassValue
import io.micronaut.core.annotation.AnnotationMetadata
import io.micronaut.core.annotation.AnnotationUtil
import io.micronaut.core.annotation.AnnotationValue
import io.micronaut.core.annotation.Internal
import io.micronaut.core.bind.annotation.Bindable
import io.micronaut.core.naming.NameUtils
import io.micronaut.core.reflect.ClassUtils
import io.micronaut.core.util.ArrayUtils
import io.micronaut.core.util.StringUtils
import io.micronaut.core.value.OptionalValues
import io.micronaut.inject.annotation.AnnotationMetadataHierarchy
import io.micronaut.inject.annotation.AnnotationMetadataReference
import io.micronaut.inject.annotation.DefaultAnnotationMetadata
import io.micronaut.inject.ast.ClassElement
import io.micronaut.inject.ast.Element
import io.micronaut.inject.ast.FieldElement
import io.micronaut.inject.ast.MethodElement
import io.micronaut.inject.ast.ParameterElement
import io.micronaut.inject.ast.PrimitiveElement
import io.micronaut.inject.configuration.ConfigurationMetadata
import io.micronaut.inject.configuration.ConfigurationMetadataBuilder
import io.micronaut.inject.configuration.PropertyMetadata
import io.micronaut.inject.visitor.VisitorConfiguration
import io.micronaut.inject.writer.BeanDefinitionReferenceWriter
import io.micronaut.inject.writer.BeanDefinitionVisitor
import io.micronaut.inject.writer.BeanDefinitionWriter
import io.micronaut.inject.writer.OriginatingElements
import org.codehaus.groovy.ast.ASTNode
import org.codehaus.groovy.ast.AnnotatedNode
import org.codehaus.groovy.ast.AnnotationNode
import org.codehaus.groovy.ast.ClassCodeVisitorSupport
import org.codehaus.groovy.ast.ClassHelper
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.ast.FieldNode
import org.codehaus.groovy.ast.GenericsType
import org.codehaus.groovy.ast.MethodNode
import org.codehaus.groovy.ast.Parameter
import org.codehaus.groovy.ast.PropertyNode
import org.codehaus.groovy.ast.expr.ClassExpression
import org.codehaus.groovy.ast.expr.Expression
import org.codehaus.groovy.ast.expr.ListExpression
import org.codehaus.groovy.ast.tools.GeneralUtils
import org.codehaus.groovy.control.CompilationUnit
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.control.messages.SyntaxErrorMessage
import org.codehaus.groovy.syntax.SyntaxException
import java.lang.reflect.Modifier
import java.time.Duration
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import java.util.function.Function
import static org.codehaus.groovy.ast.ClassHelper.makeCached
import static org.codehaus.groovy.ast.tools.GeneralUtils.getGetterName
import static org.codehaus.groovy.ast.tools.GeneralUtils.getSetterName
@CompileStatic
final class InjectVisitor extends ClassCodeVisitorSupport {
public static final String AROUND_TYPE = AnnotationUtil.ANN_AROUND
public static final String INTRODUCTION_TYPE = AnnotationUtil.ANN_INTRODUCTION
final SourceUnit sourceUnit
final ClassNode concreteClass
final ClassElement concreteClassElement
AnnotationMetadata concreteClassAnnotationMetadata
final ClassElement originatingElement
final boolean isConfigurationProperties
final boolean isFactoryClass
final boolean isExecutableType
final boolean isAopProxyType
final boolean isDeclaredBean
final ConfigurationMetadataBuilder configurationMetadataBuilder
ConfigurationMetadata configurationMetadata
final Map beanDefinitionWriters = [:]
private BeanDefinitionVisitor beanWriter
BeanDefinitionVisitor aopProxyWriter
final AtomicInteger adaptedMethodIndex = new AtomicInteger(0)
final AtomicInteger factoryMethodIndex = new AtomicInteger(0)
private final CompilationUnit compilationUnit
private final GroovyElementFactory elementFactory
GroovyVisitorContext groovyVisitorContext
InjectVisitor(SourceUnit sourceUnit, CompilationUnit compilationUnit, ClassNode targetClassNode, ConfigurationMetadataBuilder configurationMetadataBuilder) {
this(sourceUnit, compilationUnit, targetClassNode, null, configurationMetadataBuilder)
}
InjectVisitor(SourceUnit sourceUnit, CompilationUnit compilationUnit, ClassNode targetClassNode, Boolean configurationProperties, ConfigurationMetadataBuilder configurationMetadataBuilder) {
this.compilationUnit = compilationUnit
this.sourceUnit = sourceUnit
groovyVisitorContext = new GroovyVisitorContext(sourceUnit, compilationUnit) {
@Override
VisitorConfiguration getConfiguration() {
new VisitorConfiguration() {
@Override
boolean includeTypeLevelAnnotationsInGenericArguments() {
return false
}
}
}
}
this.elementFactory = groovyVisitorContext.getElementFactory()
this.configurationMetadataBuilder = configurationMetadataBuilder
this.concreteClass = targetClassNode
def annotationMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, targetClassNode)
this.concreteClassAnnotationMetadata = annotationMetadata
this.originatingElement = elementFactory.newClassElement(concreteClass, annotationMetadata)
this.concreteClassElement = originatingElement
this.isFactoryClass = annotationMetadata.hasStereotype(Factory)
this.isAopProxyType = hasAroundStereotype(annotationMetadata) && !targetClassNode.isAbstract() && !concreteClassElement.isAssignable(Interceptor.class)
this.isExecutableType = isAopProxyType || annotationMetadata.hasStereotype(Executable)
this.isConfigurationProperties = configurationProperties != null ? configurationProperties : annotationMetadata.hasDeclaredStereotype(ConfigurationReader)
if (isConfigurationProperties) {
this.configurationMetadata = configurationMetadataBuilder.visitProperties(
concreteClass,
null
)
}
if (isAopProxyType && Modifier.isFinal(targetClassNode.modifiers)) {
addError("Cannot apply AOP advice to final class. Class must be made non-final to support proxying: " + targetClassNode.name, targetClassNode)
}
this.isDeclaredBean = isExecutableType || isConfigurationProperties || isFactoryClass || annotationMetadata.hasStereotype(AnnotationUtil.SCOPE) || annotationMetadata.hasStereotype(DefaultScope) || annotationMetadata.hasDeclaredStereotype(Bean) || concreteClass.declaredConstructors.any {
AnnotationMetadata constructorMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, it)
constructorMetadata.hasStereotype(AnnotationUtil.INJECT)
}
if (isDeclaredBean) {
defineBeanDefinition(concreteClass)
}
}
static boolean hasAroundStereotype(AnnotationMetadata annotationMetadata) {
if (annotationMetadata.hasStereotype(AROUND_TYPE)) {
return true
} else {
if (annotationMetadata.hasStereotype(AnnotationUtil.ANN_INTERCEPTOR_BINDINGS)) {
return annotationMetadata.getAnnotationValuesByType(InterceptorBinding)
.stream().anyMatch{ av ->
av.enumValue("kind", InterceptorKind).orElse(InterceptorKind.AROUND) == InterceptorKind.AROUND
}
}
}
return false
}
BeanDefinitionVisitor getBeanWriter() {
if (this.beanWriter == null) {
defineBeanDefinition(concreteClass)
}
return beanWriter
}
@Override
void addError(String msg, ASTNode expr) {
SourceUnit source = getSourceUnit()
source.getErrorCollector().addError(
new SyntaxErrorMessage(new SyntaxException(msg + '\n', expr.getLineNumber(), expr.getColumnNumber(), expr.getLastLineNumber(), expr.getLastColumnNumber()), source)
)
}
@Override
void visitClass(ClassNode node) {
AnnotationMetadata annotationMetadata
if (concreteClass == node) {
annotationMetadata = concreteClassAnnotationMetadata
} else {
annotationMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, node)
}
boolean isInterface = node.isInterface()
if (isConfigurationProperties && isInterface) {
String adviceType = InjectTransform.ANN_CONFIGURATION_ADVICE
((Element)concreteClassElement).annotate(adviceType) // hack to make Groovy compile
concreteClassAnnotationMetadata = concreteClassElement.annotationMetadata
annotationMetadata = concreteClassAnnotationMetadata
}
if (annotationMetadata.hasStereotype(INTRODUCTION_TYPE)) {
String packageName = node.packageName
String beanClassName = node.nameWithoutPackage
AnnotationValue>[] aroundInterceptors = InterceptedMethodUtil
.resolveInterceptorBinding(annotationMetadata, InterceptorKind.AROUND)
AnnotationValue>[] introductionInterceptors = InterceptedMethodUtil
.resolveInterceptorBinding(annotationMetadata, InterceptorKind.INTRODUCTION)
AnnotationValue>[] interceptorTypes = (AnnotationValue>[]) ArrayUtils.concat(aroundInterceptors, introductionInterceptors)
ClassElement[] interfaceTypes = annotationMetadata.getValue(Introduction.class, "interfaces", String[].class).orElse(new String[0])
.collect { ClassElement.of(it) }
AopProxyWriter aopProxyWriter = new AopProxyWriter(
packageName,
beanClassName,
isInterface,
originatingElement,
annotationMetadata,
interfaceTypes,
groovyVisitorContext,
configurationMetadataBuilder,
configurationMetadata,
interceptorTypes
)
ClassElement groovyClassElement = elementFactory.newClassElement(
node,
annotationMetadata
)
aopProxyWriter.visitTypeArguments(groovyClassElement.getAllTypeArguments())
populateProxyWriterConstructor(groovyClassElement, aopProxyWriter, groovyClassElement.getPrimaryConstructor().orElse(null))
beanDefinitionWriters.put(node, aopProxyWriter)
this.aopProxyWriter = aopProxyWriter
visitAnnotationMetadata(aopProxyWriter, annotationMetadata)
visitIntroductionTypePublicMethods(aopProxyWriter, node)
if (ArrayUtils.isNotEmpty(interfaceTypes)) {
List annotationNodes = node.annotations
Set interfacesToVisit = []
populateIntroducedInterfaces(annotationNodes, interfacesToVisit)
if (!interfacesToVisit.isEmpty()) {
for (ClassNode itce in interfacesToVisit as Set) {
visitIntroductionTypePublicMethods(aopProxyWriter, itce)
}
}
}
if (!isInterface) {
node.visitContents(this)
}
} else {
boolean isOwningClass = node == concreteClass
if (isOwningClass && concreteClass.abstract && !isDeclaredBean) {
return
}
if (annotationMetadata.hasStereotype(AROUND_TYPE)) {
AnnotationValue>[] interceptorTypeReferences = InterceptedMethodUtil
.resolveInterceptorBinding(annotationMetadata, InterceptorKind.AROUND)
resolveProxyWriter(annotationMetadata.getValues(AROUND_TYPE, Boolean.class), false, interceptorTypeReferences)
}
ClassNode superClass = node.getSuperClass()
List superClasses = []
while (superClass != null) {
superClasses.add(superClass)
superClass = superClass.getSuperClass()
}
superClasses = superClasses.reverse()
for (classNode in superClasses) {
if (classNode.name != ClassHelper.OBJECT_TYPE.name && classNode.name != GroovyObjectSupport.name && classNode.name != Script.name) {
classNode.visitContents(this)
}
}
super.visitClass(node)
}
}
private void visitAnnotationMetadata(BeanDefinitionVisitor writer, AnnotationMetadata annotationMetadata) {
for (AnnotationValue annotation: annotationMetadata.getAnnotationValuesByType(Requires.class)) {
annotation.stringValue(RequiresCondition.MEMBER_BEAN_PROPERTY)
.ifPresent((String beanProperty) -> {
annotation.stringValue(RequiresCondition.MEMBER_BEAN)
.map{ String s -> compilationUnit.getAST().classes.find {ClassNode cn -> cn.name == s }}
.map{elementFactory.newClassElement(it, AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, it))}
.ifPresent((ClassElement classElement) -> {
String requiredValue = annotation.stringValue().orElse(null);
String notEqualsValue = annotation.stringValue(RequiresCondition.MEMBER_NOT_EQUALS).orElse(null);
writer.visitAnnotationMemberPropertyInjectionPoint(classElement, beanProperty, requiredValue, notEqualsValue)
})
})
}
}
private void populateIntroducedInterfaces(List annotationNodes, Set interfacesToVisit) {
for (ann in annotationNodes) {
if (ann.classNode.name == Introduction.class.getName()) {
Expression expression = ann.getMember("interfaces")
if (expression instanceof ClassExpression) {
interfacesToVisit.add(((ClassExpression) expression).type)
} else if (expression instanceof ListExpression) {
ListExpression list = (ListExpression) expression
for (expr in list.expressions) {
if (expr instanceof ClassExpression) {
interfacesToVisit.add(((ClassExpression) expr).type)
}
}
}
} else if (AstAnnotationUtils.hasStereotype(sourceUnit, compilationUnit, ann.classNode, Introduction)) {
populateIntroducedInterfaces(ann.classNode.annotations, interfacesToVisit)
}
}
}
@CompileStatic
protected void visitIntroductionTypePublicMethods(AopProxyWriter aopProxyWriter, ClassNode node) {
AnnotationMetadata typeAnnotationMetadata = aopProxyWriter.getAnnotationMetadata()
SourceUnit source = this.sourceUnit
CompilationUnit unit = this.compilationUnit
ClassElement concreteClassElement = this.concreteClassElement
AnnotationMetadata concreteClassAnnotationMetadata = this.concreteClassAnnotationMetadata
PublicMethodVisitor publicMethodVisitor = new PublicAbstractMethodVisitor(source, unit) {
@Override
protected boolean isAcceptableMethod(MethodNode methodNode) {
return super.isAcceptableMethod(methodNode) || hasDeclaredAroundStereotype(AstAnnotationUtils.getAnnotationMetadata(source, unit, methodNode))
}
@Override
void accept(ClassNode classNode, MethodNode methodNode) {
AnnotationMetadata annotationMetadata
if (AstAnnotationUtils.isAnnotated(node.name, methodNode) || AstAnnotationUtils.hasAnnotation(methodNode, Override)) {
// Class annotations are referenced by concreteClassAnnotationMetadata
annotationMetadata = AstAnnotationUtils.newBuilder(source, unit).buildForParent(node.name, null, methodNode)
annotationMetadata = new AnnotationMetadataHierarchy(concreteClassAnnotationMetadata, annotationMetadata)
} else {
annotationMetadata = new AnnotationMetadataReference(
aopProxyWriter.getBeanDefinitionName() + BeanDefinitionReferenceWriter.REF_SUFFIX,
typeAnnotationMetadata
)
}
MethodElement groovyMethodElement = elementFactory.newMethodElement(
concreteClassElement,
methodNode,
annotationMetadata
)
ClassNode owningType = AstGenericUtils.resolveTypeReference(methodNode.declaringClass)
ClassElement owningClassElement = elementFactory.newClassElement(
owningType,
concreteClassAnnotationMetadata
)
if (!annotationMetadata.hasStereotype("io.micronaut.validation.Validated") &&
isDeclaredBean) {
boolean hasConstraint
for (ParameterElement p: groovyMethodElement.getParameters()) {
AnnotationMetadata parameterMetadata = p.annotationMetadata
if (InjectTransform.IS_CONSTRAINT.test(parameterMetadata)) {
hasConstraint = true
break
}
}
if (hasConstraint) {
if (annotationMetadata instanceof AnnotationMetadataReference) {
annotationMetadata = AstAnnotationUtils.newBuilder(source, unit).buildForParent(node.name, node, methodNode)
groovyMethodElement = elementFactory.newMethodElement(
concreteClassElement,
methodNode,
annotationMetadata
)
}
annotationMetadata = addValidated(groovyMethodElement)
}
}
final String[] readPrefixes = annotationMetadata.getValue(AccessorsStyle.class, "readPrefixes", String[].class)
.orElse(new String[]{AccessorsStyle.DEFAULT_READ_PREFIX})
if (isConfigurationProperties && methodNode.isAbstract()) {
if (!aopProxyWriter.isValidated()) {
aopProxyWriter.setValidated(InjectTransform.IS_CONSTRAINT.test(annotationMetadata))
}
if (!NameUtils.isReaderName(methodNode.name, readPrefixes)) {
error("Only getter methods are allowed on @ConfigurationProperties interfaces: " + methodNode.name + ". You can change the accessors using @AccessorsStyle annotation)", classNode)
return
}
if (groovyMethodElement.hasParameters()) {
error("Only zero argument getter methods are allowed on @ConfigurationProperties interfaces: " + methodNode.name, classNode)
return
}
String propertyName = NameUtils.getPropertyNameForGetter(methodNode.name, readPrefixes)
String propertyType = methodNode.returnType.name
if ("void".equals(propertyType)) {
error("Getter methods must return a value @ConfigurationProperties interfaces: " + methodNode.name, classNode)
return
}
final PropertyMetadata propertyMetadata = configurationMetadataBuilder.visitProperty(
current.isInterface() ? current : classNode,
classNode,
propertyType,
propertyName,
null,
annotationMetadata.stringValue(Bindable.class, "defaultValue").orElse(null)
)
annotationMetadata = addPropertyMetadata(
groovyMethodElement,
propertyMetadata
)
final ClassNode typeElement = !ClassUtils.isJavaBasicType(propertyType) ? methodNode.returnType : null
if (typeElement != null && AstAnnotationUtils.hasStereotype(source, unit, typeElement, AnnotationUtil.SCOPE)) {
annotationMetadata = addBeanConfigAdvise(annotationMetadata)
} else {
annotationMetadata = addAnnotation(groovyMethodElement, InjectTransform.ANN_CONFIGURATION_ADVICE)
}
}
if (hasAroundStereotype(AstAnnotationUtils.getAnnotationMetadata(source, unit, methodNode))) {
AnnotationValue>[] interceptorTypeReferences = InterceptedMethodUtil
.resolveInterceptorBinding(annotationMetadata, InterceptorKind.AROUND)
aopProxyWriter.visitInterceptorBinding(interceptorTypeReferences)
}
if (methodNode.isAbstract()) {
aopProxyWriter.visitIntroductionMethod(
owningClassElement,
groovyMethodElement
)
} else {
aopProxyWriter.visitAroundMethod(
owningClassElement,
groovyMethodElement
)
}
}
@CompileDynamic
private void error(String msg, ClassNode classNode) {
addError(msg, (ASTNode) classNode)
}
@CompileDynamic
private AnnotationMetadata addBeanConfigAdvise(AnnotationMetadata annotationMetadata) {
new GroovyAnnotationMetadataBuilder(source, compilationUnit).annotate(
annotationMetadata,
AnnotationValue.builder(InjectTransform.ANN_CONFIGURATION_ADVICE).member("bean", true).build()
)
}
}
publicMethodVisitor.accept(node)
}
@Override
protected void visitConstructorOrMethod(MethodNode methodNode, boolean isConstructor) {
if (methodNode.isSynthetic() || methodNode.name.contains('$')) return
String methodName = methodNode.name
ClassNode declaringClass = methodNode.declaringClass
AnnotationMetadata methodAnnotationMetadata = getAnnotationMetadataHierarchy(
AstAnnotationUtils.getMethodAnnotationMetadata(sourceUnit, compilationUnit, methodNode)
)
def declaringElement = elementFactory.newClassElement(
declaringClass,
AnnotationMetadata.EMPTY_METADATA
)
final boolean isStatic = methodNode.isStatic()
final boolean isAbstract = methodNode.isAbstract()
final boolean isPrivate = methodNode.isPrivate()
final boolean isPublic = methodNode.isPublic()
if (isFactoryClass && !isConstructor && methodAnnotationMetadata.hasDeclaredStereotype(Bean.getName(), AnnotationUtil.SCOPE)) {
boolean isParent = declaringClass != concreteClass
MethodNode overriddenMethod = isParent ? concreteClass.getMethod(methodName, methodNode.parameters) : methodNode
boolean overridden = isParent && overriddenMethod.declaringClass != declaringClass
if (!overridden) {
methodAnnotationMetadata = new GroovyAnnotationMetadataBuilder(sourceUnit, compilationUnit).buildForParent(methodNode.returnType, methodNode, true)
visitBeanFactoryElement(declaringClass, methodNode, methodAnnotationMetadata, methodName)
}
} else if (methodAnnotationMetadata.hasStereotype(AnnotationUtil.INJECT) ||
methodAnnotationMetadata.hasDeclaredAnnotation(AnnotationUtil.POST_CONSTRUCT) ||
methodAnnotationMetadata.hasDeclaredAnnotation(AnnotationUtil.PRE_DESTROY)) {
if (isConstructor && methodAnnotationMetadata.hasStereotype(AnnotationUtil.INJECT)) {
// constructor with explicit @Inject
defineBeanDefinition(concreteClass)
} else if (!isConstructor) {
if (!isStatic && !isAbstract) {
boolean isParent = declaringClass != concreteClass
MethodNode overriddenMethod = isParent ? concreteClass.getMethod(methodName, methodNode.parameters) : methodNode
boolean overridden = isParent && overriddenMethod.declaringClass != declaringClass
boolean isPackagePrivate = isPackagePrivate(methodNode, methodNode.modifiers)
if (isParent && !isPrivate && !isPackagePrivate) {
if (overridden) {
// bail out if the method has been overridden, since it will have already been handled
return
}
}
boolean packagesDiffer = overriddenMethod.declaringClass.packageName != declaringClass.packageName
boolean isPackagePrivateAndPackagesDiffer = overridden && packagesDiffer && isPackagePrivate
boolean requiresReflection = isPrivate || isPackagePrivateAndPackagesDiffer
boolean overriddenInjected = overridden && AstAnnotationUtils.hasStereotype(sourceUnit, compilationUnit, overriddenMethod, AnnotationUtil.INJECT)
if (isParent && isPackagePrivate && !isPackagePrivateAndPackagesDiffer && overriddenInjected) {
// bail out if the method has been overridden by another method annotated with @INject
return
}
if (isParent && overridden && !overriddenInjected && !isPackagePrivateAndPackagesDiffer && !isPrivate) {
// bail out if the overridden method is package private and in the same package
// and is not annotated with @Inject
return
}
if (!requiresReflection && isInheritedAndNotPublic(methodNode, declaringClass, methodNode.modifiers)) {
requiresReflection = true
}
MethodElement groovyMethodElement = elementFactory.newMethodElement(
declaringElement,
methodNode,
methodAnnotationMetadata
)
if (isDeclaredBean && methodAnnotationMetadata.hasDeclaredAnnotation(AnnotationUtil.POST_CONSTRUCT)) {
defineBeanDefinition(concreteClass)
getBeanWriter().visitPostConstructMethod(
declaringElement,
groovyMethodElement,
requiresReflection,
groovyVisitorContext
)
} else if (isDeclaredBean && methodAnnotationMetadata.hasDeclaredAnnotation(AnnotationUtil.PRE_DESTROY)) {
defineBeanDefinition(concreteClass)
beanWriter.visitPreDestroyMethod(
declaringElement,
groovyMethodElement,
requiresReflection,
groovyVisitorContext
)
if (aopProxyWriter instanceof AopProxyWriter && !((AopProxyWriter)aopProxyWriter).isProxyTarget()) {
aopProxyWriter.visitPreDestroyMethod(
declaringElement,
groovyMethodElement,
requiresReflection,
groovyVisitorContext
)
}
} else if (methodAnnotationMetadata.hasStereotype(AnnotationUtil.INJECT)) {
defineBeanDefinition(concreteClass)
getBeanWriter().visitMethodInjectionPoint(
declaringElement,
groovyMethodElement,
requiresReflection,
groovyVisitorContext
)
}
}
}
} else if (!isConstructor) {
boolean hasInvalidModifiers = isStatic || isAbstract || methodNode.isSynthetic() || methodAnnotationMetadata.hasAnnotation(Internal) || isPrivate
boolean isExecutable = ((isExecutableType && isPublic) || methodAnnotationMetadata.hasStereotype(Executable) || hasAroundStereotype(methodAnnotationMetadata))
if (isDeclaredBean && isExecutable) {
if (hasInvalidModifiers) {
if (isPrivate && (methodAnnotationMetadata.hasDeclaredStereotype(Executable) || hasDeclaredAroundStereotype(methodAnnotationMetadata))) {
addError("Method annotated as executable but is declared private. Change the method to be non-private in order for AOP advice to be applied.", methodNode)
}
} else {
visitExecutableMethod(
declaringClass,
methodNode,
methodAnnotationMetadata,
methodName,
isPublic
)
}
} else if (isConfigurationProperties && isPublic) {
methodAnnotationMetadata = AstAnnotationUtils.newBuilder(sourceUnit, compilationUnit).buildDeclared(methodNode)
final String[] readPrefixes = declaringElement.getValue(AccessorsStyle.class, "readPrefixes", String[].class)
.orElse(new String[]{AccessorsStyle.DEFAULT_READ_PREFIX})
final String[] writePrefixes = declaringElement.getValue(AccessorsStyle.class, "writePrefixes", String[].class)
.orElse(new String[]{AccessorsStyle.DEFAULT_WRITE_PREFIX})
if (NameUtils.isWriterName(methodNode.name, writePrefixes) && methodNode.parameters.length == 1) {
String propertyName = NameUtils.getPropertyNameForSetter(methodNode.name, writePrefixes)
MethodElement groovyMethodElement = elementFactory.newMethodElement(
declaringElement,
methodNode,
methodAnnotationMetadata
)
ParameterElement parameterElement = groovyMethodElement.parameters[0]
if (methodAnnotationMetadata.hasStereotype(ConfigurationBuilder.class)) {
getBeanWriter().visitConfigBuilderMethod(
parameterElement.type,
NameUtils.getterNameFor(propertyName, readPrefixes),
methodAnnotationMetadata,
configurationMetadataBuilder,
parameterElement.type.interface
)
try {
visitConfigurationBuilder(
declaringElement,
methodAnnotationMetadata,
parameterElement.type,
getBeanWriter()
)
} finally {
getBeanWriter().visitConfigBuilderEnd()
}
} else if (declaringClass.getField(propertyName) == null) {
if (shouldExclude(configurationMetadata, propertyName)) {
return
}
PropertyMetadata propertyMetadata = configurationMetadataBuilder.visitProperty(
concreteClass,
declaringClass,
parameterElement.type.name,
propertyName,
null,
null
)
methodAnnotationMetadata = addPropertyMetadata(parameterElement, propertyMetadata)
getBeanWriter().visitSetterValue(
groovyMethodElement.declaringType,
groovyMethodElement,
false,
true
)
}
} else if (NameUtils.isReaderName(methodNode.name, readPrefixes)) {
if (!getBeanWriter().isValidated()) {
getBeanWriter().setValidated(InjectTransform.IS_CONSTRAINT.test(methodAnnotationMetadata))
}
}
} else {
def sourceUnit = sourceUnit
def compilationUnit = this.compilationUnit
final boolean isConstrained = isDeclaredBean &&
methodNode.getParameters()
.any { Parameter p ->
AnnotationMetadata annotationMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, p)
InjectTransform.IS_CONSTRAINT.test(annotationMetadata)
}
if (isConstrained) {
if (hasInvalidModifiers) {
if (isPrivate) {
addError("Method annotated with constraints but is declared private. Change the method to be non-private in order for AOP advice to be applied.", methodNode)
}
} else if (isPublic) {
visitExecutableMethod(declaringClass, methodNode, methodAnnotationMetadata, methodName, isPublic)
}
}
}
}
}
private AnnotationMetadata getAnnotationMetadataHierarchy(AnnotationMetadata methodAnnotationMetadata) {
return methodAnnotationMetadata instanceof AnnotationMetadataHierarchy ? methodAnnotationMetadata : new AnnotationMetadataHierarchy(concreteClassAnnotationMetadata, methodAnnotationMetadata)
}
@CompileStatic
private void visitBeanFactoryElement(
ClassNode declaringClass,
AnnotatedNode annotatedNode,
AnnotationMetadata methodAnnotationMetadata,
String elementName) {
if (annotatedNode instanceof MethodNode && concreteClassAnnotationMetadata.hasDeclaredStereotype(Around)) {
visitExecutableMethod(declaringClass, annotatedNode, methodAnnotationMetadata, elementName, annotatedNode.isPublic())
}
ClassElement producedClassElement
ClassNode returnType
Map> allTypeArguments
BeanDefinitionWriter beanMethodWriter
AnnotationMetadata beanFactoryMetadata = new AnnotationMetadataHierarchy(
concreteClassAnnotationMetadata,
methodAnnotationMetadata
);
if (annotatedNode instanceof MethodNode) {
def methodNode = (MethodNode) annotatedNode
MethodElement factoryMethodElement = elementFactory.newMethodElement(
concreteClassElement,
methodNode,
beanFactoryMetadata
)
producedClassElement = factoryMethodElement.genericReturnType
beanMethodWriter = new BeanDefinitionWriter(
factoryMethodElement,
OriginatingElements.of(originatingElement),
configurationMetadataBuilder,
groovyVisitorContext,
factoryMethodIndex.getAndIncrement()
)
returnType = methodNode.getReturnType()
allTypeArguments = factoryMethodElement.returnType.allTypeArguments
visitAnnotationMetadata(beanMethodWriter, beanFactoryMetadata)
beanMethodWriter.visitTypeArguments(allTypeArguments)
beanMethodWriter.visitBeanFactoryMethod(
originatingElement,
factoryMethodElement
)
} else {
FieldNode fieldNode
if (annotatedNode instanceof PropertyNode) {
fieldNode = ((PropertyNode) annotatedNode).field
} else {
fieldNode = annotatedNode as FieldNode
}
FieldElement factoryField = elementFactory.newFieldElement(
concreteClassElement,
fieldNode,
beanFactoryMetadata
)
producedClassElement = factoryField.genericField
beanMethodWriter = new BeanDefinitionWriter(
factoryField,
OriginatingElements.of(originatingElement),
configurationMetadataBuilder,
groovyVisitorContext,
factoryMethodIndex.getAndIncrement()
)
returnType = factoryField.type.nativeType as ClassNode
allTypeArguments = factoryField.type.allTypeArguments
visitAnnotationMetadata(beanMethodWriter, beanFactoryMetadata)
beanMethodWriter.visitTypeArguments(allTypeArguments)
beanMethodWriter.visitBeanFactoryField(
originatingElement,
factoryField
)
}
if (hasAroundStereotype(methodAnnotationMetadata) && !producedClassElement.isAssignable(Interceptor.class)) {
if (Modifier.isFinal(returnType.modifiers)) {
addError(
"Cannot apply AOP advice to final class. Class must be made non-final to support proxying: $annotatedNode",
annotatedNode
)
return
}
MethodElement constructor = producedClassElement.getPrimaryConstructor().orElse(null)
if (!producedClassElement.isInterface() && constructor != null && constructor.getParameters().length > 0) {
final String proxyTargetMode = methodAnnotationMetadata.stringValue(AROUND_TYPE, "proxyTargetMode")
.orElseGet(() -> {
// temporary workaround until micronaut-test can be upgraded to 3.0
if (methodAnnotationMetadata.hasAnnotation("io.micronaut.test.annotation.MockBean")) {
return "WARN";
} else {
return "ERROR";
}
});
switch (proxyTargetMode) {
case "ALLOW":
allowProxyConstruction(constructor)
break
case "WARN":
allowProxyConstruction(constructor)
AstMessageUtils.warning(sourceUnit, annotatedNode, "The produced type of a @Factory method has constructor arguments and is proxied. This can lead to unexpected behaviour. See the javadoc for Around.ProxyTargetConstructorMode for more information.")
break
default:
addError("The produced type from a factory which has AOP proxy advice specified must define an accessible no arguments constructor. Proxying types with constructor arguments can lead to unexpected behaviour. See the javadoc for for Around.ProxyTargetConstructorMode for more information and possible solutions.", annotatedNode)
return
}
}
AnnotationValue>[] interceptorTypeReferences = InterceptedMethodUtil
.resolveInterceptorBinding(methodAnnotationMetadata, InterceptorKind.AROUND)
OptionalValues aopSettings = methodAnnotationMetadata.getValues(AROUND_TYPE, Boolean)
Map finalSettings = [:]
for (key in aopSettings) {
finalSettings.put(key, aopSettings.get(key).get())
}
finalSettings.put(Interceptor.PROXY_TARGET, true)
AopProxyWriter proxyWriter = new AopProxyWriter(
beanMethodWriter,
OptionalValues.of(Boolean.class, finalSettings),
configurationMetadataBuilder,
groovyVisitorContext,
interceptorTypeReferences
)
proxyWriter.visitTypeArguments(allTypeArguments)
if (producedClassElement.isInterface()) {
proxyWriter.visitDefaultConstructor(AnnotationMetadata.EMPTY_METADATA, groovyVisitorContext)
} else {
populateProxyWriterConstructor(producedClassElement, proxyWriter, constructor)
}
SourceUnit source = this.sourceUnit
CompilationUnit unit = this.compilationUnit
ClassElement finalConcreteClassElement = this.concreteClassElement
new PublicMethodVisitor(source) {
@Override
void accept(ClassNode classNode, MethodNode targetBeanMethodNode) {
AnnotationMetadata annotationMetadata
if (AstAnnotationUtils.isAnnotated(producedClassElement.name, annotatedNode)) {
annotationMetadata = AstAnnotationUtils.newBuilder(source, unit)
.buildForParent(producedClassElement.name, annotatedNode, targetBeanMethodNode)
} else {
annotationMetadata = new AnnotationMetadataReference(
beanMethodWriter.getBeanDefinitionName() + BeanDefinitionReferenceWriter.REF_SUFFIX,
methodAnnotationMetadata
)
}
MethodElement targetMethodElement = elementFactory.newMethodElement(
finalConcreteClassElement,
targetBeanMethodNode,
annotationMetadata
)
proxyWriter.visitAroundMethod(
targetMethodElement.declaringType,
targetMethodElement
)
}
}.accept(returnType)
beanDefinitionWriters.put(new AnnotatedNode(), proxyWriter)
}
Optional preDestroy = methodAnnotationMetadata.getValue(Bean, "preDestroy", String.class)
if (preDestroy.isPresent()) {
String destroyMethodName = preDestroy.get()
MethodNode destroyMethod
ClassNode producedClassNode = (ClassNode) producedClassElement.nativeType
SourceUnit source = this.sourceUnit
new PublicMethodVisitor(source) {
@Override
void accept(ClassNode classNode, MethodNode methodNode) {
destroyMethod = methodNode
}
@Override
protected boolean isAcceptable(MethodNode node) {
return node.name == destroyMethodName && node.parameters.length == 0 && node.isPublic()
}
}.accept(producedClassNode)
if (destroyMethod != null) {
def destroyMethodElement = elementFactory.newMethodElement(
producedClassElement,
destroyMethod,
AnnotationMetadata.EMPTY_METADATA
)
beanMethodWriter.visitPreDestroyMethod(
producedClassElement,
destroyMethodElement,
false,
groovyVisitorContext
)
} else {
addError("@Bean method defines a preDestroy method that does not exist or is not public: $destroyMethodName", annotatedNode)
}
}
beanDefinitionWriters.put(annotatedNode, beanMethodWriter)
}
private static void allowProxyConstruction(MethodElement constructor) {
final ParameterElement[] parameters = constructor.getParameters()
for (ParameterElement parameter : parameters) {
if (parameter.primitive && !parameter.array) {
final String name = parameter.getType().getName()
if ("boolean" == name) {
parameter.annotate(Value.class, (builder) -> builder.value(false))
} else {
parameter.annotate(Value.class, (builder) -> builder.value(0))
}
} else {
// allow null
parameter.annotate(AnnotationUtil.NULLABLE)
parameter.removeAnnotation(AnnotationUtil.NON_NULL)
}
}
}
private static AnnotationMetadata addPropertyMetadata(Element element, PropertyMetadata propertyMetadata) {
element.annotate(
Property.class.getName(),
{ builder ->
builder.member("name", propertyMetadata.path)
}
)
return element.annotationMetadata
}
private void visitExecutableMethod(
ClassNode declaringClass,
MethodNode methodNode,
AnnotationMetadata methodAnnotationMetadata,
String methodName, boolean isPublic) {
if (declaringClass != ClassHelper.OBJECT_TYPE) {
boolean isOwningClass = declaringClass == concreteClass
boolean isParent = declaringClass != concreteClass
ClassElement declaringElement = elementFactory.newClassElement(declaringClass, concreteClassAnnotationMetadata)
def methodElement = elementFactory.newMethodElement(concreteClassElement, methodNode, methodAnnotationMetadata)
Parameter[] resolvedParameters = methodElement.parameters.collect { ParameterElement pe ->
if (pe.type.isPrimitive()) {
return (Parameter) pe.nativeType
} else {
return new Parameter((ClassNode) pe.genericType.nativeType, pe.name)
}
} as Parameter[]
MethodNode overriddenMethod = isParent ? concreteClass.getMethod(methodName, resolvedParameters) : methodNode
if (!isOwningClass && overriddenMethod != null && overriddenMethod.declaringClass != declaringClass) {
return
}
defineBeanDefinition(concreteClass)
boolean preprocess = methodAnnotationMetadata.booleanValue(Executable.class, "processOnStartup").orElse(false)
if (preprocess) {
getBeanWriter().setRequiresMethodProcessing(true)
}
final boolean hasConstraints = methodElement.parameters.any { am ->
InjectTransform.IS_CONSTRAINT.test(am.annotationMetadata)
}
if (hasConstraints) {
if (!methodAnnotationMetadata.hasStereotype("io.micronaut.validation.Validated")) {
methodAnnotationMetadata = addValidated(methodElement)
}
}
boolean executorMethodAdded = false
if (methodAnnotationMetadata.hasStereotype(Adapter.class)) {
visitAdaptedMethod(methodNode, methodAnnotationMetadata)
}
boolean hasAround = hasConstraints || hasAroundStereotype(methodAnnotationMetadata)
if ((isAopProxyType && isPublic) || (hasAround && !concreteClass.isAbstract() && !concreteClassElement.isAssignable(Interceptor.class))) {
boolean hasExplicitAround = hasDeclaredAroundStereotype(methodAnnotationMetadata)
if (methodNode.isFinal()) {
if (hasExplicitAround) {
addError("Method defines AOP advice but is declared final. Change the method to be non-final in order for AOP advice to be applied.", methodNode)
} else {
addError("Public method inherits AOP advice but is declared final. Change the method to be non-final in order for AOP advice to be applied.", methodNode)
}
} else {
AnnotationValue>[] interceptorTypeReferences = InterceptedMethodUtil
.resolveInterceptorBinding(methodAnnotationMetadata, InterceptorKind.AROUND)
OptionalValues aopSettings = methodAnnotationMetadata.getValues(AROUND_TYPE, Boolean)
AopProxyWriter proxyWriter = resolveProxyWriter(
aopSettings,
false,
interceptorTypeReferences
)
if (proxyWriter != null && !methodNode.isFinal()) {
proxyWriter.visitInterceptorBinding(interceptorTypeReferences)
proxyWriter.visitAroundMethod(
declaringElement,
methodElement
)
executorMethodAdded = true
}
}
}
if (!executorMethodAdded) {
getBeanWriter().visitExecutableMethod(
declaringElement,
methodElement,
groovyVisitorContext
)
}
}
}
static boolean hasDeclaredAroundStereotype(AnnotationMetadata methodAnnotationMetadata) {
if (methodAnnotationMetadata.hasDeclaredStereotype(AROUND_TYPE)) {
return true
} else if (methodAnnotationMetadata.hasDeclaredStereotype(AnnotationUtil.ANN_INTERCEPTOR_BINDINGS)) {
methodAnnotationMetadata.getDeclaredAnnotationValuesByType(InterceptorBinding)
.stream().anyMatch { av ->
InterceptorKind.AROUND == av.enumValue("kind", InterceptorKind).orElse(null)
}
}
}
@CompileDynamic
private AnnotationMetadata addValidated(Element element) {
return addAnnotation(element, "io.micronaut.validation.Validated")
}
@CompileDynamic
private AnnotationMetadata addAnnotation(Element element, String annotationName) {
element.annotate(annotationName)
return element.annotationMetadata
}
private AopProxyWriter resolveProxyWriter(
OptionalValues aopSettings,
boolean isFactoryType,
AnnotationValue>[] interceptorTypeReferences) {
AopProxyWriter proxyWriter = (AopProxyWriter) aopProxyWriter
if (proxyWriter == null) {
if (getBeanWriter() instanceof BeanDefinitionWriter) {
proxyWriter = new AopProxyWriter(
(BeanDefinitionWriter) getBeanWriter(),
aopSettings,
configurationMetadataBuilder,
groovyVisitorContext,
interceptorTypeReferences
)
} else {
// Unexpected: should be unreachable
throw new IllegalStateException("Internal Error: bean writer not an instance of BeanDefinitionWriter")
}
populateProxyWriterConstructor(concreteClassElement, proxyWriter, concreteClassElement.primaryConstructor.orElse(null))
String beanDefinitionName = getBeanWriter().getBeanDefinitionName()
if (isFactoryType) {
proxyWriter.visitSuperBeanDefinitionFactory(beanDefinitionName)
} else {
proxyWriter.visitSuperBeanDefinition(beanDefinitionName)
}
this.aopProxyWriter = proxyWriter
beanDefinitionWriters.put(new AnnotatedNode(), proxyWriter)
}
proxyWriter
}
protected void populateProxyWriterConstructor(ClassElement targetClass, AopProxyWriter proxyWriter, MethodElement constructor) {
if (constructor != null) {
if (constructor.parameters.length == 0) {
proxyWriter.visitDefaultConstructor(
AnnotationMetadata.EMPTY_METADATA,
groovyVisitorContext
)
} else {
proxyWriter.visitBeanDefinitionConstructor(
constructor,
constructor.isPrivate(),
groovyVisitorContext
)
}
} else {
ClassNode cn = targetClass.nativeType as ClassNode
if (cn.declaredConstructors.isEmpty()) {
proxyWriter.visitDefaultConstructor(AnnotationMetadata.EMPTY_METADATA, groovyVisitorContext)
} else {
addError("Class must have at least one non private constructor in order to be a candidate for dependency injection", (ASTNode) targetClass.nativeType)
}
}
}
protected static boolean isPackagePrivate(AnnotatedNode annotatedNode, int modifiers) {
return ((!Modifier.isProtected(modifiers) && !Modifier.isPublic(modifiers) && !Modifier.isPrivate(modifiers)) || !annotatedNode.getAnnotations(makeCached(PackageScope)).isEmpty())
}
@Override
void visitField(FieldNode fieldNode) {
if (fieldNode.name == 'metaClass') return
int modifiers = fieldNode.modifiers
if (Modifier.isStatic(modifiers)) {
return
}
if (fieldNode.isSynthetic() && !isPackagePrivate(fieldNode, fieldNode.modifiers)) {
return
}
ClassNode declaringClass = fieldNode.declaringClass
AnnotationMetadata fieldAnnotationMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, fieldNode)
if (Modifier.isFinal(modifiers) && !fieldAnnotationMetadata.hasStereotype(ConfigurationBuilder)) {
if (isFactoryClass && fieldAnnotationMetadata.hasDeclaredStereotype(Bean.class)) {
// field factory for bean
if (fieldNode.isPrivate() || fieldNode.isProtected()) {
AstMessageUtils.error(sourceUnit, fieldNode, "Beans produced from fields cannot be private or protected visibility")
} else {
visitBeanFactoryElement(
concreteClass,
fieldNode,
fieldAnnotationMetadata,
fieldNode.name
)
}
}
return
} else if (isFactoryClass && fieldAnnotationMetadata.hasDeclaredStereotype(Bean.class)) {
// field factory for bean
if (fieldNode.isPrivate() || fieldNode.isProtected()) {
AstMessageUtils.error(sourceUnit, fieldNode, "Beans produced from fields cannot be private or protected visibility")
}
return
}
boolean isInject = isFieldInjected(fieldNode, fieldAnnotationMetadata)
boolean isValue = isValueInjection(fieldNode, fieldAnnotationMetadata)
FieldElement fieldElement = elementFactory.newFieldElement(fieldNode, fieldAnnotationMetadata)
if ((isInject || isValue) && declaringClass.getProperty(fieldNode.getName()) == null) {
defineBeanDefinition(concreteClass)
if (!fieldNode.isStatic()) {
boolean isPrivate = Modifier.isPrivate(modifiers)
boolean requiresReflection = isPrivate || isInheritedAndNotPublic(fieldNode, fieldNode.declaringClass, modifiers)
if (!getBeanWriter().isValidated()) {
getBeanWriter().setValidated(InjectTransform.IS_CONSTRAINT.test(fieldAnnotationMetadata))
}
String fieldName = fieldNode.name
ClassElement fieldType = fieldElement.type
if (isValue) {
if (isConfigurationProperties && fieldAnnotationMetadata.hasStereotype(ConfigurationBuilder.class)) {
if(requiresReflection) {
// Using the field would throw a IllegalAccessError, use the method instead
String fieldGetterName = NameUtils.getterNameFor(fieldName)
MethodNode getterMethod = declaringClass.methods?.find { it.name == fieldGetterName}
if(getterMethod != null) {
getBeanWriter().visitConfigBuilderMethod(
fieldType,
getterMethod.name,
fieldAnnotationMetadata,
configurationMetadataBuilder,
fieldType.interface
)
} else {
addError("ConfigurationBuilder applied to a non accessible (private or package-private/protected in a different package) field must have a corresponding non-private getter method.", fieldNode)
}
} else {
getBeanWriter().visitConfigBuilderField(fieldType, fieldName, fieldAnnotationMetadata, configurationMetadataBuilder, fieldNode.type.interface)
}
try {
visitConfigurationBuilder(
fieldElement.declaringType,
fieldAnnotationMetadata,
fieldElement.type, getBeanWriter()
)
} finally {
getBeanWriter().visitConfigBuilderEnd()
}
} else {
if (isConfigurationProperties) {
if (shouldExclude(configurationMetadata, fieldName)) {
return
}
PropertyMetadata propertyMetadata = configurationMetadataBuilder.visitProperty(
concreteClass,
declaringClass,
fieldNode.type.name,
fieldName,
null, // TODO: fix groovy doc support
null
)
fieldElement.annotate(Property.class.getName(), {builder ->
builder.member("name", propertyMetadata.path)
})
}
getBeanWriter().visitFieldValue(
fieldElement.declaringType,
fieldElement,
requiresReflection,
isConfigurationProperties
)
}
} else {
getBeanWriter().visitFieldInjectionPoint(
fieldElement.declaringType,
fieldElement,
requiresReflection
)
}
}
}
}
@Override
@CompileDynamic
void visitProperty(PropertyNode propertyNode) {
FieldNode fieldNode = propertyNode.field
if (fieldNode.name == 'metaClass') return
def modifiers = propertyNode.getModifiers()
AnnotationMetadata fieldAnnotationMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, fieldNode)
if (Modifier.isFinal(modifiers) && !fieldAnnotationMetadata.hasStereotype(ConfigurationBuilder)) {
if (isFactoryClass && fieldAnnotationMetadata.hasDeclaredStereotype(Bean.class)) {
// field factory for bean
if (propertyNode.isPrivate()) {
AstMessageUtils.error(sourceUnit, propertyNode, "Beans produced from fields cannot be private")
} else {
visitFactoryProperty(propertyNode, fieldNode, fieldAnnotationMetadata)
}
}
return
}
boolean isInject = isFieldInjected(fieldNode, fieldAnnotationMetadata)
boolean isValue = isValueInjection(fieldNode, fieldAnnotationMetadata)
String propertyName = propertyNode.name
if (!propertyNode.isStatic() && (isInject || isValue)) {
defineBeanDefinition(concreteClass)
FieldElement fieldElement = elementFactory.newFieldElement(
fieldNode,
fieldAnnotationMetadata
)
if (!getBeanWriter().isValidated()) {
getBeanWriter().setValidated(InjectTransform.IS_CONSTRAINT.test(fieldAnnotationMetadata))
}
if (isInject) {
ParameterElement parameterElement = elementFactory.newParameterElement(fieldElement, fieldAnnotationMetadata)
MethodElement methodElement = MethodElement.of(
fieldElement.declaringType,
fieldElement,
PrimitiveElement.VOID,
PrimitiveElement.VOID,
getSetterName(propertyName),
parameterElement
)
getBeanWriter().visitMethodInjectionPoint(
fieldElement.declaringType,
methodElement,
false,
groovyVisitorContext
)
} else if (isValue) {
if (isConfigurationProperties && fieldAnnotationMetadata.hasStereotype(ConfigurationBuilder.class)) {
getBeanWriter().visitConfigBuilderMethod(
fieldElement.type,
getGetterName(propertyNode),
fieldAnnotationMetadata,
configurationMetadataBuilder,
fieldNode.type.interface)
try {
visitConfigurationBuilder(
fieldElement.declaringType,
fieldAnnotationMetadata,
fieldElement.type,
getBeanWriter()
)
} finally {
getBeanWriter().visitConfigBuilderEnd()
}
} else {
if (isConfigurationProperties) {
if (shouldExclude(configurationMetadata, propertyName)) {
return
}
PropertyMetadata propertyMetadata = configurationMetadataBuilder.visitProperty(
concreteClass,
fieldNode.declaringClass,
propertyNode.type.name,
propertyNode.name,
null, // TODO: fix groovy doc support
null
)
fieldElement.annotate(Property.class.getName(), { builder ->
builder.member("name", propertyMetadata.path)
})
fieldAnnotationMetadata = fieldElement.annotationMetadata
}
def setterName = GeneralUtils.getSetterName(propertyName)
ParameterElement parameterElement = elementFactory.newParameterElement(fieldElement, fieldAnnotationMetadata)
def methodElement = MethodElement.of(
fieldElement.declaringType,
fieldAnnotationMetadata,
PrimitiveElement.VOID,
PrimitiveElement.VOID,
setterName,
parameterElement
)
getBeanWriter().visitSetterValue(
fieldElement.declaringType,
methodElement,
false,
isConfigurationProperties
)
}
}
} else if (isAopProxyType && !propertyNode.isStatic()) {
AopProxyWriter aopWriter = (AopProxyWriter) aopProxyWriter
if (aopProxyWriter != null) {
AnnotationMetadata fieldMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, propertyNode.field)
FieldElement fieldElement = elementFactory.newFieldElement(
fieldNode,
fieldMetadata
)
ParameterElement parameterElement = elementFactory.newParameterElement(fieldElement, fieldAnnotationMetadata)
def methodAnnotationMetadata = new AnnotationMetadataHierarchy(
concreteClassAnnotationMetadata,
fieldAnnotationMetadata
)
MethodElement setterElement = MethodElement.of(
fieldElement.declaringType,
methodAnnotationMetadata,
PrimitiveElement.VOID,
PrimitiveElement.VOID,
getSetterName(propertyName),
parameterElement
)
aopWriter.visitAroundMethod(
fieldElement.declaringType,
setterElement
)
// also visit getter to ensure proxying
MethodElement getterElement = MethodElement.of(
fieldElement.declaringType,
methodAnnotationMetadata,
fieldElement.type,
fieldElement.genericType,
getGetterName(propertyNode)
)
aopWriter.visitAroundMethod(
fieldElement.declaringType,
getterElement
)
}
} else if (isFactoryClass && fieldAnnotationMetadata.hasDeclaredStereotype(Bean.class)) {
// field factory for bean
if (propertyNode.isPrivate()) {
AstMessageUtils.error(sourceUnit, propertyNode, "Beans produced from fields cannot be private");
} else {
visitFactoryProperty(propertyNode, fieldNode, fieldAnnotationMetadata)
}
}
}
private boolean isFieldInjected(FieldNode fieldNode, AnnotationMetadata fieldAnnotationMetadata) {
fieldNode != null && (fieldAnnotationMetadata.hasStereotype(AnnotationUtil.INJECT) || (fieldAnnotationMetadata.hasDeclaredStereotype(AnnotationUtil.QUALIFIER)) && !fieldAnnotationMetadata.hasDeclaredAnnotation(Bean))
}
private void visitFactoryProperty(PropertyNode propertyNode, FieldNode fieldNode, AnnotationMetadata fieldAnnotationMetadata) {
def modifiers = propertyNode.isStatic() ? Modifier.STATIC | Modifier.PUBLIC : Modifier.PUBLIC
def getterNode = new MethodNode(
getGetterName(propertyNode),
modifiers,
fieldNode.type,
new Parameter[0],
null,
null
)
getterNode.declaringClass = concreteClass
visitBeanFactoryElement(
concreteClass,
getterNode,
fieldAnnotationMetadata,
getterNode.name
)
}
private boolean isValueInjection(FieldNode fieldNode, AnnotationMetadata fieldAnnotationMetadata) {
fieldNode != null && (
fieldAnnotationMetadata.hasStereotype(Value) ||
fieldAnnotationMetadata.hasStereotype(Property) ||
isConfigurationProperties
)
}
protected boolean isInheritedAndNotPublic(AnnotatedNode annotatedNode, ClassNode declaringClass, int modifiers) {
return declaringClass != concreteClass &&
declaringClass.packageName != concreteClass.packageName &&
((Modifier.isProtected(modifiers) || !Modifier.isPublic(modifiers)) || !annotatedNode.getAnnotations(makeCached(PackageScope)).isEmpty())
}
@Override
protected SourceUnit getSourceUnit() {
return sourceUnit
}
private void defineBeanDefinition(ClassNode classNode) {
if (!beanDefinitionWriters.containsKey(classNode)) {
if (classNode.packageName == null) {
addError("Micronaut beans cannot be in the default package", classNode)
return
}
AnnotationMetadata annotationMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, classNode)
if (configurationMetadata != null) {
String existingPrefix = annotationMetadata.getValue(
ConfigurationReader.class,
"prefix", String.class)
.orElse("")
def computedPrefix = StringUtils.isNotEmpty(existingPrefix) ? existingPrefix + "." + configurationMetadata.getName() : configurationMetadata.getName()
annotationMetadata = DefaultAnnotationMetadata.mutateMember(
annotationMetadata,
ConfigurationReader.class.getName(),
"prefix",
computedPrefix
)
}
ClassElement groovyClassElement = elementFactory.newClassElement(
classNode,
annotationMetadata
)
if (annotationMetadata.hasStereotype(Singleton)) {
addError("Class annotated with groovy.lang.Singleton instead of jakarta.inject.Singleton. Import jakarta.inject.Singleton to use Micronaut Dependency Injection.", classNode)
}
beanWriter = new BeanDefinitionWriter(groovyClassElement, configurationMetadataBuilder, groovyVisitorContext)
beanWriter.visitTypeArguments(groovyClassElement.allTypeArguments)
beanDefinitionWriters.put(classNode, beanWriter)
visitAnnotationMetadata(beanWriter, annotationMetadata)
MethodElement constructor = groovyClassElement.getPrimaryConstructor().orElse(null)
if (constructor != null) {
if (constructor.parameters.length == 0) {
beanWriter.visitDefaultConstructor(AnnotationMetadata.EMPTY_METADATA, groovyVisitorContext)
} else {
def constructorMetadata = constructor.annotationMetadata
final boolean isConstructBinding = constructorMetadata.hasDeclaredStereotype(ConfigurationInject.class)
if (isConstructBinding) {
this.configurationMetadata = configurationMetadataBuilder.visitProperties(
concreteClass,
null)
}
beanWriter.visitBeanDefinitionConstructor(constructor, constructor.isPrivate(), groovyVisitorContext)
}
} else {
ClassNode cn = groovyClassElement.nativeType as ClassNode
if (cn.declaredConstructors.isEmpty()) {
beanWriter.visitDefaultConstructor(AnnotationMetadata.EMPTY_METADATA, groovyVisitorContext)
} else {
addError("Class must have at least one non private constructor in order to be a candidate for dependency injection", classNode)
}
}
} else {
beanWriter = beanDefinitionWriters.get(classNode)
}
}
@CompileDynamic
private void visitAdaptedMethod(MethodNode method, AnnotationMetadata methodAnnotationMetadata) {
if (methodAnnotationMetadata instanceof AnnotationMetadataHierarchy) {
methodAnnotationMetadata = ((AnnotationMetadataHierarchy) methodAnnotationMetadata).getDeclaredMetadata();
}
Optional adaptedType = methodAnnotationMetadata.getValue(Adapter.class, String.class).flatMap({ String s ->
ClassNode cn = sourceUnit.AST.classes.find { ClassNode cn -> cn.name == s }
if (cn != null) {
return Optional.of(cn)
}
def type = ClassUtils.forName(s, InjectTransform.classLoader).orElse(null)
if (type != null) {
return Optional.of(ClassHelper.make(type))
}
return Optional.empty()
} as Function>)
if (adaptedType.isPresent()) {
ClassNode typeToImplement = adaptedType.get()
boolean isInterface = typeToImplement.isInterface()
if (isInterface) {
String packageName = concreteClass.packageName
String declaringClassSimpleName = concreteClass.nameWithoutPackage
String beanClassName = generateAdaptedMethodClassName(declaringClassSimpleName, typeToImplement, method)
AopProxyWriter aopProxyWriter = new AopProxyWriter(
packageName,
beanClassName,
true,
false,
originatingElement,
new AnnotationMetadataHierarchy(concreteClassAnnotationMetadata, methodAnnotationMetadata),
[elementFactory.newClassElement(typeToImplement, AnnotationMetadata.EMPTY_METADATA)] as ClassElement[],
groovyVisitorContext,
configurationMetadataBuilder,
null
)
aopProxyWriter.visitDefaultConstructor(methodAnnotationMetadata, groovyVisitorContext)
beanDefinitionWriters.put(ClassHelper.make(packageName + '.' + beanClassName), aopProxyWriter)
ClassElement typeToImplementElement = elementFactory.newClassElement(
typeToImplement,
methodAnnotationMetadata
)
Map typeVariables = typeToImplementElement.getTypeArguments();
InjectVisitor thisVisitor = this
SourceUnit source = this.sourceUnit
CompilationUnit unit = this.compilationUnit
MethodElement sourceMethod = elementFactory.newMethodElement(
concreteClassElement,
method,
methodAnnotationMetadata
)
PublicAbstractMethodVisitor visitor = new PublicAbstractMethodVisitor(source, unit) {
boolean first = true
@Override
void accept(ClassNode classNode, MethodNode targetMethod) {
if (!first) {
thisVisitor.addError("Interface to adapt [" + typeToImplement + "] is not a SAM type. More than one abstract method declared.", (MethodNode)method)
return
}
first = false
MethodElement targetMethodElement = elementFactory.newMethodElement(
typeToImplementElement,
targetMethod,
AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, targetMethod)
)
ParameterElement[] sourceParams = sourceMethod.getParameters();
ParameterElement[] targetParams = targetMethodElement.getParameters();
Parameter[] targetParameters = targetMethod.getParameters()
if (targetParameters.size() == sourceParams.size()) {
int i = 0
Map genericTypes = [:]
for (Parameter targetElement in targetParameters) {
ParameterElement sourceElement = sourceParams[i]
ClassElement targetType = targetParams[i].getType()
ClassElement sourceType = sourceElement.getType()
if (targetElement.type.isGenericsPlaceHolder()) {
GenericsType[] targetGenerics = targetElement.type.genericsTypes
if (targetGenerics) {
String variableName = targetGenerics[0].name
if (typeVariables.containsKey(variableName)) {
targetType = typeVariables.get(variableName)
genericTypes.put(variableName, sourceType)
}
}
}
if (!sourceType.isAssignable(targetType.getName())) {
thisVisitor.addError("Cannot adapt method [${method.declaringClass.name}.$method.name(..)] to target method [${targetMethod.declaringClass.name}.$targetMethod.name(..)]. Argument type [" + sourceType.name + "] is not a subtype of type [$targetType.name] at position $i.", (MethodNode)method)
return
}
i++
}
if (!genericTypes.isEmpty()) {
Map> typeData = Collections.>singletonMap(
typeToImplement.name,
genericTypes
)
aopProxyWriter.visitTypeArguments(
typeData
)
}
String qualifier = concreteClassAnnotationMetadata.getValue(AnnotationUtil.NAMED, String.class).orElse(null)
MethodElement groovyMethodElement = elementFactory.newMethodElement(
concreteClassElement,
targetMethod,
methodAnnotationMetadata
)
AnnotationClassValue[] adaptedArgumentTypes = new AnnotationClassValue[sourceParams.length]
int j = 0
for (ParameterElement ve in sourceMethod.parameters) {
adaptedArgumentTypes[j] = new AnnotationClassValue(ve.type.name)
j++
}
groovyMethodElement.annotate(Adapter.class, { builder ->
builder.member(Adapter.InternalAttributes.ADAPTED_BEAN, new AnnotationClassValue<>(concreteClass.name))
builder.member(Adapter.InternalAttributes.ADAPTED_METHOD, method.name)
builder.member(Adapter.InternalAttributes.ADAPTED_ARGUMENT_TYPES, adaptedArgumentTypes)
if (StringUtils.isNotEmpty(qualifier)) {
builder.member(Adapter.InternalAttributes.ADAPTED_QUALIFIER, qualifier)
}
})
ClassElement declaringElement = elementFactory.newClassElement(
targetMethod.declaringClass,
AnnotationMetadata.EMPTY_METADATA
)
aopProxyWriter.visitAroundMethod(
declaringElement,
groovyMethodElement
)
} else {
thisVisitor.addError(
"Cannot adapt method [${method.declaringClass.name}.$method.name(..)] to target method [${targetMethod.declaringClass.name}.$targetMethod.name(..)]. Argument lengths don't match.",
(MethodNode) method
)
}
}
}
visitor.accept(typeToImplement)
}
}
}
private String generateAdaptedMethodClassName(String declaringClassSimpleName, ClassNode typeToImplement, MethodNode method) {
String rootName = declaringClassSimpleName + '$' + typeToImplement.nameWithoutPackage + '$' + method.getName()
return rootName + adaptedMethodIndex.incrementAndGet()
}
private void visitConfigurationBuilder(ClassElement declaringClass,
AnnotationMetadata annotationMetadata,
ClassElement classNode,
BeanDefinitionVisitor writer) {
Boolean allowZeroArgs = annotationMetadata.getValue(ConfigurationBuilder.class, "allowZeroArgs", Boolean.class).orElse(false)
List prefixes = Arrays.asList(annotationMetadata.getValue(AccessorsStyle.class, "writePrefixes", String[].class).orElse(["set"] as String[]))
String configurationPrefix = annotationMetadata.getValue(ConfigurationBuilder.class, String.class)
.map({ value -> value + "."}).orElse("")
Set includes = annotationMetadata.getValue(ConfigurationBuilder.class, "includes", Set.class).orElse(Collections.emptySet())
Set excludes = annotationMetadata.getValue(ConfigurationBuilder.class, "excludes", Set.class).orElse(Collections.emptySet())
SourceUnit source = this.sourceUnit
CompilationUnit compilationUnit = this.compilationUnit
ClassElement concreteClassElement = this.concreteClassElement
PublicMethodVisitor visitor = new PublicMethodVisitor(source) {
@Override
void accept(ClassNode cn, MethodNode method) {
String name = method.getName()
String prefix = getMethodPrefix(name)
String propertyName = NameUtils.decapitalize(name.substring(prefix.length()))
if (shouldExclude(includes, excludes, propertyName)) {
return
}
MethodElement groovyMethodElement = elementFactory.newMethodElement(
concreteClassElement,
method,
AstAnnotationUtils.getAnnotationMetadata(source, compilationUnit, method)
)
ParameterElement[] params = groovyMethodElement.parameters
int paramCount = params.size()
if (paramCount < 2) {
ParameterElement paramType = params.size() == 1 ? params[0] : null
PropertyMetadata metadata = configurationMetadataBuilder.visitProperty(
concreteClassElement.nativeType as ClassNode,
declaringClass.nativeType as ClassNode,
paramType?.type?.name,
configurationPrefix + propertyName,
null,
null
)
writer.visitConfigBuilderMethod(
prefix,
groovyMethodElement.returnType,
name,
paramType?.type,
paramType?.type?.typeArguments,
metadata.path
)
} else if (paramCount == 2) {
// check the params are a long and a TimeUnit
ParameterElement first = params[0]
ParameterElement second = params[1]
PropertyMetadata metadata = configurationMetadataBuilder.visitProperty(
concreteClassElement.nativeType as ClassNode,
declaringClass.nativeType as ClassNode,
Duration.class.name,
configurationPrefix + propertyName,
null,
null
)
if (second.type.name == TimeUnit.class.name && first.type.name == "long") {
writer.visitConfigBuilderDurationMethod(
prefix,
groovyMethodElement.returnType,
name,
metadata.path
)
}
}
}
@Override
protected boolean isAcceptable(MethodNode node) {
// ignore deprecated methods
if (AstAnnotationUtils.hasStereotype(source, compilationUnit, node, Deprecated.class)) {
return false
}
int paramCount = node.getParameters().size()
((paramCount > 0 && paramCount < 3) || (allowZeroArgs && paramCount == 0)) &&
super.isAcceptable(node) &&
isPrefixedWith(node.getName())
}
private boolean isPrefixedWith(String name) {
for (String prefix : prefixes) {
if (name.startsWith(prefix)) return true
}
return false
}
private String getMethodPrefix(String methodName) {
for (String prefix : prefixes) {
if (methodName.startsWith(prefix)) {
return prefix
}
}
return methodName
}
}
visitor.accept(classNode.nativeType as ClassNode)
}
private boolean shouldExclude(Set includes, Set excludes, String propertyName) {
if (!includes.isEmpty() && !includes.contains(propertyName)) {
return true
}
if (!excludes.isEmpty() && excludes.contains(propertyName)) {
return true
}
return false
}
private boolean shouldExclude(ConfigurationMetadata configurationMetadata, String propertyName) {
return shouldExclude(configurationMetadata.getIncludes(), configurationMetadata.getExcludes(), propertyName)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy