diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java index 5041708cd3d..24e4fa73234 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java @@ -32,12 +32,15 @@ import io.micronaut.inject.annotation.MutableAnnotationMetadata; import io.micronaut.inject.ast.ClassElement; 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.processing.JavaModelUtils; @@ -88,6 +91,7 @@ final class BeanIntrospectionWriter extends AbstractClassFileWriter { 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 Method FIND_PROPERTY_BY_INDEX_METHOD = Method.getMethod( ReflectionUtils.getRequiredInternalMethod(AbstractInitializableBeanIntrospection.class, "getPropertyByIndex", int.class) ); @@ -334,7 +338,7 @@ public void accept(ClassWriterOutputVisitor classWriterOutputVisitor) throws IOE } } - private void buildStaticInit(ClassWriter classWriter) { + private void buildStaticInit(ClassWriter classWriter, boolean isEnum) { GeneratorAdapter staticInit = visitStaticInitializer(classWriter); Map defaults = new HashMap<>(); @@ -388,6 +392,23 @@ private void buildStaticInit(ClassWriter classWriter) { }); staticInit.putStatic(introspectionType, FIELD_BEAN_METHODS_REFERENCES, beanMethodsRefs); } + if (isEnum) { + Type type = Type.getType(AbstractEnumBeanIntrospectionAndReference.EnumConstantRef[].class); + classWriter.visitField( + ACC_PRIVATE | ACC_FINAL | ACC_STATIC, FIELD_ENUM_CONSTANTS_REFERENCES, + type.getDescriptor(), + null, + null + ); + pushNewArray(staticInit, AbstractEnumBeanIntrospectionAndReference.EnumConstantRef.class, ((EnumElement) classElement).elements(), enumConstantElement -> { + pushEnumConstantReference( + classWriter, + staticInit, + enumConstantElement + ); + }); + staticInit.putStatic(introspectionType, FIELD_ENUM_CONSTANTS_REFERENCES, type); + } int indexesIndex = 0; for (String annotationName : indexByAnnotations.keySet()) { @@ -555,6 +576,29 @@ private void pushBeanMethodReference(ClassWriter classWriter, int.class); } + private void pushEnumConstantReference(ClassWriter classWriter, + GeneratorAdapter staticInit, + EnumConstantElement enumConstantElement) { + staticInit.newInstance(Type.getType(AbstractEnumBeanIntrospectionAndReference.EnumConstantRef.class)); + staticInit.dup(); + // 1: value + staticInit.getStatic(getTypeReference(enumConstantElement.getOwningType()), enumConstantElement.getName(), getTypeReference(enumConstantElement.getOwningType())); + // 2: annotation metadata + AnnotationMetadata annotationMetadata = enumConstantElement.getAnnotationMetadata(); + if (annotationMetadata.isEmpty()) { + staticInit.getStatic(Type.getType(AnnotationMetadata.class), "EMPTY_METADATA", Type.getType(AnnotationMetadata.class)); + } else { + pushAnnotationMetadata(classWriter, staticInit, annotationMetadata); + } + + invokeConstructor( + staticInit, + AbstractEnumBeanIntrospectionAndReference.EnumConstantRef.class, + Enum.class, + AnnotationMetadata.class + ); + } + private boolean hasAssociatedConstructorArgument(String name, TypedElement typedElement) { if (constructor != null) { ParameterElement[] parameters = constructor.getParameters(); @@ -568,7 +612,8 @@ private boolean hasAssociatedConstructorArgument(String name, TypedElement typed } private void writeIntrospectionClass(ClassWriterOutputVisitor classWriterOutputVisitor) throws IOException { - final Type superType = Type.getType(AbstractInitializableBeanIntrospectionAndReference.class); + boolean isEnum = classElement.isEnum(); + final Type superType = isEnum ? Type.getType(AbstractEnumBeanIntrospectionAndReference.class) : Type.getType(AbstractInitializableBeanIntrospectionAndReference.class); ClassWriter classWriter = new AptClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES, visitorContext); classWriter.visit( @@ -584,7 +629,7 @@ private void writeIntrospectionClass(ClassWriterOutputVisitor classWriterOutputV annotateAsGeneratedAndService(classWriter, introspectionName); - buildStaticInit(classWriter); + buildStaticInit(classWriter, isEnum); final GeneratorAdapter constructorWriter = startConstructor(classWriter); @@ -632,17 +677,34 @@ private void writeIntrospectionClass(ClassWriterOutputVisitor classWriterOutputV FIELD_BEAN_METHODS_REFERENCES, Type.getType(AbstractInitializableBeanIntrospection.BeanMethodRef[].class)); } - - invokeConstructor( - constructorWriter, - AbstractInitializableBeanIntrospectionAndReference.class, - Class.class, - AnnotationMetadata.class, - AnnotationMetadata.class, - Argument[].class, - AbstractInitializableBeanIntrospection.BeanPropertyRef[].class, - AbstractInitializableBeanIntrospection.BeanMethodRef[].class - ); + if (isEnum) { + constructorWriter.getStatic(introspectionType, + FIELD_ENUM_CONSTANTS_REFERENCES, + Type.getType(AbstractEnumBeanIntrospectionAndReference.EnumConstantRef[].class) + ); + invokeConstructor( + constructorWriter, + AbstractEnumBeanIntrospectionAndReference.class, + Class.class, + AnnotationMetadata.class, + AnnotationMetadata.class, + Argument[].class, + AbstractInitializableBeanIntrospection.BeanPropertyRef[].class, + AbstractInitializableBeanIntrospection.BeanMethodRef[].class, + AbstractEnumBeanIntrospectionAndReference.EnumConstantRef[].class + ); + } else { + invokeConstructor( + constructorWriter, + AbstractInitializableBeanIntrospectionAndReference.class, + Class.class, + AnnotationMetadata.class, + AnnotationMetadata.class, + Argument[].class, + AbstractInitializableBeanIntrospection.BeanPropertyRef[].class, + AbstractInitializableBeanIntrospection.BeanMethodRef[].class + ); + } constructorWriter.returnValue(); constructorWriter.visitMaxs(2, 1); diff --git a/core/src/main/java/io/micronaut/core/beans/EnumBeanIntrospection.java b/core/src/main/java/io/micronaut/core/beans/EnumBeanIntrospection.java new file mode 100644 index 00000000000..609271c8edb --- /dev/null +++ b/core/src/main/java/io/micronaut/core/beans/EnumBeanIntrospection.java @@ -0,0 +1,50 @@ +/* + * Copyright 2017-2024 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.core.beans; + +import io.micronaut.core.annotation.AnnotationMetadataDelegate; + +import java.util.List; + +/** + * A variation of {@link BeanIntrospection} that is representing an enum as a bean. + * + * @param The type of the enum + * @author Denis Stepanov + * @since 4.4.0 + */ +public interface EnumBeanIntrospection> extends BeanIntrospection { + + /** + * @return The constants of the enum + */ + List> getConstants(); + + /** + * The enum constant. + * + * @param The enum type + */ + interface EnumConstant extends AnnotationMetadataDelegate { + + /** + * @return The instance value. + */ + E getValue(); + + } + +} diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy index 03cdfcf4dd8..d4b0c59471a 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy @@ -17,6 +17,7 @@ import io.micronaut.core.beans.BeanIntrospectionReference import io.micronaut.core.beans.BeanIntrospector import io.micronaut.core.beans.BeanMethod import io.micronaut.core.beans.BeanProperty +import io.micronaut.core.beans.EnumBeanIntrospection import io.micronaut.core.convert.ConversionContext import io.micronaut.core.convert.TypeConverter import io.micronaut.core.reflect.InstantiationUtils @@ -3924,6 +3925,61 @@ public enum Test { thrown(ClassNotFoundException) } + void "test enum bean with annotations"() { + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test; + +import io.micronaut.core.annotation.*; +import com.fasterxml.jackson.annotation.JsonProperty; + +@Introspected +public enum Test { + + @JsonProperty("X") + A(0), + @JsonProperty("Y") + B(1), + @JsonProperty("Z") + C(2); + + private final int number; + + Test(int number) { + this.number = number; + } + + public int getNumber() { + return number; + } +} +''') + + expect: + introspection != null + introspection.beanProperties.size() == 1 + introspection.getProperty("number").isPresent() + introspection instanceof EnumBeanIntrospection + + when: + def enumIntrospection = introspection as EnumBeanIntrospection + def instance = introspection.instantiate("A") + + then: + instance.name() == "A" + introspection.getRequiredProperty("number", int).get(instance) == 0 + + when: + def annotationMetadata = enumIntrospection.constants.find { it.value == instance }.annotationMetadata + + then: + annotationMetadata.stringValue(JsonProperty).get() == "X" + + and: + enumIntrospection.constants[0].stringValue(JsonProperty).get() == "X" + enumIntrospection.constants[1].stringValue(JsonProperty).get() == "Y" + enumIntrospection.constants[2].stringValue(JsonProperty).get() == "Z" + } + void "test enum bean properties with custom getter"() { BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' package test; diff --git a/inject/src/main/java/io/micronaut/inject/beans/AbstractEnumBeanIntrospectionAndReference.java b/inject/src/main/java/io/micronaut/inject/beans/AbstractEnumBeanIntrospectionAndReference.java new file mode 100644 index 00000000000..882a2c2f47a --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/beans/AbstractEnumBeanIntrospectionAndReference.java @@ -0,0 +1,88 @@ +/* + * Copyright 2017-2024 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; + +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.UsedByGeneratedCode; +import io.micronaut.core.beans.BeanIntrospectionReference; +import io.micronaut.core.beans.EnumBeanIntrospection; +import io.micronaut.core.type.Argument; + +import java.util.List; + +/** + * A variation of {@link AbstractInitializableBeanIntrospection} that is also a {@link BeanIntrospectionReference}. + * + * @param The enum type + * @author Denis Stepanov + * @since 4.4.0 + */ +@Internal +public abstract class AbstractEnumBeanIntrospectionAndReference> extends AbstractInitializableBeanIntrospectionAndReference implements EnumBeanIntrospection { + + private final List> enumConstantRefs; + + /** + * The default constructor. + * + * @param beanType The bean type + * @param annotationMetadata The annotation metadata + * @param constructorAnnotationMetadata The constructor annotation metadata + * @param constructorArguments The constructor arguments + * @param propertiesRefs The property references + * @param methodsRefs The method references + * @param enumValueRefs The enum references + */ + protected AbstractEnumBeanIntrospectionAndReference(Class beanType, + AnnotationMetadata annotationMetadata, + AnnotationMetadata constructorAnnotationMetadata, + Argument[] constructorArguments, + BeanPropertyRef[] propertiesRefs, + BeanMethodRef[] methodsRefs, + EnumConstantRef[] enumValueRefs) { + super(beanType, annotationMetadata, constructorAnnotationMetadata, constructorArguments, propertiesRefs, methodsRefs); + this.enumConstantRefs = List.of(enumValueRefs); + } + + @NonNull + @Override + public List> getConstants() { + return enumConstantRefs; + } + + /** + * Enum value compile-time data container. + */ + @Internal + @UsedByGeneratedCode + public record EnumConstantRef>(@NonNull E value, + @NonNull AnnotationMetadata annotationMetadata) implements EnumConstant { + + @NonNull + @Override + public E getValue() { + return value; + } + + @NonNull + @Override + public AnnotationMetadata getAnnotationMetadata() { + return annotationMetadata; + } + } +} diff --git a/inject/src/main/resources/META-INF/native-image/io.micronaut/micronaut-inject/native-image.properties b/inject/src/main/resources/META-INF/native-image/io.micronaut/micronaut-inject/native-image.properties index c7a4f5ba0ad..e19f7c9465b 100644 --- a/inject/src/main/resources/META-INF/native-image/io.micronaut/micronaut-inject/native-image.properties +++ b/inject/src/main/resources/META-INF/native-image/io.micronaut/micronaut-inject/native-image.properties @@ -27,4 +27,5 @@ Args = --enable-http \ --initialize-at-build-time=io.micronaut.context.AbstractExecutableMethodsDefinition$MethodReference \ --initialize-at-build-time=io.micronaut.inject.beans.AbstractInitializableBeanIntrospection$BeanPropertyRef \ --initialize-at-build-time=io.micronaut.inject.beans.AbstractInitializableBeanIntrospection$BeanMethodRef \ + --initialize-at-build-time=io.micronaut.inject.beans.AbstractEnumBeanIntrospectionAndReference$EnumConstantRef \ --initialize-at-run-time=io.micronaut.context.env.CachedEnvironment