MapperVisitor.java
/*
* Copyright 2017-2023 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.context.annotation.Mapper;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.inject.ast.ClassElement;
import io.micronaut.inject.ast.MethodElement;
import io.micronaut.inject.ast.ParameterElement;
import io.micronaut.inject.ast.PropertyElementQuery;
import io.micronaut.inject.processing.ProcessingException;
import io.micronaut.inject.visitor.TypeElementVisitor;
import io.micronaut.inject.visitor.VisitorContext;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* The mapper visitor.
* @since 4.1.0
*/
public final class MapperVisitor implements TypeElementVisitor<Object, Mapper> {
private ClassElement lastClassElement;
@Override
public Set<String> getSupportedAnnotationNames() {
return Set.of(Mapper.class.getName());
}
@Override
public void visitClass(ClassElement element, VisitorContext context) {
lastClassElement = element;
}
@Override
public void visitMethod(MethodElement element, VisitorContext context) {
if (element.hasDeclaredAnnotation(Mapper.class)) {
if (!element.isAbstract()) {
throw new ProcessingException(element, "@Mapper can only be declared on abstract methods");
}
ClassElement toType = element.getGenericReturnType();
if (toType.isVoid()) {
throw new ProcessingException(element, "A void return type is not permitted for a mapper");
}
List<AnnotationValue<Mapper.Mapping>> values = element.getAnnotationMetadata().getAnnotationValuesByType(Mapper.Mapping.class);
if (!CollectionUtils.isEmpty(values)) {
validateMappingAnnotations(element, values, toType);
}
if (lastClassElement != null) {
lastClassElement.annotate(Mapper.class);
}
}
}
@SuppressWarnings("java:S1192")
private void validateMappingAnnotations(MethodElement element, List<AnnotationValue<Mapper.Mapping>> values, ClassElement toType) {
@NonNull ParameterElement[] parameters = element.getParameters();
for (int i = 0; i < parameters.length; i++) {
ParameterElement parameter = parameters[i];
ClassElement fromType = parameter.getGenericType();
boolean isMap = fromType.isAssignable(Map.class);
if (isMap) {
List<? extends ClassElement> boundGenerics = fromType.getBoundGenericTypes();
if (boundGenerics.isEmpty() || !boundGenerics.iterator().next().isAssignable(String.class)) {
throw new ProcessingException(element, "@Mapping from parameter that is a Map must have String keys");
}
}
}
Set<String> toDefs = new HashSet<>();
for (AnnotationValue<Mapper.Mapping> value : values) {
value.stringValue("to").ifPresent(to -> {
if (toDefs.contains(to)) {
throw new ProcessingException(element, "Multiple @Mapping definitions map to the same property: " + to);
} else {
toDefs.add(to);
if (!hasPropertyWithName(toType, to)) {
throw new ProcessingException(element, "@Mapping(to=\"" + to + "\") specifies a property that doesn't exist in type " + toType.getName());
}
}
});
value.stringValue("from").ifPresent(from -> {
if (from.contains("#{")) {
return;
}
if (from.contains(".")) {
int index = from.indexOf(".");
String argumentName = from.substring(0, index);
String propertyName = from.substring(index + 1);
boolean anyMatch = false;
for (ParameterElement parameter: parameters) {
if (parameter.getName().equals(argumentName)) {
anyMatch = true;
if (parameter.getType().getName().equals(Map.class.getName())) {
break;
}
if (!hasPropertyWithName(parameter.getGenericType(), propertyName)) {
throw new ProcessingException(element, "@Mapping(from=\"" + from + "\") specifies property " + propertyName + " that doesn't exist in type " + parameter.getGenericType().getName());
}
break;
}
}
if (!anyMatch) {
throw new ProcessingException(element, "@Mapping(from=\"" + from + "\") specifies argument " + argumentName + " that doesn't exist for method");
}
} else {
String propertyName = from.substring(from.indexOf(".") + 1);
for (ParameterElement parameter: parameters) {
if (parameter.getType().getName().equals(Map.class.getName())) {
continue;
}
if (!hasPropertyWithName(parameter.getGenericType(), propertyName)) {
throw new ProcessingException(element, "@Mapping(from=\"" + from + "\") specifies property " + propertyName + " that doesn't exist in type " + parameter.getGenericType().getName());
}
}
}
});
}
}
private boolean hasPropertyWithName(ClassElement element, String propertyName) {
return element.getBeanProperties(PropertyElementQuery.of(element).includes(Collections.singleton(propertyName)))
.stream().anyMatch(v -> !v.isExcluded());
}
@Override
public @NonNull VisitorKind getVisitorKind() {
return VisitorKind.ISOLATING;
}
}