
com.linecorp.armeria.server.AnnotatedValueResolver Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of armeria-shaded Show documentation
Show all versions of armeria-shaded Show documentation
Asynchronous HTTP/2 RPC/REST client/server library built on top of Java 8, Netty, Thrift and GRPC (armeria-shaded)
/*
* Copyright 2018 LINE Corporation
*
* LINE Corporation licenses this file to you 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 com.linecorp.armeria.server;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.linecorp.armeria.common.HttpParameters.EMPTY_PARAMETERS;
import static com.linecorp.armeria.internal.DefaultValues.getSpecifiedValue;
import static com.linecorp.armeria.server.AnnotatedElementNameUtil.findName;
import static com.linecorp.armeria.server.AnnotatedHttpServiceTypeUtil.normalizeContainerType;
import static com.linecorp.armeria.server.AnnotatedHttpServiceTypeUtil.stringToType;
import static com.linecorp.armeria.server.AnnotatedHttpServiceTypeUtil.validateElementType;
import static java.util.Objects.requireNonNull;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ascii;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.MapMaker;
import com.linecorp.armeria.common.AggregatedHttpMessage;
import com.linecorp.armeria.common.HttpHeaderNames;
import com.linecorp.armeria.common.HttpParameters;
import com.linecorp.armeria.common.HttpRequest;
import com.linecorp.armeria.common.MediaType;
import com.linecorp.armeria.common.Request;
import com.linecorp.armeria.common.RequestContext;
import com.linecorp.armeria.common.util.Exceptions;
import com.linecorp.armeria.internal.FallthroughException;
import com.linecorp.armeria.server.AnnotatedBeanFactory.BeanFactoryId;
import com.linecorp.armeria.server.annotation.ByteArrayRequestConverterFunction;
import com.linecorp.armeria.server.annotation.Cookies;
import com.linecorp.armeria.server.annotation.Default;
import com.linecorp.armeria.server.annotation.Header;
import com.linecorp.armeria.server.annotation.JacksonRequestConverterFunction;
import com.linecorp.armeria.server.annotation.Param;
import com.linecorp.armeria.server.annotation.RequestConverterFunction;
import com.linecorp.armeria.server.annotation.RequestObject;
import com.linecorp.armeria.server.annotation.StringRequestConverterFunction;
import io.netty.handler.codec.http.HttpConstants;
import io.netty.handler.codec.http.QueryStringDecoder;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.ServerCookieDecoder;
import io.netty.util.AsciiString;
final class AnnotatedValueResolver {
private static final Logger logger = LoggerFactory.getLogger(AnnotatedValueResolver.class);
private static final List defaultRequestConverters =
ImmutableList.of((resolverContext, expectedResultType, beanFactoryId) ->
AnnotatedBeanFactory.find(beanFactoryId)
.orElseThrow(RequestConverterFunction::fallthrough)
.apply(resolverContext),
RequestObjectResolver.of(new JacksonRequestConverterFunction()),
RequestObjectResolver.of(new StringRequestConverterFunction()),
RequestObjectResolver.of(new ByteArrayRequestConverterFunction()));
private static final Object[] emptyArguments = new Object[0];
/**
* Returns an array of arguments which are resolved by each {@link AnnotatedValueResolver} of the
* specified {@code resolvers}.
*/
static Object[] toArguments(List resolvers,
ResolverContext resolverContext) {
requireNonNull(resolvers, "resolvers");
requireNonNull(resolverContext, "resolverContext");
if (resolvers.isEmpty()) {
return emptyArguments;
}
return resolvers.stream().map(resolver -> resolver.resolve(resolverContext)).toArray();
}
/**
* Returns a list of {@link RequestObjectResolver} that default request converters are added.
*/
static List toRequestObjectResolvers(List converters) {
final ImmutableList.Builder builder = ImmutableList.builder();
// Wrap every converters received from a user with a default object resolver.
converters.stream().map(RequestObjectResolver::of).forEach(builder::add);
builder.addAll(defaultRequestConverters);
return builder.build();
}
/**
* Returns a list of {@link AnnotatedValueResolver} which is constructed with the specified
* {@link Executable}, {@code pathParams} and {@code objectResolvers}. The {@link Executable} can be
* one of {@link Constructor} or {@link Method}.
*/
static List of(Executable constructorOrMethod, Set pathParams,
List objectResolvers) {
final Parameter[] parameters = constructorOrMethod.getParameters();
if (parameters.length == 0) {
throw new NoParameterException(constructorOrMethod.toGenericString());
}
//
// Try to check whether it is an annotated constructor or method first. e.g.
//
// @Param
// void setter(String name) { ... }
//
// In this case, we need to retrieve the value of @Param annotation from 'name' parameter,
// not the constructor or method. Also 'String' type is used for the parameter.
//
final Optional resolver;
if (isAnnotationPresent(constructorOrMethod)) {
//
// Only allow a single parameter on an annotated method. The followings cause an error:
//
// @Param
// void setter(String name, int id, String address) { ... }
//
// @Param
// void setter() { ... }
//
if (parameters.length != 1) {
throw new IllegalArgumentException("Only one parameter is allowed to an annotated method: " +
constructorOrMethod.toGenericString());
}
//
// Filter out like the following case:
//
// @Param
// void setter(@Header String name) { ... }
//
if (isAnnotationPresent(parameters[0])) {
throw new IllegalArgumentException("Both a method and parameter are annotated: " +
constructorOrMethod.toGenericString());
}
resolver = of(constructorOrMethod,
parameters[0], parameters[0].getType(), pathParams, objectResolvers);
} else {
//
// There's no annotation. So there should be no @Default annotation, too.
// e.g.
// @Default("a")
// void method1(ServiceRequestContext) { ... }
//
if (constructorOrMethod.isAnnotationPresent(Default.class)) {
throw new IllegalArgumentException(
'@' + Default.class.getSimpleName() + " is not supported for: " +
constructorOrMethod.toGenericString());
}
resolver = Optional.empty();
}
//
// If there is no annotation on the constructor or method, try to check whether it has
// annotated parameters. e.g.
//
// void setter1(@Param String name) { ... }
// void setter2(@Param String name, @Header List xForwardedFor) { ... }
//
final List list =
resolver.>map(ImmutableList::of).orElseGet(
() -> Arrays.stream(parameters)
.map(p -> of(p, pathParams, objectResolvers))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList()));
if (list.isEmpty()) {
throw new NoAnnotatedParameterException(constructorOrMethod.toGenericString());
}
if (list.size() != parameters.length) {
throw new IllegalArgumentException("Unsupported parameter exists: " +
constructorOrMethod.toGenericString());
}
return list;
}
/**
* Returns a list of {@link AnnotatedValueResolver} which is constructed with the specified
* {@link Parameter}, {@code pathParams} and {@code objectResolvers}.
*/
static Optional of(Parameter parameter, Set pathParams,
List objectResolvers) {
return of(parameter, parameter, parameter.getType(), pathParams, objectResolvers);
}
/**
* Returns a list of {@link AnnotatedValueResolver} which is constructed with the specified
* {@link Field}, {@code pathParams} and {@code objectResolvers}.
*/
static Optional of(Field field, Set pathParams,
List objectResolvers) {
return of(field, field, field.getType(), pathParams, objectResolvers);
}
/**
* Creates a new {@link AnnotatedValueResolver} instance if the specified {@code annotatedElement} is
* a component of {@link AnnotatedHttpService}.
*
* @param annotatedElement an element which is annotated with a value specifier such as {@link Param} and
* {@link Header}.
* @param typeElement an element which is used for retrieving its type and name.
* @param type a type of the given {@link Parameter} or {@link Field}. It is a type of
* the specified {@code typeElement} parameter.
* @param pathParams a set of path variables.
* @param objectResolvers a list of {@link RequestObjectResolver} to be evaluated for the objects which
* are annotated with {@link RequestObject} annotation.
*/
private static Optional of(AnnotatedElement annotatedElement,
AnnotatedElement typeElement, Class> type,
Set pathParams,
List objectResolvers) {
requireNonNull(annotatedElement, "annotatedElement");
requireNonNull(typeElement, "typeElement");
requireNonNull(type, "type");
requireNonNull(pathParams, "pathParams");
requireNonNull(objectResolvers, "objectResolvers");
final Param param = annotatedElement.getAnnotation(Param.class);
if (param != null) {
final String name = findName(param, typeElement);
if (pathParams.contains(name)) {
return Optional.of(ofPathVariable(param, name, annotatedElement, typeElement, type));
} else {
return Optional.of(ofHttpParameter(param, name, annotatedElement, typeElement, type));
}
}
final Header header = annotatedElement.getAnnotation(Header.class);
if (header != null) {
return Optional.of(ofHeader(header, annotatedElement, typeElement, type));
}
final RequestObject requestObject = annotatedElement.getAnnotation(RequestObject.class);
if (requestObject != null) {
return Optional.of(ofRequestObject(requestObject, annotatedElement, type,
pathParams, objectResolvers));
}
// There should be no '@Default' annotation on 'annotatedElement' if 'annotatedElement' is
// different from 'typeElement', because it was checked before calling this method.
// So, 'typeElement' should be used when finding an injectable type because we need to check
// syntactic errors like below:
//
// void method1(@Default("a") ServiceRequestContext) { ... }
//
return Optional.ofNullable(ofInjectableTypes(typeElement, type));
}
private static boolean isAnnotationPresent(AnnotatedElement element) {
return element.isAnnotationPresent(Param.class) ||
element.isAnnotationPresent(Header.class) ||
element.isAnnotationPresent(RequestObject.class);
}
private static AnnotatedValueResolver ofPathVariable(Param param, String name,
AnnotatedElement annotatedElement,
AnnotatedElement typeElement, Class> type) {
return builder(annotatedElement, type)
.annotation(param)
.httpElementName(name)
.typeElement(typeElement)
.supportOptional(true)
.pathVariable(true)
.resolver(resolver(ctx -> ctx.context().pathParam(name)))
.build();
}
private static AnnotatedValueResolver ofHttpParameter(Param param, String name,
AnnotatedElement annotatedElement,
AnnotatedElement typeElement, Class> type) {
return builder(annotatedElement, type)
.annotation(param)
.httpElementName(name)
.typeElement(typeElement)
.supportOptional(true)
.supportDefault(true)
.supportContainer(true)
.aggregation(AggregationStrategy.FOR_FORM_DATA)
.resolver(resolver(ctx -> ctx.httpParameters().getAll(name),
() -> "Cannot resolve a value from HTTP parameter: " + name))
.build();
}
private static AnnotatedValueResolver ofHeader(Header header,
AnnotatedElement annotatedElement,
AnnotatedElement typeElement, Class> type) {
final String name = findName(header, typeElement);
return builder(annotatedElement, type)
.annotation(header)
.httpElementName(name)
.typeElement(typeElement)
.supportOptional(true)
.supportDefault(true)
.supportContainer(true)
.resolver(resolver(
ctx -> ctx.request().headers().getAll(AsciiString.of(name)),
() -> "Cannot resolve a value from HTTP header: " + name))
.build();
}
private static AnnotatedValueResolver ofRequestObject(RequestObject requestObject,
AnnotatedElement annotatedElement,
Class> type, Set pathParams,
List objectResolvers) {
final List newObjectResolvers;
if (requestObject.value() != RequestConverterFunction.class) {
// There is a converter which is specified by a user.
final RequestConverterFunction requestConverterFunction =
AnnotatedHttpServiceFactory.getInstance(requestObject, RequestConverterFunction.class);
newObjectResolvers = ImmutableList.builder()
.add(RequestObjectResolver.of(requestConverterFunction))
.addAll(objectResolvers)
.build();
} else {
newObjectResolvers = objectResolvers;
}
// To do recursive resolution like a bean inside another bean, the original object resolvers should
// be passed into the AnnotatedBeanFactory#register.
final BeanFactoryId beanFactoryId = AnnotatedBeanFactory.register(type, pathParams, objectResolvers);
return builder(annotatedElement, type)
.annotation(requestObject)
.aggregation(AggregationStrategy.ALWAYS)
.resolver(resolver(newObjectResolvers, beanFactoryId))
.build();
}
@Nullable
private static AnnotatedValueResolver ofInjectableTypes(AnnotatedElement annotatedElement, Class> type) {
if (type == RequestContext.class || type == ServiceRequestContext.class) {
return builder(annotatedElement, type)
.resolver((unused, ctx) -> ctx.context())
.build();
}
if (type == Request.class || type == HttpRequest.class) {
return builder(annotatedElement, type)
.resolver((unused, ctx) -> ctx.request())
.build();
}
if (type == AggregatedHttpMessage.class) {
return builder(annotatedElement, AggregatedHttpMessage.class)
.resolver((unused, ctx) -> ctx.message())
.aggregation(AggregationStrategy.ALWAYS)
.build();
}
if (type == HttpParameters.class) {
return builder(annotatedElement, HttpParameters.class)
.resolver((unused, ctx) -> ctx.httpParameters())
.aggregation(AggregationStrategy.FOR_FORM_DATA)
.build();
}
if (type == Cookies.class) {
return builder(annotatedElement, Cookies.class)
.resolver((unused, ctx) -> {
final List values = ctx.request().headers().getAll(HttpHeaderNames.COOKIE);
if (values.isEmpty()) {
return Cookies.copyOf(ImmutableSet.of());
}
final ImmutableSet.Builder cookies = ImmutableSet.builder();
values.stream()
.map(ServerCookieDecoder.STRICT::decode)
.forEach(cookies::addAll);
return Cookies.copyOf(cookies.build());
})
.build();
}
// Unsupported type.
return null;
}
/**
* Returns a single value resolver which retrieves a value from the specified {@code getter}
* and converts it.
*/
private static BiFunction
resolver(Function getter) {
return (resolver, ctx) -> resolver.convert(getter.apply(ctx));
}
/**
* Returns a collection value resolver which retrieves a list of string from the specified {@code getter}
* and adds them to the specified collection data type.
*/
private static BiFunction
resolver(Function> getter, Supplier failureMessageSupplier) {
return (resolver, ctx) -> {
final List values = getter.apply(ctx);
if (!resolver.hasContainer()) {
if (values != null && !values.isEmpty()) {
return resolver.convert(values.get(0));
}
return resolver.defaultOrException();
}
try {
assert resolver.containerType() != null;
@SuppressWarnings("unchecked")
final Collection
© 2015 - 2025 Weber Informatics LLC | Privacy Policy