package io.micronaut.http.bind;
import io.micronaut.core.bind.ArgumentBinder;
import io.micronaut.core.bind.annotation.Bindable;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.naming.NameUtils;
import io.micronaut.core.reflect.ClassUtils;
import io.micronaut.core.reflect.ReflectionUtils;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.core.util.clhm.ConcurrentLinkedHashMap;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.*;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.bind.binders.*;
import io.micronaut.http.cookie.Cookie;
import io.micronaut.http.cookie.Cookies;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.lang.annotation.Annotation;
import java.util.*;
import static io.micronaut.core.util.KotlinUtils.KOTLIN_COROUTINES_SUPPORTED;
@Singleton
public class DefaultRequestBinderRegistry implements RequestBinderRegistry {
private static final long CACHE_MAX_SIZE = 30;
private final Map<Class<? extends Annotation>, RequestArgumentBinder> byAnnotation = new LinkedHashMap<>();
private final Map<TypeAndAnnotation, RequestArgumentBinder> byTypeAndAnnotation = new LinkedHashMap<>();
private final Map<Integer, RequestArgumentBinder> byType = new LinkedHashMap<>();
private final ConversionService<?> conversionService;
private final Map<TypeAndAnnotation, Optional<RequestArgumentBinder>> argumentBinderCache =
new ConcurrentLinkedHashMap.Builder<TypeAndAnnotation, Optional<RequestArgumentBinder>>().maximumWeightedCapacity(CACHE_MAX_SIZE).build();
public DefaultRequestBinderRegistry(ConversionService conversionService, RequestArgumentBinder... binders) {
this(conversionService, Arrays.asList(binders));
}
@Inject public DefaultRequestBinderRegistry(ConversionService conversionService, List<RequestArgumentBinder> binders) {
this.conversionService = conversionService;
if (CollectionUtils.isNotEmpty(binders)) {
for (RequestArgumentBinder binder : binders) {
addRequestArgumentBinder(binder);
}
}
registerDefaultConverters(conversionService);
registerDefaultAnnotationBinders(byAnnotation);
byType.put(Argument.of(HttpHeaders.class).typeHashCode(), (RequestArgumentBinder<HttpHeaders>) (argument, source) -> () -> Optional.of(source.getHeaders()));
byType.put(Argument.of(HttpRequest.class).typeHashCode(), (RequestArgumentBinder<HttpRequest>) (argument, source) -> {
Optional<Argument<?>> typeVariable = argument.getFirstTypeVariable()
.filter(arg -> arg.getType() != Object.class)
.filter(arg -> arg.getType() != Void.class);
if (typeVariable.isPresent() && HttpMethod.permitsRequestBody(source.getMethod())) {
if (source.getBody().isPresent()) {
return () -> Optional.of(new FullHttpRequest(source, typeVariable.get()));
} else {
return ArgumentBinder.BindingResult.UNSATISFIED;
}
} else {
return () -> Optional.of(source);
}
});
byType.put(Argument.of(HttpParameters.class).typeHashCode(), (RequestArgumentBinder<HttpParameters>) (argument, source) -> () -> Optional.of(source.getParameters()));
byType.put(Argument.of(Cookies.class).typeHashCode(), (RequestArgumentBinder<Cookies>) (argument, source) -> () -> Optional.of(source.getCookies()));
byType.put(Argument.of(Cookie.class).typeHashCode(), (RequestArgumentBinder<Cookie>) (context, source) -> {
Cookies cookies = source.getCookies();
String name = context.getArgument().getName();
Cookie cookie = cookies.get(name);
if (cookie == null) {
cookie = cookies.get(NameUtils.hyphenate(name));
}
Cookie finalCookie = cookie;
return () -> finalCookie != null ? Optional.of(finalCookie) : Optional.empty();
});
}
@SuppressWarnings("rawtypes")
@Override
public <T, ST> void addRequestArgumentBinder(ArgumentBinder<T, ST> binder) {
if (binder instanceof AnnotatedRequestArgumentBinder) {
AnnotatedRequestArgumentBinder<?, ?> annotatedRequestArgumentBinder = (AnnotatedRequestArgumentBinder) binder;
Class<? extends Annotation> annotationType = annotatedRequestArgumentBinder.getAnnotationType();
if (binder instanceof TypedRequestArgumentBinder) {
TypedRequestArgumentBinder<?> typedRequestArgumentBinder = (TypedRequestArgumentBinder) binder;
Argument argumentType = typedRequestArgumentBinder.argumentType();
byTypeAndAnnotation.put(new TypeAndAnnotation(argumentType, annotationType), (RequestArgumentBinder) binder);
List<Class<?>> superTypes = typedRequestArgumentBinder.superTypes();
if (CollectionUtils.isNotEmpty(superTypes)) {
for (Class<?> superType : superTypes) {
byTypeAndAnnotation.put(new TypeAndAnnotation(Argument.of(superType), annotationType), (RequestArgumentBinder) binder);
}
} else if (typedRequestArgumentBinder.supportsSuperTypes()) {
Set<Class> allInterfaces = ReflectionUtils.getAllInterfaces(argumentType.getType());
if (ClassUtils.REFLECTION_LOGGER.isWarnEnabled()) {
ClassUtils.REFLECTION_LOGGER.warn(
"Request argument binder [{}] triggered the use of reflection for types {}",
typedRequestArgumentBinder,
allInterfaces
);
}
for (Class<?> itfce : allInterfaces) {
byTypeAndAnnotation.put(new TypeAndAnnotation(Argument.of(itfce), annotationType), (RequestArgumentBinder) binder);
}
}
} else {
byAnnotation.put(annotationType, annotatedRequestArgumentBinder);
}
} else if (binder instanceof TypedRequestArgumentBinder) {
TypedRequestArgumentBinder typedRequestArgumentBinder = (TypedRequestArgumentBinder) binder;
byType.put(typedRequestArgumentBinder.argumentType().typeHashCode(), typedRequestArgumentBinder);
}
}
@Override
public <T> Optional<ArgumentBinder<T, HttpRequest<?>>> findArgumentBinder(Argument<T> argument, HttpRequest<?> source) {
Optional<Class<? extends Annotation>> opt = argument.getAnnotationMetadata().getAnnotationTypeByStereotype(Bindable.class);
if (opt.isPresent()) {
Class<? extends Annotation> annotationType = opt.get();
RequestArgumentBinder<T> binder = findBinder(argument, annotationType);
if (binder == null) {
binder = byAnnotation.get(annotationType);
}
if (binder != null) {
return Optional.of(binder);
}
} else {
RequestArgumentBinder<T> binder = byType.get(argument.typeHashCode());
if (binder != null) {
return Optional.of(binder);
} else {
binder = byType.get(Argument.of(argument.getType()).typeHashCode());
if (binder != null) {
return Optional.of(binder);
}
}
}
return Optional.of(new ParameterAnnotationBinder<>(conversionService));
}
protected <T> RequestArgumentBinder findBinder(Argument<T> argument, Class<? extends Annotation> annotationType) {
TypeAndAnnotation key = new TypeAndAnnotation(argument, annotationType);
return argumentBinderCache.computeIfAbsent(key, key1 -> {
RequestArgumentBinder requestArgumentBinder = byTypeAndAnnotation.get(key1);
if (requestArgumentBinder == null) {
Class<?> javaType = key1.type.getType();
for (Map.Entry<TypeAndAnnotation, RequestArgumentBinder> entry : byTypeAndAnnotation.entrySet()) {
TypeAndAnnotation typeAndAnnotation = entry.getKey();
if (typeAndAnnotation.annotation == annotationType) {
Argument<?> t = typeAndAnnotation.type;
if (t.getType().isAssignableFrom(javaType)) {
requestArgumentBinder = entry.getValue();
if (requestArgumentBinder != null) {
break;
}
}
}
}
if (requestArgumentBinder == null) {
requestArgumentBinder = byTypeAndAnnotation.get(new TypeAndAnnotation(Argument.of(argument.getType()), annotationType));
}
}
return Optional.ofNullable(requestArgumentBinder);
}).orElse(null);
}
protected void registerDefaultConverters(ConversionService<?> conversionService) {
conversionService.addConverter(
CharSequence.class,
MediaType.class, (object, targetType, context) -> {
if (StringUtils.isEmpty(object)) {
return Optional.empty();
} else {
final String str = object.toString();
try {
return Optional.of(new MediaType(str));
} catch (IllegalArgumentException e) {
context.reject(e);
return Optional.empty();
}
}
});
}
protected void registerDefaultAnnotationBinders(Map<Class<? extends Annotation>, RequestArgumentBinder> byAnnotation) {
DefaultBodyAnnotationBinder bodyBinder = new DefaultBodyAnnotationBinder(conversionService);
byAnnotation.put(Body.class, bodyBinder);
CookieAnnotationBinder<Object> cookieAnnotationBinder = new CookieAnnotationBinder<>(conversionService);
byAnnotation.put(cookieAnnotationBinder.getAnnotationType(), cookieAnnotationBinder);
HeaderAnnotationBinder<Object> headerAnnotationBinder = new HeaderAnnotationBinder<>(conversionService);
byAnnotation.put(headerAnnotationBinder.getAnnotationType(), headerAnnotationBinder);
ParameterAnnotationBinder<Object> parameterAnnotationBinder = new ParameterAnnotationBinder<>(conversionService);
byAnnotation.put(parameterAnnotationBinder.getAnnotationType(), parameterAnnotationBinder);
RequestAttributeAnnotationBinder<Object> requestAttributeAnnotationBinder = new RequestAttributeAnnotationBinder<>(conversionService);
byAnnotation.put(requestAttributeAnnotationBinder.getAnnotationType(), requestAttributeAnnotationBinder);
PathVariableAnnotationBinder<Object> pathVariableAnnotationBinder = new PathVariableAnnotationBinder<>(conversionService);
byAnnotation.put(pathVariableAnnotationBinder.getAnnotationType(), pathVariableAnnotationBinder);
RequestBeanAnnotationBinder<Object> requestBeanAnnotationBinder = new RequestBeanAnnotationBinder<>(this, conversionService);
byAnnotation.put(requestBeanAnnotationBinder.getAnnotationType(), requestBeanAnnotationBinder);
if (KOTLIN_COROUTINES_SUPPORTED) {
ContinuationArgumentBinder continuationArgumentBinder = new ContinuationArgumentBinder();
byType.put(continuationArgumentBinder.argumentType().typeHashCode(), continuationArgumentBinder);
}
}
private static final class TypeAndAnnotation {
private final Argument<?> type;
private final Class<? extends Annotation> annotation;
public TypeAndAnnotation(Argument<?> type, Class<? extends Annotation> annotation) {
this.type = type;
this.annotation = annotation;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
TypeAndAnnotation that = (TypeAndAnnotation) o;
if (!type.equalsType(that.type)) {
return false;
}
return annotation.equals(that.annotation);
}
@Override
public int hashCode() {
int result = type.typeHashCode();
result = 31 * result + annotation.hashCode();
return result;
}
}
}