AstBeanPropertiesUtils.java
/*
* Copyright 2017-2022 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.ast.utils;
import io.micronaut.context.annotation.BeanProperties;
import io.micronaut.context.annotation.Property;
import io.micronaut.context.annotation.Value;
import io.micronaut.core.annotation.AnnotationUtil;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.naming.NameUtils;
import io.micronaut.inject.ast.PropertyElementQuery;
import io.micronaut.inject.ast.ClassElement;
import io.micronaut.inject.ast.FieldElement;
import io.micronaut.inject.ast.MemberElement;
import io.micronaut.inject.ast.MethodElement;
import io.micronaut.inject.ast.PrimitiveElement;
import io.micronaut.inject.ast.PropertyElement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* The AST bean properties utils.
*
* @author Denis Stepanov
* @since 4.0.0
*/
@Internal
public final class AstBeanPropertiesUtils {
private AstBeanPropertiesUtils() {
}
/**
* Resolve the bean properties based on the configuration.
*
* @param configuration The configuration
* @param classElement The class element
* @param methodsSupplier The methods supplier
* @param fieldSupplier The fields supplier
* @param excludeElementsInRole Should exclude elements in role?
* @param propertyFields The fields that are properties
* @param customReaderPropertyNameResolver Custom resolver of the property name from the reader
* @param customWriterPropertyNameResolver Custom resolver of the property name from the writer
* @param propertyCreator The property creator
* @return the list of properties
*/
public static List<PropertyElement> resolveBeanProperties(PropertyElementQuery configuration,
ClassElement classElement,
Supplier<List<MethodElement>> methodsSupplier,
Supplier<List<FieldElement>> fieldSupplier,
boolean excludeElementsInRole,
Set<String> propertyFields,
Function<MethodElement, Optional<String>> customReaderPropertyNameResolver,
Function<MethodElement, Optional<String>> customWriterPropertyNameResolver,
Function<BeanPropertyData, PropertyElement> propertyCreator) {
BeanProperties.Visibility visibility = configuration.getVisibility();
Set<BeanProperties.AccessKind> accessKinds = configuration.getAccessKinds();
Set<String> includes = configuration.getIncludes();
Set<String> excludes = configuration.getExcludes();
String[] readPrefixes = configuration.getReadPrefixes();
String[] writePrefixes = configuration.getWritePrefixes();
Map<String, BeanPropertyData> props = new LinkedHashMap<>();
for (MethodElement methodElement : methodsSupplier.get()) {
// Records include everything
if (methodElement.isStatic() && !configuration.isAllowStaticProperties() || !excludeElementsInRole && isMethodInRole(methodElement)) {
continue;
}
String methodName = methodElement.getName();
if (methodName.equals("getMetaClass")) {
continue;
}
boolean isAccessor = canMethodBeUsedForAccess(methodElement, accessKinds, visibility);
if (classElement.isRecord()) {
if (!isAccessor) {
continue;
}
String propertyName = methodElement.getSimpleName();
processRecord(props, methodElement, propertyName);
} else if (NameUtils.isReaderName(methodName, readPrefixes) && methodElement.getParameters().length == 0) {
String propertyName = customReaderPropertyNameResolver.apply(methodElement)
.orElseGet(() -> NameUtils.getPropertyNameForGetter(methodName, readPrefixes));
processGetter(props, methodElement, propertyName, isAccessor, configuration);
} else if (NameUtils.isWriterName(methodName, writePrefixes)
&& (methodElement.getParameters().length == 1
|| configuration.isAllowSetterWithZeroArgs() && methodElement.getParameters().length == 0
|| configuration.isAllowSetterWithMultipleArgs() && methodElement.getParameters().length > 1)) {
String propertyName = customWriterPropertyNameResolver.apply(methodElement)
.orElseGet(() -> NameUtils.getPropertyNameForSetter(methodName, writePrefixes));
processSetter(classElement, props, methodElement, propertyName, isAccessor, configuration);
}
}
for (FieldElement fieldElement : fieldSupplier.get()) {
if (fieldElement.isStatic() && !configuration.isAllowStaticProperties() || !excludeElementsInRole && isFieldInRole(fieldElement)) {
continue;
}
String propertyName = fieldElement.getSimpleName();
boolean isAccessor = propertyFields.contains(propertyName) || canFieldBeUsedForAccess(fieldElement, accessKinds, visibility);
if (!isAccessor && !props.containsKey(propertyName)) {
continue;
}
BeanPropertyData beanPropertyData = props.computeIfAbsent(propertyName, BeanPropertyData::new);
resolveReadAccessForField(fieldElement, isAccessor, beanPropertyData);
resolveWriteAccessForField(fieldElement, isAccessor, beanPropertyData);
}
if (!props.isEmpty()) {
List<PropertyElement> beanProperties = new ArrayList<>(props.size());
for (Map.Entry<String, BeanPropertyData> entry : props.entrySet()) {
String propertyName = entry.getKey();
BeanPropertyData value = entry.getValue();
if (configuration.isIgnoreSettersWithDifferingType() && value.setter != null && value.getter != null) {
// ensure types match
ClassElement getterType = value.getter.getGenericReturnType();
ClassElement setterType = value.setter.getParameters()[0].getGenericType();
if (isIncompatibleSetterType(setterType, getterType)) {
// getter and setter don't match, remove setter
value.setter = null;
value.type = getterType;
}
}
// Define the property type based on its writer element
if (value.writeAccessKind == BeanProperties.AccessKind.FIELD && !value.field.getType().equals(value.type)) {
value.type = value.field.getGenericType();
} else if (value.writeAccessKind == BeanProperties.AccessKind.METHOD
&& value.setter != null
&& value.setter.getParameters().length > 0) {
value.type = value.setter.getParameters()[0].getGenericType();
}
// In a case when the field's type is the same as the selected property type,
// and it has more type arguments annotations - use it as the property type
if (value.field != null
&& value.field.getType().equals(value.type)
&& hasMoreAnnotations(value.field.getType(), value.type)) {
value.type = value.field.getGenericType();
}
// In a case when the getter's type is the same as the selected property type,
// and it has more type arguments annotations - use it as the property type
if (value.getter != null
&& value.getter.getGenericReturnType().equals(value.type)
&& hasMoreAnnotations(value.getter.getGenericReturnType(), value.type)) {
value.type = value.getter.getGenericReturnType();
}
if (value.readAccessKind != null || value.writeAccessKind != null) {
value.isExcluded = shouldExclude(includes, excludes, propertyName)
|| isExcludedByAnnotations(configuration, value)
|| isExcludedBecauseOfMissingAccess(value);
PropertyElement propertyElement = propertyCreator.apply(value);
if (propertyElement != null) {
beanProperties.add(propertyElement);
}
}
}
return beanProperties;
}
return Collections.emptyList();
}
private static boolean hasMoreAnnotations(ClassElement c1, ClassElement c2) {
return countGenericTypeAnnotations(c1) > countGenericTypeAnnotations(c2.getType())
|| c1.getTypeAnnotationMetadata().getAnnotationNames().size() > c2.getTypeAnnotationMetadata().getAnnotationNames().size();
}
private static boolean isFieldInRole(FieldElement fieldElement) {
return fieldElement.hasDeclaredAnnotation(AnnotationUtil.INJECT)
|| fieldElement.hasStereotype(Value.class)
|| fieldElement.hasStereotype(Property.class);
}
private static boolean isMethodInRole(MethodElement methodElement) {
return methodElement.hasDeclaredAnnotation(AnnotationUtil.INJECT)
|| methodElement.hasDeclaredAnnotation(AnnotationUtil.PRE_DESTROY)
|| methodElement.hasDeclaredAnnotation(AnnotationUtil.POST_CONSTRUCT);
}
private static int countGenericTypeAnnotations(ClassElement cl) {
return cl.getTypeArguments().values().stream().mapToInt(t -> t.getAnnotationMetadata().getAnnotationNames().size()).sum();
}
private static boolean isExcludedBecauseOfMissingAccess(BeanPropertyData value) {
if (value.readAccessKind == BeanProperties.AccessKind.METHOD
&& value.getter == null
&& value.writeAccessKind == BeanProperties.AccessKind.METHOD
&& value.setter == null) {
return true;
}
if (value.readAccessKind == BeanProperties.AccessKind.FIELD
&& value.writeAccessKind == BeanProperties.AccessKind.FIELD
&& value.field == null) {
return true;
}
return value.readAccessKind == null && value.writeAccessKind == null;
}
private static boolean isExcludedByAnnotations(PropertyElementQuery conf, BeanPropertyData value) {
if (conf.getExcludedAnnotations().isEmpty()) {
return false;
}
if (value.field != null && conf.getExcludedAnnotations().stream().anyMatch(value.field::hasAnnotation)) {
return true;
}
if (value.getter != null && conf.getExcludedAnnotations().stream().anyMatch(value.getter::hasAnnotation)) {
return true;
}
return (value.setter != null && conf.getExcludedAnnotations().stream().anyMatch(value.setter::hasAnnotation));
}
private static void processRecord(Map<String, BeanPropertyData> props, MethodElement methodElement, String propertyName) {
BeanPropertyData beanPropertyData = props.computeIfAbsent(propertyName, BeanPropertyData::new);
beanPropertyData.getter = methodElement;
beanPropertyData.readAccessKind = BeanProperties.AccessKind.METHOD;
beanPropertyData.type = beanPropertyData.getter.getGenericReturnType();
}
private static void processGetter(Map<String, BeanPropertyData> props, MethodElement methodElement, String propertyName, boolean isAccessor, PropertyElementQuery configuration) {
BeanPropertyData beanPropertyData = props.computeIfAbsent(propertyName, BeanPropertyData::new);
beanPropertyData.getter = methodElement;
if (isAccessor) {
beanPropertyData.readAccessKind = BeanProperties.AccessKind.METHOD;
}
ClassElement genericReturnType = beanPropertyData.getter.getGenericReturnType();
ClassElement getterType = unwrapType(genericReturnType);
if (configuration.isIgnoreSettersWithDifferingType() && beanPropertyData.type != null) {
if (!getterType.isAssignable(unwrapType(beanPropertyData.type))) {
beanPropertyData.getter = null; // not a compatible getter
beanPropertyData.readAccessKind = null;
}
} else {
beanPropertyData.type = genericReturnType;
}
}
private static void processSetter(ClassElement classElement, Map<String, BeanPropertyData> props, MethodElement methodElement, String propertyName, boolean isAccessor, PropertyElementQuery configuration) {
BeanPropertyData beanPropertyData = props.computeIfAbsent(propertyName, BeanPropertyData::new);
ClassElement paramType = methodElement.getParameters().length == 0 ? PrimitiveElement.BOOLEAN : methodElement.getParameters()[0].getGenericType();
ClassElement setterType = unwrapType(paramType);
ClassElement existingType = beanPropertyData.type != null ? unwrapType(beanPropertyData.type) : null;
if (setterType != null && beanPropertyData.setter != null) {
if (existingType != null && setterType.isAssignable(existingType)) {
// Override the setter because the type is higher
beanPropertyData.setter = methodElement;
} else if (beanPropertyData.setter.getDeclaringType().equals(methodElement.getDeclaringType())) {
// the same declared type; skip - take the first setter
return;
} else if (classElement.isAssignable(beanPropertyData.setter.getDeclaringType())) {
// override must be a subclass
beanPropertyData.setter = methodElement;
} else {
return;
}
} else {
beanPropertyData.setter = methodElement;
}
if (isAccessor) {
beanPropertyData.writeAccessKind = BeanProperties.AccessKind.METHOD;
}
if (configuration.isIgnoreSettersWithDifferingType() && beanPropertyData.type != null) {
if (existingType != null && isIncompatibleSetterType(setterType, existingType)) {
beanPropertyData.setter = null; // not a compatible setter
beanPropertyData.writeAccessKind = null;
}
} else {
beanPropertyData.type = paramType;
}
}
private static boolean isIncompatibleSetterType(ClassElement setterType, ClassElement existingType) {
return setterType != null && !existingType.isAssignable(setterType) && !setterType.getName().equals(existingType.getName());
}
private static ClassElement unwrapType(ClassElement type) {
if (type.isOptional()) {
return type.getOptionalValueType().orElse(type);
}
return type;
}
private static void resolveWriteAccessForField(FieldElement fieldElement, boolean isAccessor, BeanPropertyData beanPropertyData) {
if (fieldElement.isFinal()) {
return;
}
ClassElement fieldType = unwrapType(fieldElement.getGenericType());
if (beanPropertyData.type == null || fieldType.isAssignable(unwrapType(beanPropertyData.type))) {
beanPropertyData.field = fieldElement;
} else {
isAccessor = false; // not compatible field or setter is present
}
if (beanPropertyData.setter == null && isAccessor) {
// Use the field for read
beanPropertyData.writeAccessKind = BeanProperties.AccessKind.FIELD;
}
if (beanPropertyData.type == null) {
beanPropertyData.type = fieldElement.getGenericType();
}
}
private static void resolveReadAccessForField(FieldElement fieldElement, boolean isAccessor, BeanPropertyData beanPropertyData) {
ClassElement fieldType = unwrapType(fieldElement.getGenericType());
if (beanPropertyData.type == null || fieldType.isAssignable(unwrapType(beanPropertyData.type))) {
beanPropertyData.field = fieldElement;
} else {
isAccessor = false; // not compatible field or getter is present
}
if (beanPropertyData.getter == null && isAccessor) {
// Use the field for write
beanPropertyData.readAccessKind = BeanProperties.AccessKind.FIELD;
}
if (beanPropertyData.type == null) {
beanPropertyData.type = fieldElement.getGenericType();
}
}
private static boolean canFieldBeUsedForAccess(FieldElement fieldElement,
Set<BeanProperties.AccessKind> accessKinds,
BeanProperties.Visibility visibility) {
if (fieldElement.getOwningType().isRecord()) {
return false;
}
if (accessKinds.contains(BeanProperties.AccessKind.FIELD)) {
return isAccessible(fieldElement, visibility);
}
return false;
}
private static boolean canMethodBeUsedForAccess(MethodElement methodElement,
Set<BeanProperties.AccessKind> accessKinds,
BeanProperties.Visibility visibility) {
return accessKinds.contains(BeanProperties.AccessKind.METHOD) && isAccessible(methodElement, visibility);
}
private static boolean isAccessible(MemberElement memberElement, BeanProperties.Visibility visibility) {
return switch (visibility) {
case DEFAULT ->
!memberElement.isPrivate() && (memberElement.isAccessible() || memberElement.getDeclaringType().hasDeclaredStereotype(BeanProperties.class));
case PUBLIC -> memberElement.isPublic();
case ANY -> true;
};
}
private static boolean shouldExclude(Set<String> includes, Set<String> excludes, String propertyName) {
if (!includes.isEmpty() && !includes.contains(propertyName)) {
return true;
}
return !excludes.isEmpty() && excludes.contains(propertyName);
}
/**
* Internal holder class for getters and setters.
*/
@SuppressWarnings("VisibilityModifier")
public static final class BeanPropertyData {
public ClassElement type;
public MethodElement getter;
public MethodElement setter;
public FieldElement field;
public BeanProperties.AccessKind readAccessKind;
public BeanProperties.AccessKind writeAccessKind;
public final String propertyName;
public boolean isExcluded;
public BeanPropertyData(String propertyName) {
this.propertyName = propertyName;
}
}
}