BeanIntrospectionWriter.java
/*
* Copyright 2017-2020 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.inject.beans.visitor;
import io.micronaut.core.annotation.AnnotationClassValue;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.Generated;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.beans.BeanIntrospection;
import io.micronaut.core.beans.BeanIntrospectionReference;
import io.micronaut.core.naming.NameUtils;
import io.micronaut.core.reflect.ReflectionUtils;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.ArrayUtils;
import io.micronaut.inject.annotation.AnnotationMetadataGenUtils;
import io.micronaut.inject.annotation.AnnotationMetadataHierarchy;
import io.micronaut.inject.annotation.AnnotationMetadataReference;
import io.micronaut.inject.annotation.MutableAnnotationMetadata;
import io.micronaut.inject.ast.ClassElement;
import io.micronaut.inject.ast.Element;
import io.micronaut.inject.ast.ElementQuery;
import io.micronaut.inject.ast.EnumConstantElement;
import io.micronaut.inject.ast.EnumElement;
import io.micronaut.inject.ast.FieldElement;
import io.micronaut.inject.ast.KotlinParameterElement;
import io.micronaut.inject.ast.MemberElement;
import io.micronaut.inject.ast.MethodElement;
import io.micronaut.inject.ast.ParameterElement;
import io.micronaut.inject.ast.TypedElement;
import io.micronaut.inject.beans.AbstractEnumBeanIntrospectionAndReference;
import io.micronaut.inject.beans.AbstractInitializableBeanIntrospection;
import io.micronaut.inject.beans.AbstractInitializableBeanIntrospectionAndReference;
import io.micronaut.inject.visitor.VisitorContext;
import io.micronaut.inject.writer.ArgumentExpUtils;
import io.micronaut.inject.writer.ClassOutputWriter;
import io.micronaut.inject.writer.ClassWriterOutputVisitor;
import io.micronaut.inject.writer.DispatchWriter;
import io.micronaut.inject.writer.EvaluatedExpressionProcessor;
import io.micronaut.inject.writer.OriginatingElements;
import io.micronaut.inject.writer.MethodGenUtils;
import io.micronaut.sourcegen.bytecode.ByteCodeWriter;
import io.micronaut.sourcegen.model.AnnotationDef;
import io.micronaut.sourcegen.model.ClassDef;
import io.micronaut.sourcegen.model.ClassTypeDef;
import io.micronaut.sourcegen.model.ExpressionDef;
import io.micronaut.sourcegen.model.FieldDef;
import io.micronaut.sourcegen.model.MethodDef;
import io.micronaut.sourcegen.model.StatementDef;
import io.micronaut.sourcegen.model.TypeDef;
import io.micronaut.sourcegen.model.VariableDef;
import javax.lang.model.element.Modifier;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* A class file writer that writes a {@link BeanIntrospectionReference} and associated
* {@link BeanIntrospection} for the given class.
*
* @author graemerocher
* @author Denis Stepanov
* @since 1.1
*/
@Internal
final class BeanIntrospectionWriter implements OriginatingElements, ClassOutputWriter {
private static final String INTROSPECTION_SUFFIX = "$Introspection";
private static final String FIELD_CONSTRUCTOR_ANNOTATION_METADATA = "$FIELD_CONSTRUCTOR_ANNOTATION_METADATA";
private static final String FIELD_CONSTRUCTOR_ARGUMENTS = "$CONSTRUCTOR_ARGUMENTS";
private static final String FIELD_BEAN_PROPERTIES_REFERENCES = "$PROPERTIES_REFERENCES";
private static final String FIELD_BEAN_METHODS_REFERENCES = "$METHODS_REFERENCES";
private static final String FIELD_ENUM_CONSTANTS_REFERENCES = "$ENUM_CONSTANTS_REFERENCES";
private static final java.lang.reflect.Method FIND_PROPERTY_BY_INDEX_METHOD =
ReflectionUtils.getRequiredInternalMethod(AbstractInitializableBeanIntrospection.class, "getPropertyByIndex", int.class);
private static final java.lang.reflect.Method FIND_INDEXED_PROPERTY_METHOD =
ReflectionUtils.getRequiredInternalMethod(AbstractInitializableBeanIntrospection.class, "findIndexedProperty", Class.class, String.class);
private static final java.lang.reflect.Method GET_INDEXED_PROPERTIES =
ReflectionUtils.getRequiredInternalMethod(AbstractInitializableBeanIntrospection.class, "getIndexedProperties", Class.class);
private static final java.lang.reflect.Method GET_BP_INDEXED_SUBSET_METHOD =
ReflectionUtils.getRequiredInternalMethod(AbstractInitializableBeanIntrospection.class, "getBeanPropertiesIndexedSubset", int[].class);
private static final java.lang.reflect.Constructor<?> BEAN_METHOD_REF_CONSTRUCTOR = ReflectionUtils.getRequiredInternalConstructor(
AbstractInitializableBeanIntrospection.BeanMethodRef.class,
Argument.class,
String.class,
AnnotationMetadata.class,
Argument[].class,
int.class
);
private static final java.lang.reflect.Constructor<?> ENUM_CONSTANT_DYNAMIC_REF_CONSTRUCTOR = ReflectionUtils.getRequiredInternalConstructor(
AbstractEnumBeanIntrospectionAndReference.EnumConstantDynamicRef.class,
AnnotationClassValue.class,
String.class,
AnnotationMetadata.class
);
private static final java.lang.reflect.Constructor<?> INTROSPECTION_SUPER_CONSTRUCTOR = ReflectionUtils.getRequiredInternalConstructor(
AbstractInitializableBeanIntrospectionAndReference.class,
Class.class,
AnnotationMetadata.class,
AnnotationMetadata.class,
Argument[].class,
AbstractInitializableBeanIntrospection.BeanPropertyRef[].class,
AbstractInitializableBeanIntrospection.BeanMethodRef[].class
);
private static final java.lang.reflect.Constructor<?> ENUM_INTROSPECTION_SUPER_CONSTRUCTOR = ReflectionUtils.getRequiredInternalConstructor(
AbstractEnumBeanIntrospectionAndReference.class,
Class.class,
AnnotationMetadata.class,
AnnotationMetadata.class,
Argument[].class,
AbstractInitializableBeanIntrospection.BeanPropertyRef[].class,
AbstractInitializableBeanIntrospection.BeanMethodRef[].class,
AbstractEnumBeanIntrospectionAndReference.EnumConstantDynamicRef[].class
);
private static final java.lang.reflect.Constructor<?> BEAN_PROPERTY_REF_CONSTRUCTOR = ReflectionUtils.getRequiredInternalConstructor(
AbstractInitializableBeanIntrospection.BeanPropertyRef.class,
Argument.class,
Argument.class,
Argument.class,
int.class,
int.class,
int.class,
boolean.class,
boolean.class
);
private static final java.lang.reflect.Method INSTANTIATE_METHOD = ReflectionUtils.getRequiredMethod(
AbstractInitializableBeanIntrospection.class,
"instantiate"
);
private static final java.lang.reflect.Method INSTANTIATE_INTERNAL_METHOD = ReflectionUtils.getRequiredMethod(
AbstractInitializableBeanIntrospection.class,
"instantiateInternal", Object[].class
);
private static final java.lang.reflect.Method HAS_BUILDER_METHOD = ReflectionUtils.getRequiredMethod(
BeanIntrospection.class,
"hasBuilder"
);
private static final java.lang.reflect.Method IS_BUILDABLE_METHOD = ReflectionUtils.getRequiredMethod(
BeanIntrospection.class,
"isBuildable"
);
private static final java.lang.reflect.Method HAS_CONSTRUCTOR_METHOD = ReflectionUtils.getRequiredMethod(
AbstractInitializableBeanIntrospection.class,
"hasConstructor"
);
private final String introspectionName;
private final ClassTypeDef introspectionTypeDef;
private final Map<AnnotationWithValue, String> indexByAnnotationAndValue = new HashMap<>(2);
private final Map<String, Set<String>> indexByAnnotations = new HashMap<>(2);
private final Map<String, FieldDef> annotationIndexFields = new HashMap<>(2);
private final ClassTypeDef beanType;
private final ClassElement beanClassElement;
private boolean executed = false;
private MethodElement constructor;
private MethodElement defaultConstructor;
private final List<BeanPropertyData> beanProperties = new ArrayList<>();
private final List<BeanMethodData> beanMethods = new ArrayList<>();
private final DispatchWriter dispatchWriter;
private final EvaluatedExpressionProcessor evaluatedExpressionProcessor;
private final AnnotationMetadata annotationMetadata;
private final OriginatingElements originatingElements;
private CopyConstructorDispatchTarget copyConstructorDispatchTarget;
/**
* Default constructor.
*
* @param beanClassElement The class element
* @param annotationMetadata The bean annotation metadata
* @param visitorContext The visitor context
*/
BeanIntrospectionWriter(String targetPackage, ClassElement beanClassElement, AnnotationMetadata annotationMetadata,
VisitorContext visitorContext) {
final String name = beanClassElement.getName();
this.beanClassElement = beanClassElement;
this.beanType = ClassTypeDef.of(beanClassElement);
this.introspectionName = computeShortIntrospectionName(targetPackage, name);
this.introspectionTypeDef = ClassTypeDef.of(introspectionName);
this.dispatchWriter = new DispatchWriter();
this.annotationMetadata = annotationMetadata.getTargetAnnotationMetadata();
this.originatingElements = OriginatingElements.of(beanClassElement);
evaluatedExpressionProcessor = new EvaluatedExpressionProcessor(visitorContext, beanClassElement);
evaluatedExpressionProcessor.processEvaluatedExpressions(annotationMetadata, null);
}
/**
* Constructor used to generate a reference for already compiled classes.
*
* @param generatingType The originating type
* @param index A unique index
* @param originatingElement The originating element
* @param beanClassElement The class element
* @param annotationMetadata The bean annotation metadata
* @param visitorContext The visitor context
*/
BeanIntrospectionWriter(
String targetPackage,
String generatingType,
int index,
ClassElement originatingElement,
ClassElement beanClassElement,
AnnotationMetadata annotationMetadata,
VisitorContext visitorContext) {
final String className = beanClassElement.getName();
this.beanClassElement = beanClassElement;
this.beanType = ClassTypeDef.of(beanClassElement);
this.introspectionName = computeIntrospectionName(targetPackage, className);
this.introspectionTypeDef = ClassTypeDef.of(introspectionName);
this.dispatchWriter = new DispatchWriter();
this.annotationMetadata = annotationMetadata.getTargetAnnotationMetadata();
this.originatingElements = OriginatingElements.of(originatingElement);
evaluatedExpressionProcessor = new EvaluatedExpressionProcessor(visitorContext, beanClassElement);
evaluatedExpressionProcessor.processEvaluatedExpressions(annotationMetadata, null);
}
/**
* @return The name of the class that the introspection will write.
*/
public String getIntrospectionName() {
return introspectionName;
}
/**
* @return The constructor.
*/
@Nullable
public MethodElement getConstructor() {
return constructor;
}
/**
* The bean type.
*
* @return The bean type
*/
public ClassTypeDef getBeanType() {
return beanType;
}
/**
* Visit a property.
*
* @param type The property type
* @param genericType The generic type
* @param name The property name
* @param readMember The read method
* @param readType The read type
* @param writeMember The write member
* @param writeType The write type
* @param isReadOnly Is read only
*/
void visitProperty(
@NonNull ClassElement type,
@NonNull ClassElement genericType,
@NonNull String name,
@Nullable MemberElement readMember,
@Nullable MemberElement writeMember,
@Nullable ClassElement readType,
@Nullable ClassElement writeType,
boolean isReadOnly) {
this.evaluatedExpressionProcessor.processEvaluatedExpressions(genericType.getAnnotationMetadata(), beanClassElement);
int readDispatchIndex = -1;
if (readMember != null) {
if (readMember instanceof MethodElement element) {
readDispatchIndex = dispatchWriter.addMethod(beanClassElement, element, true);
} else if (readMember instanceof FieldElement element) {
readDispatchIndex = dispatchWriter.addGetField(element);
} else {
throw new IllegalStateException();
}
}
int writeDispatchIndex = -1;
int withMethodIndex = -1;
if (writeMember != null) {
if (writeMember instanceof MethodElement element) {
writeDispatchIndex = dispatchWriter.addMethod(beanClassElement, element, true);
} else if (writeMember instanceof FieldElement element) {
writeDispatchIndex = dispatchWriter.addSetField(element);
} else {
throw new IllegalStateException();
}
}
boolean isMutable = !isReadOnly || hasAssociatedConstructorArgument(name, genericType);
if (isMutable) {
if (writeMember == null) {
final String prefix = this.annotationMetadata.stringValue(Introspected.class, "withPrefix").orElse("with");
ElementQuery<MethodElement> elementQuery = ElementQuery.of(MethodElement.class)
.onlyAccessible()
.onlyDeclared()
.onlyInstance()
.filter((methodElement -> {
ParameterElement[] parameters = methodElement.getParameters();
String methodName = methodElement.getName();
return methodName.startsWith(prefix) && methodName.equals(prefix + NameUtils.capitalize(name))
&& parameters.length == 1
&& methodElement.getGenericReturnType().getName().equals(beanClassElement.getName())
&& type.getType().isAssignable(parameters[0].getType());
}));
MethodElement withMethod = beanClassElement.getEnclosedElement(elementQuery).orElse(null);
if (withMethod != null) {
withMethodIndex = dispatchWriter.addMethod(beanClassElement, withMethod, true);
} else {
MethodElement constructor = this.constructor == null ? defaultConstructor : this.constructor;
if (constructor != null) {
if (copyConstructorDispatchTarget == null) {
copyConstructorDispatchTarget = new CopyConstructorDispatchTarget(beanType, beanProperties, dispatchWriter, constructor);
}
copyConstructorDispatchTarget.propertyNames.put(name, dispatchWriter.getDispatchTargets().size());
withMethodIndex = dispatchWriter.addDispatchTarget(copyConstructorDispatchTarget);
}
}
}
// Otherwise, set method would be used in BeanProperty
} else {
withMethodIndex = dispatchWriter.addDispatchTarget(new ExceptionDispatchTarget(
UnsupportedOperationException.class,
"Cannot mutate property [" + name + "] that is not mutable via a setter method, field or constructor argument for type: " + beanType.getName()
));
}
beanProperties.add(new BeanPropertyData(
name,
genericType,
readType,
writeType,
readDispatchIndex,
writeDispatchIndex,
withMethodIndex,
isReadOnly
));
}
/**
* Visits a bean method.
*
* @param element The method
*/
public void visitBeanMethod(MethodElement element) {
if (element != null && !element.isPrivate()) {
int dispatchIndex = dispatchWriter.addMethod(beanClassElement, element);
beanMethods.add(new BeanMethodData(element, dispatchIndex));
this.evaluatedExpressionProcessor.processEvaluatedExpressions(element.getAnnotationMetadata(), beanClassElement);
for (ParameterElement parameter : element.getParameters()) {
this.evaluatedExpressionProcessor.processEvaluatedExpressions(parameter.getAnnotationMetadata(), beanClassElement);
}
}
}
/**
* Builds an index for the given property and annotation.
*
* @param annotationName The annotation
* @param property The property
* @param value the value of the annotation
*/
void indexProperty(String annotationName, String property, @Nullable String value) {
indexByAnnotationAndValue.put(new AnnotationWithValue(annotationName, value), property);
indexByAnnotations.computeIfAbsent(annotationName, (a) -> new LinkedHashSet<>()).add(property);
}
@Override
public void accept(ClassWriterOutputVisitor classWriterOutputVisitor) throws IOException {
if (!executed) {
// Run only once
executed = true;
// First write the introspection for the annotation metadata can be populated with defaults that reference will contain
writeIntrospectionClass(classWriterOutputVisitor);
this.evaluatedExpressionProcessor.writeEvaluatedExpressions(classWriterOutputVisitor);
}
}
private ExpressionDef pushBeanPropertyReference(BeanPropertyData beanPropertyData,
List<StatementDef> staticStatements,
Function<String, ExpressionDef> loadClassValueExpressionFn) {
ClassTypeDef beanPropertyRefDef = ClassTypeDef.of(AbstractInitializableBeanIntrospection.BeanPropertyRef.class);
boolean mutable = !beanPropertyData.isReadOnly || hasAssociatedConstructorArgument(beanPropertyData.name, beanPropertyData.type);
StatementDef.DefineAndAssign defineAndAssign = ArgumentExpUtils.pushCreateArgument(
annotationMetadata,
beanClassElement,
introspectionTypeDef,
beanPropertyData.name,
beanPropertyData.type,
loadClassValueExpressionFn
).newLocal(beanPropertyData.name + "Arg");
staticStatements.add(defineAndAssign);
VariableDef mainArgument = defineAndAssign.variable();
ExpressionDef readArgument = null;
ExpressionDef writeArgument = null;
if (beanPropertyData.type.equals(beanPropertyData.readType) && beanPropertyData.type.equals(beanPropertyData.writeType)) {
readArgument = mainArgument;
writeArgument = mainArgument;
} else if (beanPropertyData.type.equals(beanPropertyData.readType) && beanPropertyData.writeType == null) {
readArgument = mainArgument;
} else if (beanPropertyData.type.equals(beanPropertyData.writeType) && beanPropertyData.readType == null) {
writeArgument = mainArgument;
} else {
readArgument = beanPropertyData.readType == null ? null : ArgumentExpUtils.pushCreateArgument(
annotationMetadata,
beanClassElement,
introspectionTypeDef,
beanPropertyData.name,
beanPropertyData.readType,
loadClassValueExpressionFn
);
writeArgument = beanPropertyData.writeType == null ? null : ArgumentExpUtils.pushCreateArgument(
annotationMetadata,
beanClassElement,
introspectionTypeDef,
beanPropertyData.name,
beanPropertyData.writeType,
loadClassValueExpressionFn
);
}
return beanPropertyRefDef.instantiate(
BEAN_PROPERTY_REF_CONSTRUCTOR,
mainArgument,
readArgument == null ? ExpressionDef.nullValue() : readArgument,
writeArgument == null ? ExpressionDef.nullValue() : writeArgument,
ExpressionDef.constant(beanPropertyData.getDispatchIndex),
ExpressionDef.constant(beanPropertyData.setDispatchIndex),
ExpressionDef.constant(beanPropertyData.withMethodDispatchIndex),
ExpressionDef.constant(beanPropertyData.isReadOnly),
ExpressionDef.constant(mutable)
);
}
private ExpressionDef newBeanMethodRef(BeanMethodData beanMethodData, Function<String, ExpressionDef> loadClassValueExpressionFn) {
return ClassTypeDef.of(AbstractInitializableBeanIntrospection.BeanMethodRef.class)
.instantiate(
BEAN_METHOD_REF_CONSTRUCTOR,
// 1: return argument
ArgumentExpUtils.pushReturnTypeArgument(
annotationMetadata,
introspectionTypeDef,
beanMethodData.methodElement.getOwningType(),
beanMethodData.methodElement.getGenericReturnType(),
loadClassValueExpressionFn),
// 2: name
ExpressionDef.constant(beanMethodData.methodElement.getName()),
// 3: annotation metadata
getAnnotationMetadataExpression(beanMethodData.methodElement.getAnnotationMetadata(), loadClassValueExpressionFn),
// 4: arguments
beanMethodData.methodElement.getParameters().length == 0 ? ExpressionDef.nullValue() : ArgumentExpUtils.pushBuildArgumentsForMethod(
annotationMetadata,
beanClassElement,
introspectionTypeDef,
Arrays.asList(beanMethodData.methodElement.getParameters()),
loadClassValueExpressionFn
),
// 5: method index
ExpressionDef.constant(beanMethodData.dispatchIndex)
);
}
private ExpressionDef newEnumConstantRef(EnumConstantElement enumConstantElement, Function<String, ExpressionDef> loadClassValueExpressionFn) {
return ClassTypeDef.of(
AbstractEnumBeanIntrospectionAndReference.EnumConstantDynamicRef.class
).instantiate(
ENUM_CONSTANT_DYNAMIC_REF_CONSTRUCTOR,
// 1: push annotation class value
loadClassValueExpressionFn.apply(enumConstantElement.getOwningType().getName()),
// 2: push enum name
ExpressionDef.constant(enumConstantElement.getName()),
// 3: annotation metadata
enumConstantElement.getAnnotationMetadata() == null || enumConstantElement.getAnnotationMetadata().isEmpty() ? (
ClassTypeDef.of(AnnotationMetadata.class).getStaticField("EMPTY_METADATA", TypeDef.of(AnnotationMetadata.class))
) : getAnnotationMetadataExpression(enumConstantElement.getAnnotationMetadata(), loadClassValueExpressionFn)
);
}
private boolean hasAssociatedConstructorArgument(String name, TypedElement typedElement) {
if (constructor != null) {
ParameterElement[] parameters = constructor.getParameters();
for (ParameterElement parameter : parameters) {
if (name.equals(parameter.getName())) {
return typedElement.getType().isAssignable(parameter.getGenericType());
}
}
}
return false;
}
private void writeIntrospectionClass(ClassWriterOutputVisitor classWriterOutputVisitor) throws IOException {
boolean isEnum = beanClassElement.isEnum();
Map<String, MethodDef> loadTypeMethods = new LinkedHashMap<>();
ClassTypeDef thisType = ClassTypeDef.of(introspectionName);
Function<String, ExpressionDef> loadClassValueExpressionFn = AnnotationMetadataGenUtils.createLoadClassValueExpressionFn(introspectionTypeDef, loadTypeMethods);
ClassDef.ClassDefBuilder classDefBuilder = ClassDef.builder(introspectionName).synthetic().addModifiers(Modifier.FINAL, Modifier.PUBLIC);
classDefBuilder.superclass(isEnum ? ClassTypeDef.of(AbstractEnumBeanIntrospectionAndReference.class) : ClassTypeDef.of(AbstractInitializableBeanIntrospectionAndReference.class));
classWriterOutputVisitor.visitServiceDescriptor(BeanIntrospectionReference.class, introspectionName, beanClassElement);
classDefBuilder.addAnnotation(AnnotationDef.builder(Generated.class).addMember("service", introspectionName).build());
// init expressions at build time
evaluatedExpressionProcessor.registerExpressionForBuildTimeInit(classDefBuilder);
FieldDef constructorAnnotationMetadataField;
FieldDef constructorArgumentsField;
FieldDef beanPropertiesField;
FieldDef beanMethodsField;
FieldDef enumsField;
List<StatementDef> staticStatements = new ArrayList<>();
if (constructor != null) {
if (!constructor.getAnnotationMetadata().isEmpty()) {
constructorAnnotationMetadataField = FieldDef.builder(FIELD_CONSTRUCTOR_ANNOTATION_METADATA, AnnotationMetadata.class)
.addModifiers(Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC)
.initializer(getAnnotationMetadataExpression(constructor.getAnnotationMetadata(), loadClassValueExpressionFn))
.build();
classDefBuilder.addField(
constructorAnnotationMetadataField
);
} else {
constructorAnnotationMetadataField = null;
}
if (ArrayUtils.isNotEmpty(constructor.getParameters())) {
constructorArgumentsField = FieldDef.builder(FIELD_CONSTRUCTOR_ARGUMENTS, Argument[].class)
.addModifiers(Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC)
.initializer(
ArgumentExpUtils.pushBuildArgumentsForMethod(
annotationMetadata,
constructor.getOwningType(),
introspectionTypeDef,
Arrays.asList(constructor.getParameters()),
loadClassValueExpressionFn
)
)
.build();
classDefBuilder.addField(
constructorArgumentsField
);
} else {
constructorArgumentsField = null;
}
} else {
constructorArgumentsField = null;
constructorAnnotationMetadataField = null;
}
if (!beanProperties.isEmpty()) {
beanPropertiesField = FieldDef.builder(FIELD_BEAN_PROPERTIES_REFERENCES, AbstractInitializableBeanIntrospection.BeanPropertyRef[].class)
.addModifiers(Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC)
.build();
classDefBuilder.addField(beanPropertiesField);
staticStatements.add(
thisType.getStaticField(beanPropertiesField).put(
ClassTypeDef.of(AbstractInitializableBeanIntrospection.BeanPropertyRef.class).array()
.instantiate(
beanProperties.stream()
.map(e -> pushBeanPropertyReference(e, staticStatements, loadClassValueExpressionFn))
.toList()
)
)
);
} else {
beanPropertiesField = null;
}
if (!beanMethods.isEmpty()) {
beanMethodsField = FieldDef.builder(FIELD_BEAN_METHODS_REFERENCES, AbstractInitializableBeanIntrospection.BeanMethodRef[].class)
.addModifiers(Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC)
.initializer(
ClassTypeDef.of(AbstractInitializableBeanIntrospection.BeanMethodRef.class).array()
.instantiate(
beanMethods.stream()
.map(e -> newBeanMethodRef(e, loadClassValueExpressionFn))
.toList()
)
)
.build();
classDefBuilder.addField(beanMethodsField);
} else {
beanMethodsField = null;
}
if (isEnum) {
enumsField = FieldDef.builder(FIELD_ENUM_CONSTANTS_REFERENCES, AbstractEnumBeanIntrospectionAndReference.EnumConstantDynamicRef[].class)
.addModifiers(Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC)
.initializer(
ClassTypeDef.of(AbstractEnumBeanIntrospectionAndReference.EnumConstantDynamicRef.class).array()
.instantiate(
((EnumElement) beanClassElement).elements().stream()
.map(e -> newEnumConstantRef(e, loadClassValueExpressionFn))
.toList()
)
)
.build();
classDefBuilder.addField(enumsField);
} else {
enumsField = null;
}
int indexesIndex = 0;
for (String annotationName : indexByAnnotations.keySet()) {
int[] indexes = indexByAnnotations.get(annotationName)
.stream()
.mapToInt(this::getPropertyIndex)
.toArray();
FieldDef field = FieldDef.builder("INDEX_" + (++indexesIndex), int[].class)
.addModifiers(Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC)
.initializer(
TypeDef.Primitive.INT.array()
.instantiate(
Arrays.stream(indexes).mapToObj(TypeDef.Primitive.INT::constant).toList()
)
)
.build();
classDefBuilder.addField(
field
);
annotationIndexFields.put(annotationName, field);
}
AnnotationMetadataGenUtils.addAnnotationDefaults(staticStatements, annotationMetadata, loadClassValueExpressionFn);
FieldDef annotationMetadataField = AnnotationMetadataGenUtils.createAnnotationMetadataFieldAndInitialize(annotationMetadata, loadClassValueExpressionFn);
if (annotationMetadataField != null) {
classDefBuilder.addField(
annotationMetadataField
);
}
if (!staticStatements.isEmpty()) {
classDefBuilder.addStaticInitializer(StatementDef.multi(staticStatements));
}
classDefBuilder.addMethod(
MethodDef.constructor()
.addModifiers(Modifier.PUBLIC)
.build((aThis, methodParameters) -> {
List<ExpressionDef> values = new ArrayList<>();
// 1st argument: The bean type
values.add(ExpressionDef.constant(beanType));
// 2nd argument: The annotation metadata
if (annotationMetadataField == null) {
values.add(ExpressionDef.nullValue());
} else {
values.add(introspectionTypeDef.getStaticField(annotationMetadataField));
}
// 3rd argument: constructor metadata
values.add(constructorAnnotationMetadataField != null ? introspectionTypeDef.getStaticField(constructorAnnotationMetadataField) : ExpressionDef.nullValue());
// 4th argument: constructor arguments
values.add(constructorArgumentsField != null ? introspectionTypeDef.getStaticField(constructorArgumentsField) : ExpressionDef.nullValue());
values.add(beanPropertiesField == null ? ExpressionDef.nullValue() : introspectionTypeDef.getStaticField(beanPropertiesField));
values.add(beanMethodsField == null ? ExpressionDef.nullValue() : introspectionTypeDef.getStaticField(beanMethodsField));
if (enumsField != null) {
values.add(introspectionTypeDef.getStaticField(enumsField));
return aThis.superRef().invokeConstructor(ENUM_INTROSPECTION_SUPER_CONSTRUCTOR, values);
} else {
return aThis.superRef().invokeConstructor(INTROSPECTION_SUPER_CONSTRUCTOR, values);
}
})
);
MethodDef dispatchOneMethod = dispatchWriter.buildDispatchOneMethod();
if (dispatchOneMethod != null) {
classDefBuilder.addMethod(dispatchOneMethod);
}
MethodDef dispatchMethod = dispatchWriter.buildDispatchMethod();
if (dispatchMethod != null) {
classDefBuilder.addMethod(dispatchMethod);
}
MethodDef buildGetTargetMethodByIndex = dispatchWriter.buildGetTargetMethodByIndex();
if (buildGetTargetMethodByIndex != null) {
classDefBuilder.addMethod(buildGetTargetMethodByIndex);
}
MethodDef findIndexedProperty = getFindIndexedProperty();
if (findIndexedProperty != null) {
classDefBuilder.addMethod(findIndexedProperty);
}
MethodDef getIndexedProperties = getGetIndexedProperties();
if (getIndexedProperties != null) {
classDefBuilder.addMethod(getIndexedProperties);
}
boolean hasBuilder = annotationMetadata != null &&
(annotationMetadata.isPresent(Introspected.class, "builder") || annotationMetadata.hasDeclaredAnnotation("lombok.Builder"));
if (defaultConstructor != null || constructor != null) {
classDefBuilder.addMethod(
getBooleanMethod(HAS_CONSTRUCTOR_METHOD, true)
);
}
if (defaultConstructor != null) {
classDefBuilder.addMethod(
getInstantiateMethod(defaultConstructor, INSTANTIATE_METHOD)
);
// in case invoked directly or via instantiateUnsafe
if (constructor == null) {
classDefBuilder.addMethod(
getInstantiateMethod(defaultConstructor, INSTANTIATE_INTERNAL_METHOD)
);
classDefBuilder.addMethod(
getBooleanMethod(IS_BUILDABLE_METHOD, true)
);
}
}
if (constructor != null) {
if (defaultConstructor == null) {
if (ArrayUtils.isEmpty(constructor.getParameters())) {
classDefBuilder.addMethod(
getInstantiateMethod(constructor, INSTANTIATE_METHOD)
);
} else {
boolean kotlinAllDefault = Arrays.stream(constructor.getParameters())
.allMatch(p -> p instanceof KotlinParameterElement kp && kp.hasDefault());
if (kotlinAllDefault) {
classDefBuilder.addMethod(
getInstantiateMethod(constructor, INSTANTIATE_METHOD)
);
}
}
}
classDefBuilder.addMethod(
getInstantiateMethod(constructor, INSTANTIATE_INTERNAL_METHOD)
);
classDefBuilder.addMethod(
getBooleanMethod(IS_BUILDABLE_METHOD, true)
);
} else if (defaultConstructor == null) {
classDefBuilder.addMethod(
getBooleanMethod(IS_BUILDABLE_METHOD, hasBuilder)
);
}
classDefBuilder.addMethod(
getBooleanMethod(HAS_BUILDER_METHOD, hasBuilder)
);
loadTypeMethods.values().forEach(classDefBuilder::addMethod);
try (OutputStream outputStream = classWriterOutputVisitor.visitClass(introspectionName, getOriginatingElements())) {
outputStream.write(new ByteCodeWriter(false, true).write(classDefBuilder.build()));
}
}
private MethodDef getBooleanMethod(Method method, boolean state) {
return MethodDef.override(method)
.build((aThis, methodParameters) -> ExpressionDef.constant(state).returning());
}
@Nullable
private MethodDef getFindIndexedProperty() {
if (indexByAnnotationAndValue.isEmpty()) {
return null;
}
TypeDef returnType = TypeDef.of(FIND_INDEXED_PROPERTY_METHOD.getReturnType());
Set<String> keys = indexByAnnotationAndValue.keySet()
.stream()
.map(s -> s.annotationName)
.collect(Collectors.toSet());
return MethodDef.builder(FIND_INDEXED_PROPERTY_METHOD.getName())
.addParameters(FIND_INDEXED_PROPERTY_METHOD.getParameterTypes())
.addModifiers(Modifier.PUBLIC)
.returns(returnType)
.build((aThis, methodParameters) ->
methodParameters.get(0).invoke("getName", TypeDef.STRING)
.asStatementSwitch(returnType, keys.stream()
.collect(Collectors.toMap(
ExpressionDef::constant,
annotationName -> onMatch(aThis, methodParameters, annotationName, returnType)
)), ExpressionDef.nullValue().returning()));
}
private StatementDef onMatch(VariableDef.This aThis, List<VariableDef.MethodParameter> parameters, String annotationName, TypeDef returnType) {
List<StatementDef> statements = new ArrayList<>();
VariableDef.MethodParameter annotationValueParameter = parameters.get(1);
if (indexByAnnotationAndValue.keySet().stream().anyMatch(s -> s.annotationName.equals(annotationName) && s.value == null)) {
String propertyName = indexByAnnotationAndValue.get(new AnnotationWithValue(annotationName, null));
int propertyIndex = getPropertyIndex(propertyName);
statements.add(
annotationValueParameter.ifNonNull(
aThis.invoke(FIND_PROPERTY_BY_INDEX_METHOD, ExpressionDef.constant(propertyIndex)).returning()
)
);
} else {
statements.add(
annotationValueParameter.ifNull(
ExpressionDef.nullValue().returning()
)
);
}
Set<String> valueMatches = indexByAnnotationAndValue.keySet()
.stream()
.filter(s -> s.annotationName.equals(annotationName) && s.value != null)
.map(s -> s.value)
.collect(Collectors.toSet());
if (!valueMatches.isEmpty()) {
statements.add(annotationValueParameter.asExpressionSwitch(returnType, valueMatches.stream()
.collect(Collectors.toMap(ExpressionDef::constant, e -> {
String propertyName = indexByAnnotationAndValue.get(new AnnotationWithValue(annotationName, e));
int propertyIndex = getPropertyIndex(propertyName);
return aThis.invoke(FIND_PROPERTY_BY_INDEX_METHOD, ExpressionDef.constant(propertyIndex));
})), ExpressionDef.nullValue()).returning()
);
}
statements.add(ExpressionDef.nullValue().returning());
return StatementDef.multi(statements);
}
@Nullable
private MethodDef getGetIndexedProperties() {
if (indexByAnnotations.isEmpty()) {
return null;
}
TypeDef returnType = TypeDef.of(GET_INDEXED_PROPERTIES.getReturnType());
return MethodDef.builder(GET_INDEXED_PROPERTIES.getName())
.returns(returnType)
.addModifiers(Modifier.PUBLIC)
.addParameters(GET_INDEXED_PROPERTIES.getParameterTypes())
.build((aThis, methodParameters) ->
methodParameters.get(0).invoke("getName", TypeDef.STRING)
.asExpressionSwitch(returnType, indexByAnnotations.keySet().stream()
.collect(Collectors.toMap(
ExpressionDef::constant,
annotationName ->
aThis.invoke(
GET_BP_INDEXED_SUBSET_METHOD,
introspectionTypeDef.getStaticField(annotationIndexFields.get(annotationName))
)
)), ExpressionDef.nullValue())
.returning());
}
private int getPropertyIndex(String propertyName) {
BeanPropertyData beanPropertyData = beanProperties.stream().filter(bp -> bp.name.equals(propertyName)).findFirst().orElse(null);
if (beanPropertyData != null) {
return beanProperties.indexOf(beanPropertyData);
}
throw new IllegalStateException("Property not found: " + propertyName + " " + beanClassElement.getName());
}
private MethodDef getInstantiateMethod(MethodElement constructor, Method method) {
return MethodDef.override(method)
.build((aThis, methodParameters) -> {
if (method.getParameters().length == 0) {
return MethodGenUtils.invokeBeanConstructor(constructor, true, null).returning();
} else {
List<ExpressionDef> values = IntStream.range(0, constructor.getSuspendParameters().length)
.<ExpressionDef>mapToObj(index -> methodParameters.get(0).arrayElement(index))
.toList();
return MethodGenUtils.invokeBeanConstructor(constructor, true, values).returning();
}
});
}
private ExpressionDef getAnnotationMetadataExpression(AnnotationMetadata annotationMetadata, Function<String, ExpressionDef> loadClassValueExpressionFn) {
MutableAnnotationMetadata.contributeDefaults(
this.annotationMetadata,
annotationMetadata
);
annotationMetadata = annotationMetadata.getTargetAnnotationMetadata();
if (annotationMetadata.isEmpty()) {
return ExpressionDef.nullValue();
} else if (annotationMetadata instanceof AnnotationMetadataReference annotationMetadataReference) {
return AnnotationMetadataGenUtils.annotationMetadataReference(annotationMetadataReference);
} else if (annotationMetadata instanceof AnnotationMetadataHierarchy annotationMetadataHierarchy) {
return AnnotationMetadataGenUtils.instantiateNewMetadataHierarchy(annotationMetadataHierarchy, loadClassValueExpressionFn);
} else if (annotationMetadata instanceof MutableAnnotationMetadata mutableAnnotationMetadata) {
return AnnotationMetadataGenUtils.instantiateNewMetadata(mutableAnnotationMetadata, loadClassValueExpressionFn);
} else {
throw new IllegalStateException("Unknown annotation metadata: " + annotationMetadata);
}
}
@NonNull
private static String computeShortIntrospectionName(String packageName, String className) {
final String shortName = NameUtils.getSimpleName(className);
return packageName + ".$" + shortName + INTROSPECTION_SUFFIX;
}
@NonNull
private static String computeIntrospectionName(String packageName, String className) {
return packageName + ".$" + className.replace('.', '_') + INTROSPECTION_SUFFIX;
}
/**
* Visit the constructor. If any.
*
* @param constructor The constructor method
*/
void visitConstructor(MethodElement constructor) {
this.constructor = constructor;
processConstructorEvaluatedMetadata(constructor);
}
/**
* Visit the default constructor. If any.
*
* @param constructor The constructor method
*/
void visitDefaultConstructor(MethodElement constructor) {
this.defaultConstructor = constructor;
processConstructorEvaluatedMetadata(constructor);
}
private void processConstructorEvaluatedMetadata(MethodElement constructor) {
this.evaluatedExpressionProcessor.processEvaluatedExpressions(constructor.getAnnotationMetadata(), null);
for (ParameterElement parameter : constructor.getParameters()) {
this.evaluatedExpressionProcessor.processEvaluatedExpressions(parameter.getAnnotationMetadata(), null);
}
}
@NonNull
@Override
public Element[] getOriginatingElements() {
return originatingElements.getOriginatingElements();
}
@Override
public void addOriginatingElement(@NonNull Element element) {
originatingElements.addOriginatingElement(element);
}
private record ExceptionDispatchTarget(Class<?> exceptionType,
String message) implements DispatchWriter.DispatchTarget {
@Override
public boolean supportsDispatchOne() {
return true;
}
@Override
public boolean supportsDispatchMulti() {
return false;
}
@Override
public StatementDef dispatch(ExpressionDef target, ExpressionDef valuesArray) {
return ClassTypeDef.of(exceptionType).instantiate(ExpressionDef.constant(message)).doThrow();
}
@Override
public StatementDef dispatchOne(int caseValue, ExpressionDef caseExpression, ExpressionDef target, ExpressionDef value) {
return ClassTypeDef.of(exceptionType).instantiate(ExpressionDef.constant(message)).doThrow();
}
@Override
public MethodElement getMethodElement() {
return null;
}
@Override
public TypedElement getDeclaringType() {
return null;
}
}
/**
* Copy constructor "with" method writer.
*/
private static final class CopyConstructorDispatchTarget implements DispatchWriter.DispatchTarget {
private final ClassTypeDef beanType;
private final List<BeanPropertyData> beanProperties;
private final DispatchWriter dispatchWriter;
private final MethodElement constructor;
private final Map<String, Integer> propertyNames = new HashMap<>();
private StatementDef statement;
private CopyConstructorDispatchTarget(ClassTypeDef beanType,
List<BeanPropertyData> beanProperties,
DispatchWriter dispatchWriter,
MethodElement constructor) {
this.beanType = beanType;
this.beanProperties = beanProperties;
this.dispatchWriter = dispatchWriter;
this.constructor = constructor;
}
@Override
public boolean supportsDispatchOne() {
return true;
}
@Override
public boolean supportsDispatchMulti() {
return false;
}
@Override
public StatementDef dispatchOne(int caseValue, ExpressionDef caseExpression, ExpressionDef target, ExpressionDef value) {
if (statement == null) {
// The unique statement provided for the switch case should produce one case
statement = createStatement(caseExpression, target, value);
}
return statement;
}
private StatementDef createStatement(ExpressionDef caseExpression, ExpressionDef target, ExpressionDef value) {
// In this case we have to do the copy constructor approach
Set<BeanPropertyData> constructorProps = new HashSet<>();
boolean isMutable = true;
String nonMutableMessage = null;
ParameterElement[] parameters = constructor.getParameters();
Object[] constructorArguments = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
ParameterElement parameter = parameters[i];
String parameterName = parameter.getName();
BeanPropertyData prop = beanProperties.stream()
.filter(bp -> bp.name.equals(parameterName))
.findAny().orElse(null);
int readDispatchIndex = prop == null ? -1 : prop.getDispatchIndex;
if (readDispatchIndex != -1) {
Object member;
ClassElement propertyType;
DispatchWriter.DispatchTarget dispatchTarget = dispatchWriter.getDispatchTargets().get(readDispatchIndex);
if (dispatchTarget instanceof DispatchWriter.MethodDispatchTarget methodDispatchTarget) {
MethodElement methodElement = methodDispatchTarget.getMethodElement();
propertyType = methodElement.getGenericReturnType();
member = methodElement;
} else if (dispatchTarget instanceof DispatchWriter.FieldGetDispatchTarget fieldGetDispatchTarget) {
FieldElement field = fieldGetDispatchTarget.getField();
propertyType = field.getGenericType();
member = field;
} else {
throw new IllegalStateException();
}
if (propertyType.isAssignable(parameter.getGenericType())) {
constructorArguments[i] = member;
constructorProps.add(prop);
} else {
isMutable = false;
nonMutableMessage = "Cannot create copy of type [" + beanType.getName() + "]. Property of type [" + propertyType.getName() + "] is not assignable to constructor argument [" + parameterName + "]";
}
} else {
isMutable = false;
nonMutableMessage = "Cannot create copy of type [" + beanType.getName() + "]. Constructor contains argument [" + parameterName + "] that is not a readable property";
break;
}
}
if (isMutable) {
return target.cast(beanType).newLocal("prevBean", prevBeanVar -> {
List<ExpressionDef> values = new ArrayList<>(constructorArguments.length);
for (int i = 0; i < parameters.length; i++) {
ParameterElement parameter = parameters[i];
Object constructorArgument = constructorArguments[i];
ExpressionDef oldValueExp;
if (constructorArgument instanceof MethodElement readMethod) {
oldValueExp = prevBeanVar.invoke(readMethod);
} else {
oldValueExp = prevBeanVar.field((FieldElement) constructorArgument);
}
Integer propertyIndex = propertyNames.get(parameter.getName());
ExpressionDef paramExp;
if (propertyIndex != null) {
ExpressionDef.Cast newPropertyValue = value.cast(TypeDef.erasure(parameter.getType()));
if (propertyNames.size() == 1) {
paramExp = newPropertyValue;
} else {
paramExp = caseExpression.equalsStructurally(ExpressionDef.constant((int) propertyIndex)).doIfElse(
newPropertyValue,
oldValueExp
);
}
} else {
paramExp = oldValueExp;
}
values.add(paramExp);
}
// NOTE: It doesn't make sense to check defaults for the copy constructor
ExpressionDef newInstance = MethodGenUtils.invokeBeanConstructor(constructor, false, values);
return withSetSettersAndFields(newInstance, prevBeanVar, constructorProps);
});
} else {
// In this case the bean cannot be mutated via either copy constructor or setter so simply throw an exception
return ClassTypeDef.of(UnsupportedOperationException.class).instantiate(ExpressionDef.constant(nonMutableMessage)).doThrow();
}
}
@Override
public StatementDef dispatch(ExpressionDef target, ExpressionDef valuesArray) {
throw new IllegalStateException("Not supported");
}
private StatementDef withSetSettersAndFields(ExpressionDef newInstance,
VariableDef prevBeanVar,
Set<BeanPropertyData> constructorProps) {
List<BeanPropertyData> readWriteProps = beanProperties.stream()
.filter(bp -> bp.setDispatchIndex != -1 && bp.getDispatchIndex != -1 && !constructorProps.contains(bp)).toList();
if (readWriteProps.isEmpty()) {
return newInstance.returning();
}
return newInstance
.newLocal("newBean", newBeanVar -> {
List<StatementDef> statements = new ArrayList<>();
for (BeanPropertyData readWriteProp : readWriteProps) {
DispatchWriter.DispatchTarget readDispatch = dispatchWriter.getDispatchTargets().get(readWriteProp.getDispatchIndex);
ExpressionDef oldValueExp;
if (readDispatch instanceof DispatchWriter.MethodDispatchTarget methodDispatchTarget) {
MethodElement readMethod = methodDispatchTarget.getMethodElement();
oldValueExp = prevBeanVar.invoke(readMethod);
} else if (readDispatch instanceof DispatchWriter.FieldGetDispatchTarget fieldGetDispatchTarget) {
FieldElement fieldElement = fieldGetDispatchTarget.getField();
oldValueExp = prevBeanVar.field(fieldElement);
} else {
throw new IllegalStateException();
}
DispatchWriter.DispatchTarget writeDispatch = dispatchWriter.getDispatchTargets().get(readWriteProp.setDispatchIndex);
if (writeDispatch instanceof DispatchWriter.MethodDispatchTarget methodDispatchTarget) {
MethodElement writeMethod = methodDispatchTarget.getMethodElement();
statements.add(
newBeanVar.invoke(
writeMethod,
oldValueExp
)
);
} else if (writeDispatch instanceof DispatchWriter.FieldSetDispatchTarget fieldSetDispatchTarget) {
FieldElement fieldElement = fieldSetDispatchTarget.getField();
statements.add(
newBeanVar.field(fieldElement).assign(oldValueExp)
);
} else {
throw new IllegalStateException();
}
}
statements.add(newBeanVar.returning());
return StatementDef.multi(statements);
});
}
@Override
public MethodElement getMethodElement() {
return null;
}
@Override
public TypedElement getDeclaringType() {
return constructor.getDeclaringType();
}
}
private record BeanMethodData(MethodElement methodElement, int dispatchIndex) {
}
/**
* @param name
* @param type
* @param readType
* @param writeType
* @param getDispatchIndex
* @param setDispatchIndex
* @param withMethodDispatchIndex
* @param isReadOnly
*/
private record BeanPropertyData(@NonNull String name,
@NonNull ClassElement type,
@Nullable ClassElement readType,
@Nullable ClassElement writeType,
int getDispatchIndex,
int setDispatchIndex,
int withMethodDispatchIndex,
boolean isReadOnly) {
}
/**
* index to be created.
*
* @param annotationName The annotation name
* @param value The annotation value
*/
private record AnnotationWithValue(@NonNull String annotationName, @Nullable String value) {
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
AnnotationWithValue that = (AnnotationWithValue) o;
return annotationName.equals(that.annotationName) && Objects.equals(value, that.value);
}
@Override
public int hashCode() {
return annotationName.hashCode();
}
}
}