package io.micronaut.validation.routes.rules;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import io.micronaut.core.bind.annotation.Bindable;
import io.micronaut.core.naming.NameUtils;
import io.micronaut.http.annotation.RequestBean;
import io.micronaut.http.uri.UriMatchTemplate;
import io.micronaut.http.uri.UriMatchVariable;
import io.micronaut.inject.ast.ClassElement;
import io.micronaut.inject.ast.MethodElement;
import io.micronaut.inject.ast.ParameterElement;
import io.micronaut.inject.ast.TypedElement;
import io.micronaut.validation.routes.RouteValidationResult;
public class NullableParameterRule implements RouteValidationRule {
@Override
public RouteValidationResult validate(List<UriMatchTemplate> templates, ParameterElement[] parameters, MethodElement method) {
List<String> errorMessages = new ArrayList<>();
boolean isClient = method.hasAnnotation("io.micronaut.http.client.annotation.Client");
if (!isClient) {
Map<String, UriMatchVariable> variables = new HashMap<>();
Set<UriMatchVariable> required = new HashSet<>();
for (UriMatchTemplate template: templates) {
for (UriMatchVariable variable: template.getVariables()) {
if (!variable.isOptional() || variable.isExploded()) {
required.add(variable);
}
variables.compute(variable.getName(), (key, var) -> {
if (var == null) {
if (variable.isOptional() && !variable.isExploded()) {
return variable;
} else {
return null;
}
} else {
if (!var.isOptional() || var.isExploded()) {
if (variable.isOptional() && !variable.isExploded()) {
return variable;
} else {
return var;
}
} else {
return var;
}
}
});
}
}
for (UriMatchVariable variable: required) {
if (templates.stream().anyMatch(t -> !t.getVariableNames().contains(variable.getName()))) {
variables.putIfAbsent(variable.getName(), variable);
}
}
for (UriMatchVariable variable : variables.values()) {
Arrays.stream(parameters)
.flatMap(p -> getTypedElements(p).stream())
.filter(p -> p.getName().equals(variable.getName()))
.forEach(p -> {
ClassElement type = p.getType();
boolean hasDefaultValue = p.findAnnotation(Bindable.class).flatMap(av -> av.stringValue("defaultValue")).isPresent();
if (!isNullable(p) && type != null && !type.isAssignable(Optional.class) && !hasDefaultValue) {
errorMessages.add(String.format("The uri variable [%s] is optional, but the corresponding method argument [%s %s] is not defined as an Optional or annotated with the javax.annotation.Nullable annotation.", variable.getName(), p.getType().toString(), p.getName()));
}
});
}
}
return new RouteValidationResult(errorMessages.toArray(new String[0]));
}
private boolean isNullable(TypedElement p) {
return p.getAnnotationNames().stream().anyMatch(n -> NameUtils.getSimpleName(n).equals("Nullable"));
}
private List<TypedElement> getTypedElements(ParameterElement parameterElement) {
if (parameterElement.hasAnnotation(RequestBean.class)) {
return parameterElement.getType().getBeanProperties().stream()
.filter(p -> p.hasStereotype(Bindable.class))
.collect(Collectors.toList());
} else {
return Collections.singletonList(parameterElement);
}
}
}