{"action":"created","comment":{"url":"https://api.github.com/repos/line/armeria/pulls/comments/190132611","pull_request_review_id":121684991,"id":190132611,"diff_hunk":"@@ -0,0 +1,1079 @@\n+/*\n+ * Copyright 2018 LINE Corporation\n+ *\n+ * LINE Corporation licenses this file to you under the Apache License,\n+ * version 2.0 (the \"License\"); you may not use this file except in compliance\n+ * with the License. You may obtain a copy of the License at:\n+ *\n+ *   https://www.apache.org/licenses/LICENSE-2.0\n+ *\n+ * Unless required by applicable law or agreed to in writing, software\n+ * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n+ * License for the specific language governing permissions and limitations\n+ * under the License.\n+ */\n+package com.linecorp.armeria.server;\n+\n+import static com.google.common.base.Preconditions.checkArgument;\n+import static com.google.common.collect.ImmutableMap.toImmutableMap;\n+import static com.linecorp.armeria.common.HttpParameters.EMPTY_PARAMETERS;\n+import static com.linecorp.armeria.internal.DefaultValues.getSpecifiedValue;\n+import static com.linecorp.armeria.server.AnnotatedElementNameUtil.findName;\n+import static com.linecorp.armeria.server.AnnotatedHttpServiceTypeUtil.normalizeContainerType;\n+import static com.linecorp.armeria.server.AnnotatedHttpServiceTypeUtil.stringToType;\n+import static com.linecorp.armeria.server.AnnotatedHttpServiceTypeUtil.validateElementType;\n+import static java.util.Objects.requireNonNull;\n+\n+import java.lang.annotation.Annotation;\n+import java.lang.reflect.AnnotatedElement;\n+import java.lang.reflect.Constructor;\n+import java.lang.reflect.Executable;\n+import java.lang.reflect.Field;\n+import java.lang.reflect.Method;\n+import java.lang.reflect.Parameter;\n+import java.lang.reflect.ParameterizedType;\n+import java.lang.reflect.Type;\n+import java.nio.charset.StandardCharsets;\n+import java.util.AbstractMap.SimpleImmutableEntry;\n+import java.util.Arrays;\n+import java.util.Collection;\n+import java.util.EnumSet;\n+import java.util.List;\n+import java.util.Map;\n+import java.util.Map.Entry;\n+import java.util.Optional;\n+import java.util.Set;\n+import java.util.concurrent.ConcurrentMap;\n+import java.util.function.BiFunction;\n+import java.util.function.Function;\n+import java.util.function.Supplier;\n+import java.util.stream.Collectors;\n+\n+import javax.annotation.Nullable;\n+\n+import org.slf4j.Logger;\n+import org.slf4j.LoggerFactory;\n+\n+import com.google.common.annotations.VisibleForTesting;\n+import com.google.common.base.Ascii;\n+import com.google.common.base.MoreObjects;\n+import com.google.common.collect.ImmutableList;\n+import com.google.common.collect.MapMaker;\n+\n+import com.linecorp.armeria.common.AggregatedHttpMessage;\n+import com.linecorp.armeria.common.HttpParameters;\n+import com.linecorp.armeria.common.HttpRequest;\n+import com.linecorp.armeria.common.MediaType;\n+import com.linecorp.armeria.common.Request;\n+import com.linecorp.armeria.common.RequestContext;\n+import com.linecorp.armeria.common.util.Exceptions;\n+import com.linecorp.armeria.internal.FallthroughException;\n+import com.linecorp.armeria.server.AnnotatedBeanFactory.BeanFactoryId;\n+import com.linecorp.armeria.server.annotation.ByteArrayRequestConverterFunction;\n+import com.linecorp.armeria.server.annotation.Default;\n+import com.linecorp.armeria.server.annotation.Header;\n+import com.linecorp.armeria.server.annotation.JacksonRequestConverterFunction;\n+import com.linecorp.armeria.server.annotation.Param;\n+import com.linecorp.armeria.server.annotation.RequestConverterFunction;\n+import com.linecorp.armeria.server.annotation.RequestObject;\n+import com.linecorp.armeria.server.annotation.StringRequestConverterFunction;\n+\n+import io.netty.handler.codec.http.HttpConstants;\n+import io.netty.handler.codec.http.QueryStringDecoder;\n+import io.netty.util.AsciiString;\n+\n+final class AnnotatedValueResolver {\n+    private static final Logger logger = LoggerFactory.getLogger(AnnotatedValueResolver.class);\n+\n+    private static final List<RequestObjectResolver> defaultRequestConverters =\n+            ImmutableList.of((resolverContext, expectedResultType, beanFactoryId) ->\n+                                     AnnotatedBeanFactory.find(beanFactoryId)\n+                                                         .orElseThrow(RequestConverterFunction::fallthrough)\n+                                                         .apply(resolverContext),\n+                             RequestObjectResolver.of(new JacksonRequestConverterFunction()),\n+                             RequestObjectResolver.of(new StringRequestConverterFunction()),\n+                             RequestObjectResolver.of(new ByteArrayRequestConverterFunction()));\n+\n+    private static final Object[] emptyArguments = new Object[0];\n+\n+    /**\n+     * Returns an array of arguments which are resolved by each {@link AnnotatedValueResolver} of the\n+     * specified {@code resolvers}.\n+     */\n+    static Object[] toArguments(List<AnnotatedValueResolver> resolvers,\n+                                ResolverContext resolverContext) {\n+        requireNonNull(resolvers, \"resolvers\");\n+        if (resolvers.isEmpty()) {\n+            return emptyArguments;\n+        }\n+        return resolvers.stream().map(resolver -> resolver.resolve(resolverContext)).toArray();\n+    }\n+\n+    /**\n+     * Returns a list of {@link RequestObjectResolver} that default request converters are added.\n+     */\n+    static List<RequestObjectResolver> toRequestObjectResolvers(List<RequestConverterFunction> converters) {\n+        final ImmutableList.Builder<RequestObjectResolver> builder = ImmutableList.builder();\n+        // Wrap every converters received from a user with a default object resolver.\n+        converters.stream().map(RequestObjectResolver::of).forEach(builder::add);\n+        builder.addAll(defaultRequestConverters);\n+        return builder.build();\n+    }\n+\n+    /**\n+     * Returns a list of {@link AnnotatedValueResolver} which is constructed with the specified\n+     * {@link Executable}, {@code pathParams} and {@code objectResolvers}. The {@link Executable} can be\n+     * one of {@link Constructor} or {@link Method}.\n+     */\n+    static List<AnnotatedValueResolver> of(Executable constructorOrMethod, Set<String> pathParams,\n+                                           List<RequestObjectResolver> objectResolvers) {\n+        final Parameter[] parameters = constructorOrMethod.getParameters();\n+        if (parameters.length == 0) {\n+            throw new NoParameterException(constructorOrMethod.toGenericString());\n+        }\n+        //\n+        // Try to check whether it is an annotated constructor or method first. e.g.\n+        //\n+        // @Param\n+        // void setter(String name) { ... }\n+        //\n+        // In this case, we need to retrieve the value of @Param annotation from 'name' parameter,\n+        // not the constructor or method. Also 'String' type is used for the parameter.\n+        //\n+        final Optional<AnnotatedValueResolver> resolver;\n+        if (isAnnotationPresent(constructorOrMethod)) {\n+            //\n+            // Only allow a single parameter on an annotated method. The followings cause an error:\n+            //\n+            // @Param\n+            // void setter(String name, int id, String address) { ... }\n+            //\n+            // @Param\n+            // void setter() { ... }\n+            //\n+            if (parameters.length != 1) {\n+                throw new IllegalArgumentException(\"Only one parameter is allowed to an annotated method: \" +\n+                                                   constructorOrMethod.toGenericString());\n+            }\n+            //\n+            // Filter out like the following case:\n+            //\n+            // @Param\n+            // void setter(@Header String name) { ... }\n+            //\n+            if (isAnnotationPresent(parameters[0])) {\n+                throw new IllegalArgumentException(\"Both a method and parameter are annotated: \" +\n+                                                   constructorOrMethod.toGenericString());\n+            }\n+\n+            resolver = of(constructorOrMethod,\n+                          parameters[0], parameters[0].getType(), pathParams, objectResolvers);\n+        } else {\n+            //\n+            // There's no annotation. So there should be no @Default annotation, too.\n+            // e.g.\n+            // @Default(\"a\")\n+            // void method1(ServiceRequestContext) { ... }\n+            //\n+            if (constructorOrMethod.isAnnotationPresent(Default.class)) {\n+                throw new IllegalArgumentException(\n+                        '@' + Default.class.getSimpleName() + \" is not supported for: \" +\n+                        constructorOrMethod.toGenericString());\n+            }\n+\n+            resolver = Optional.empty();\n+        }\n+\n+        //\n+        // If there is no annotation on the constructor or method, try to check whether it has\n+        // annotated parameters. e.g.\n+        //\n+        // void setter1(@Param String name) { ... }\n+        // void setter2(@Param String name, @Header List<String> xForwardedFor) { ... }\n+        //\n+        final List<AnnotatedValueResolver> list =\n+                resolver.<List<AnnotatedValueResolver>>map(ImmutableList::of).orElseGet(\n+                        () -> Arrays.stream(parameters)\n+                                    .map(p -> of(p, pathParams, objectResolvers))\n+                                    .filter(Optional::isPresent)\n+                                    .map(Optional::get)\n+                                    .collect(Collectors.toList()));\n+        if (list.isEmpty()) {\n+            throw new NoAnnotatedParameterException(constructorOrMethod.toGenericString());\n+        }\n+        if (list.size() != parameters.length) {\n+            throw new IllegalArgumentException(\"Unsupported parameter exists: \" +\n+                                               constructorOrMethod.toGenericString());\n+        }\n+        return list;\n+    }\n+\n+    /**\n+     * Returns a list of {@link AnnotatedValueResolver} which is constructed with the specified\n+     * {@link Parameter}, {@code pathParams} and {@code objectResolvers}.\n+     */\n+    static Optional<AnnotatedValueResolver> of(Parameter parameter, Set<String> pathParams,\n+                                               List<RequestObjectResolver> objectResolvers) {\n+        return of(parameter, parameter, parameter.getType(), pathParams, objectResolvers);\n+    }\n+\n+    /**\n+     * Returns a list of {@link AnnotatedValueResolver} which is constructed with the specified\n+     * {@link Field}, {@code pathParams} and {@code objectResolvers}.\n+     */\n+    static Optional<AnnotatedValueResolver> of(Field field, Set<String> pathParams,\n+                                               List<RequestObjectResolver> objectResolvers) {\n+        return of(field, field, field.getType(), pathParams, objectResolvers);\n+    }\n+\n+    /**\n+     * Creates a new {@link AnnotatedValueResolver} instance if the specified {@code annotatedElement} is\n+     * a component of {@link AnnotatedHttpService}.\n+     *\n+     * @param annotatedElement an element which is annotated with a value specifier such as {@link Param} and\n+     *                         {@link Header}.\n+     * @param typeElement      an element which is used for retrieving its type and name.\n+     * @param type             a type of the given {@link Parameter} or {@link Field}. It is a type of\n+     *                         the specified {@code typeElement} parameter.\n+     * @param pathParams       a set of path variables.\n+     * @param objectResolvers  a list of {@link RequestObjectResolver} to be evaluated for the objects which\n+     *                         are annotated with {@link RequestObject} annotation.\n+     */\n+    private static Optional<AnnotatedValueResolver> of(AnnotatedElement annotatedElement,\n+                                                       AnnotatedElement typeElement, Class<?> type,\n+                                                       Set<String> pathParams,\n+                                                       List<RequestObjectResolver> objectResolvers) {\n+        requireNonNull(annotatedElement, \"annotatedElement\");\n+        requireNonNull(typeElement, \"typeElement\");\n+        requireNonNull(type, \"type\");\n+        requireNonNull(pathParams, \"pathParams\");\n+        requireNonNull(objectResolvers, \"objectResolvers\");\n+\n+        final Param param = annotatedElement.getAnnotation(Param.class);\n+        if (param != null) {\n+            final String name = findName(param, typeElement);\n+            if (pathParams.contains(name)) {\n+                return Optional.of(ofPathVariable(param, name, annotatedElement, typeElement, type));\n+            } else {\n+                return Optional.of(ofHttpParameter(param, name, annotatedElement, typeElement, type));\n+            }\n+        }\n+\n+        final Header header = annotatedElement.getAnnotation(Header.class);\n+        if (header != null) {\n+            return Optional.of(ofHeader(header, annotatedElement, typeElement, type));\n+        }\n+\n+        final RequestObject requestObject = annotatedElement.getAnnotation(RequestObject.class);\n+        if (requestObject != null) {\n+            return Optional.of(ofRequestObject(requestObject, annotatedElement, type,\n+                                               pathParams, objectResolvers));\n+        }\n+        // There should be no '@Default' annotation on 'annotatedElement' if 'annotatedElement' is\n+        // different from 'typeElement', because it was checked before calling this method.\n+        // So, 'typeElement' should be used when finding an injectable type because we need to check\n+        // syntactic errors like below:\n+        //\n+        // void method1(@Default(\"a\") ServiceRequestContext) { ... }\n+        //\n+        return Optional.ofNullable(ofInjectableTypes(typeElement, type));\n+    }\n+\n+    private static boolean isAnnotationPresent(AnnotatedElement element) {\n+        return element.isAnnotationPresent(Param.class) ||\n+               element.isAnnotationPresent(Header.class) ||\n+               element.isAnnotationPresent(RequestObject.class);\n+    }\n+\n+    private static AnnotatedValueResolver ofPathVariable(Param param, String name,\n+                                                         AnnotatedElement annotatedElement,\n+                                                         AnnotatedElement typeElement, Class<?> type) {\n+        return builder(annotatedElement, type)\n+                .annotation(param)\n+                .httpElementName(name)\n+                .typeElement(typeElement)\n+                .pathVariable(true)\n+                .resolver(resolver(ctx -> ctx.context().pathParam(name)))\n+                .build();\n+    }\n+\n+    private static AnnotatedValueResolver ofHttpParameter(Param param, String name,\n+                                                          AnnotatedElement annotatedElement,\n+                                                          AnnotatedElement typeElement, Class<?> type) {\n+        return builder(annotatedElement, type)\n+                .annotation(param)\n+                .httpElementName(name)\n+                .typeElement(typeElement)\n+                .supportOptional(true)\n+                .supportDefault(true)\n+                .supportContainer(true)\n+                .aggregation(AggregationStrategy.FOR_FORM_DATA)\n+                .resolver(resolver(ctx -> ctx.httpParameters().getAll(name),\n+                                   () -> \"Cannot resolve a value from HTTP parameter: \" + name))\n+                .build();\n+    }\n+\n+    private static AnnotatedValueResolver ofHeader(Header header,\n+                                                   AnnotatedElement annotatedElement,\n+                                                   AnnotatedElement typeElement, Class<?> type) {\n+        final String name = findName(header, typeElement);\n+        return builder(annotatedElement, type)\n+                .annotation(header)\n+                .httpElementName(name)\n+                .typeElement(typeElement)\n+                .supportOptional(true)\n+                .supportDefault(true)\n+                .supportContainer(true)\n+                .resolver(resolver(\n+                        ctx -> ctx.request().headers().getAll(AsciiString.of(name)),\n+                        () -> \"Cannot resolve a value from HTTP header: \" + name))\n+                .build();\n+    }\n+\n+    private static AnnotatedValueResolver ofRequestObject(RequestObject requestObject,\n+                                                          AnnotatedElement annotatedElement,\n+                                                          Class<?> type, Set<String> pathParams,\n+                                                          List<RequestObjectResolver> objectResolvers) {\n+        final List<RequestObjectResolver> newObjectResolvers;\n+        if (requestObject.value() != RequestConverterFunction.class) {\n+            // There is a converter which is specified by a user.\n+            final RequestConverterFunction requestConverterFunction =\n+                    AnnotatedHttpServiceFactory.getInstance(requestObject, RequestConverterFunction.class);\n+            newObjectResolvers = ImmutableList.<RequestObjectResolver>builder()\n+                    .add(RequestObjectResolver.of(requestConverterFunction))\n+                    .addAll(objectResolvers)\n+                    .build();\n+        } else {\n+            newObjectResolvers = objectResolvers;\n+        }\n+\n+        // To do recursive resolution like a bean inside another bean, the original object resolvers should\n+        // be passed into the AnnotatedBeanFactory#register.\n+        final BeanFactoryId beanFactoryId = AnnotatedBeanFactory.register(type, pathParams, objectResolvers);\n+\n+        return builder(annotatedElement, type)\n+                .annotation(requestObject)\n+                .aggregation(AggregationStrategy.ALWAYS)\n+                .resolver(resolver(newObjectResolvers, beanFactoryId))\n+                .build();\n+    }\n+\n+    @Nullable\n+    private static AnnotatedValueResolver ofInjectableTypes(AnnotatedElement annotatedElement, Class<?> type) {\n+        if (type == RequestContext.class || type == ServiceRequestContext.class) {\n+            return builder(annotatedElement, type)\n+                    .resolver((unused, ctx) -> ctx.context())\n+                    .build();\n+        }\n+\n+        if (type == Request.class || type == HttpRequest.class) {\n+            return builder(annotatedElement, type)\n+                    .resolver((unused, ctx) -> ctx.request())\n+                    .build();\n+        }\n+\n+        if (type == AggregatedHttpMessage.class) {\n+            return builder(annotatedElement, AggregatedHttpMessage.class)\n+                    .resolver((unused, ctx) -> ctx.message())\n+                    .aggregation(AggregationStrategy.ALWAYS)\n+                    .build();\n+        }\n+\n+        if (type == HttpParameters.class) {\n+            return builder(annotatedElement, HttpParameters.class)\n+                    .resolver((unused, ctx) -> ctx.httpParameters())\n+                    .aggregation(AggregationStrategy.FOR_FORM_DATA)\n+                    .build();\n+        }\n+        // Unsupported type.\n+        return null;\n+    }\n+\n+    /**\n+     * Returns a single value resolver which retrieves a value from the specified {@code getter}\n+     * and converts it.\n+     */\n+    private static BiFunction<AnnotatedValueResolver, ResolverContext, Object>\n+    resolver(Function<ResolverContext, String> getter) {\n+        return (resolver, ctx) -> resolver.convert(getter.apply(ctx));\n+    }\n+\n+    /**\n+     * Returns a collection value resolver which retrieves a list of string from the specified {@code getter}\n+     * and adds them to the specified collection data type.\n+     */\n+    private static BiFunction<AnnotatedValueResolver, ResolverContext, Object>\n+    resolver(Function<ResolverContext, List<String>> getter, Supplier<String> failureMessageSupplier) {\n+        return (resolver, ctx) -> {\n+            final List<String> values = getter.apply(ctx);\n+            if (!resolver.hasContainer()) {\n+                if (values != null && !values.isEmpty()) {\n+                    return resolver.convert(values.get(0));\n+                }\n+                return resolver.defaultOrException();\n+            }\n+\n+            try {\n+                assert resolver.containerType() != null;\n+                @SuppressWarnings(\"unchecked\")\n+                final Collection<Object> resolvedValues =\n+                        (Collection<Object>) resolver.containerType().newInstance();\n+\n+                // Do not convert value here because the element type is String.\n+                if (values != null && !values.isEmpty()) {\n+                    values.stream().map(resolver::convert).forEach(resolvedValues::add);\n+                } else {\n+                    final Object defaultValue = resolver.defaultOrException();\n+                    if (defaultValue != null) {\n+                        resolvedValues.add(defaultValue);\n+                    }\n+                }\n+                return resolvedValues;\n+            } catch (Throwable cause) {\n+                throw new IllegalArgumentException(failureMessageSupplier.get(), cause);\n+            }\n+        };\n+    }\n+\n+    /**\n+     * Returns a bean resolver which retrieves a value using request converters. If the target element\n+     * is an annotated bean, a bean factory of the specified {@link BeanFactoryId} will be used for creating an\n+     * instance.\n+     */\n+    private static BiFunction<AnnotatedValueResolver, ResolverContext, Object>\n+    resolver(List<RequestObjectResolver> objectResolvers, BeanFactoryId beanFactoryId) {\n+        return (resolver, ctx) -> {\n+            Object value = null;\n+            for (final RequestObjectResolver objectResolver : objectResolvers) {\n+                try {\n+                    value = objectResolver.convert(ctx, resolver.elementType(), beanFactoryId);\n+                    break;\n+                } catch (FallthroughException ignore) {\n+                    // Do nothing.\n+                } catch (Throwable cause) {\n+                    Exceptions.throwUnsafely(cause);\n+                }\n+            }\n+            if (value != null) {\n+                return value;\n+            }\n+            throw new IllegalArgumentException(\"No suitable request converter found for a @\" +\n+                                               RequestObject.class.getSimpleName() + \" '\" +\n+                                               resolver.elementType().getSimpleName() + '\\'');\n+        };\n+    }\n+\n+    @Nullable\n+    private final Annotation annotation;\n+\n+    @Nullable\n+    private final String httpElementName;\n+\n+    private final boolean isPathVariable;\n+    private final boolean shouldExist;\n+    private final boolean shouldWrapValueAsOptional;\n+\n+    @Nullable\n+    private final Class<?> containerType;\n+    private final Class<?> elementType;\n+\n+    @Nullable\n+    private final Object defaultValue;\n+\n+    private final BiFunction<AnnotatedValueResolver, ResolverContext, Object> resolver;\n+\n+    @Nullable\n+    private final EnumConverter<?> enumConverter;\n+\n+    private final AggregationStrategy aggregationStrategy;\n+\n+    private static final ConcurrentMap<Class<?>, EnumConverter<?>> enumConverters = new MapMaker().makeMap();\n+\n+    private AnnotatedValueResolver(@Nullable Annotation annotation, @Nullable String httpElementName,\n+                                   boolean isPathVariable, boolean shouldExist,\n+                                   boolean shouldWrapValueAsOptional,\n+                                   @Nullable Class<?> containerType, Class<?> elementType,\n+                                   @Nullable String defaultValue,\n+                                   BiFunction<AnnotatedValueResolver, ResolverContext, Object> resolver,\n+                                   AggregationStrategy aggregationStrategy) {\n+        this.annotation = annotation;\n+        this.httpElementName = httpElementName;\n+        this.isPathVariable = isPathVariable;\n+        this.shouldExist = shouldExist;\n+        this.shouldWrapValueAsOptional = shouldWrapValueAsOptional;\n+        this.elementType = requireNonNull(elementType, \"elementType\");\n+        this.containerType = containerType;\n+        this.resolver = requireNonNull(resolver, \"resolver\");\n+        this.aggregationStrategy = requireNonNull(aggregationStrategy, \"aggregationStrategy\");\n+        enumConverter = enumConverter(elementType);\n+\n+        // Must be called after initializing 'enumConverter'.\n+        this.defaultValue = defaultValue != null ? convert(defaultValue, elementType, enumConverter)\n+                                                 : null;\n+    }\n+\n+    @Nullable\n+    private static EnumConverter<?> enumConverter(Class<?> elementType) {\n+        if (!elementType.isEnum()) {\n+            return null;\n+        }\n+        return enumConverters.computeIfAbsent(elementType, newElementType -> {\n+            logger.info(\"Register Enum {}\", newElementType);\n+            return new EnumConverter<>(newElementType.asSubclass(Enum.class));\n+        });\n+    }\n+\n+    @VisibleForTesting\n+    @Nullable\n+    Annotation annotation() {\n+        return annotation;\n+    }\n+\n+    boolean isAnnotationType(Class<? extends Annotation> target) {\n+        return annotation != null &&\n+               annotation.annotationType() == target;\n+    }\n+\n+    @Nullable\n+    String httpElementName() {\n+        // Currently, this is non-null only if the element one of the HTTP path variable, parameter or header.\n+        return httpElementName;\n+    }\n+\n+    boolean isPathVariable() {\n+        return isPathVariable;\n+    }\n+\n+    @VisibleForTesting\n+    public boolean shouldExist() {\n+        return shouldExist;\n+    }\n+\n+    @VisibleForTesting\n+    @Nullable\n+    Class<?> containerType() {\n+        // 'List' or 'Set'\n+        return containerType;\n+    }\n+\n+    @VisibleForTesting\n+    Class<?> elementType() {\n+        return elementType;\n+    }\n+\n+    @VisibleForTesting\n+    @Nullable\n+    Object defaultValue() {\n+        return defaultValue;\n+    }\n+\n+    AggregationStrategy aggregationStrategy() {\n+        return aggregationStrategy;\n+    }\n+\n+    boolean hasContainer() {\n+        return containerType != null &&\n+               (List.class.isAssignableFrom(containerType) || Set.class.isAssignableFrom(containerType));\n+    }\n+\n+    Object resolve(ResolverContext ctx) {\n+        final Object resolved = resolver.apply(this, ctx);\n+        return shouldWrapValueAsOptional ? Optional.ofNullable(resolved)\n+                                         : resolved;\n+    }\n+\n+    private static Object convert(String value, Class<?> elementType,\n+                                  @Nullable EnumConverter<?> enumConverter) {\n+        return enumConverter != null ? enumConverter.toEnum(value)\n+                                     : stringToType(value, elementType);\n+    }\n+\n+    @Nullable\n+    private Object convert(@Nullable String value) {\n+        if (value == null) {\n+            return defaultOrException();\n+        }\n+        return convert(value, elementType, enumConverter);\n+    }\n+\n+    @Nullable\n+    private Object defaultOrException() {\n+        if (!shouldExist) {\n+            // May return 'null' if no default value is specified.\n+            return defaultValue;\n+        }\n+        throw new IllegalArgumentException(\"Mandatory parameter is missing: \" + httpElementName);\n+    }\n+\n+    @Override\n+    public String toString() {\n+        return MoreObjects.toStringHelper(this)\n+                          .add(\"annotation\",\n+                               annotation != null ? annotation.annotationType().getSimpleName() : \"(none)\")\n+                          .add(\"httpElementName\", httpElementName)\n+                          .add(\"pathVariable\", isPathVariable)\n+                          .add(\"shouldExist\", shouldExist)\n+                          .add(\"shouldWrapValueAsOptional\", shouldWrapValueAsOptional)\n+                          .add(\"elementType\", elementType.getSimpleName())\n+                          .add(\"containerType\",\n+                               containerType != null ? containerType.getSimpleName() : \"(none)\")\n+                          .add(\"defaultValue\", defaultValue)\n+                          .add(\"defaultValueType\",\n+                               defaultValue != null ? defaultValue.getClass().getSimpleName() : \"(none)\")\n+                          .add(\"resolver\", resolver)\n+                          .add(\"enumConverter\", enumConverter)\n+                          .toString();\n+    }\n+\n+    private static Builder builder(AnnotatedElement annotatedElement, Type type) {\n+        return new Builder(annotatedElement, type);\n+    }\n+\n+    private static final class Builder {\n+        private final AnnotatedElement annotatedElement;\n+        private final Type type;\n+        private AnnotatedElement typeElement;\n+        @Nullable\n+        private Annotation annotation;\n+        @Nullable\n+        private String httpElementName;\n+        private boolean pathVariable;\n+        private boolean supportContainer;\n+        private boolean supportOptional;\n+        private boolean supportDefault;\n+        @Nullable\n+        private BiFunction<AnnotatedValueResolver, ResolverContext, Object> resolver;\n+        private AggregationStrategy aggregation = AggregationStrategy.NONE;\n+\n+        private Builder(AnnotatedElement annotatedElement, Type type) {\n+            this.annotatedElement = requireNonNull(annotatedElement, \"annotatedElement\");\n+            this.type = requireNonNull(type, \"type\");\n+            typeElement = annotatedElement;\n+        }\n+\n+        /**\n+         * Sets the annotation which is one of {@link Param}, {@link Header} or {@link RequestObject}.\n+         */\n+        private Builder annotation(Annotation annotation) {\n+            this.annotation = annotation;\n+            return this;\n+        }\n+\n+        /**\n+         * Sets a name of the element.\n+         */\n+        private Builder httpElementName(String httpElementName) {\n+            this.httpElementName = httpElementName;\n+            return this;\n+        }\n+\n+        /**\n+         * Sets whether this element is a path variable.\n+         */\n+        private Builder pathVariable(boolean pathVariable) {\n+            this.pathVariable = pathVariable;\n+            return this;\n+        }\n+\n+        /**\n+         * Sets whether the value type can be a {@link List} or {@link Set}.\n+         */\n+        private Builder supportContainer(boolean supportContainer) {\n+            this.supportContainer = supportContainer;\n+            return this;\n+        }\n+\n+        /**\n+         * Sets whether the value type can be wrapped by {@link Optional}.\n+         */\n+        private Builder supportOptional(boolean supportOptional) {\n+            this.supportOptional = supportOptional;\n+            return this;\n+        }\n+\n+        /**\n+         * Sets whether the element can be annotated with {@link Default} annotation.\n+         */\n+        private Builder supportDefault(boolean supportDefault) {\n+            this.supportDefault = supportDefault;\n+            return this;\n+        }\n+\n+        /**\n+         * Sets an {@link AnnotatedElement} which is to infer its type.","path":"core/src/main/java/com/linecorp/armeria/server/AnnotatedValueResolver.java","position":704,"original_position":704,"commit_id":"1b1f29eb40aa0f6ebfddea873543ac210b8c6481","original_commit_id":"1b1f29eb40aa0f6ebfddea873543ac210b8c6481","user":{"login":"minwoox","id":25103250,"avatar_url":"https://avatars2.githubusercontent.com/u/25103250?v=4","gravatar_id":"","url":"https://api.github.com/users/minwoox","html_url":"https://github.com/minwoox","followers_url":"https://api.github.com/users/minwoox/followers","following_url":"https://api.github.com/users/minwoox/following{/other_user}","gists_url":"https://api.github.com/users/minwoox/gists{/gist_id}","starred_url":"https://api.github.com/users/minwoox/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/minwoox/subscriptions","organizations_url":"https://api.github.com/users/minwoox/orgs","repos_url":"https://api.github.com/users/minwoox/repos","events_url":"https://api.github.com/users/minwoox/events{/privacy}","received_events_url":"https://api.github.com/users/minwoox/received_events","type":"User","site_admin":false},"body":"is used?","created_at":"2018-05-23T06:15:39Z","updated_at":"2018-05-23T06:31:56Z","html_url":"https://github.com/line/armeria/pull/1191#discussion_r190132611","pull_request_url":"https://api.github.com/repos/line/armeria/pulls/1191","author_association":"MEMBER","_links":{"self":{"href":"https://api.github.com/repos/line/armeria/pulls/comments/190132611"},"html":{"href":"https://github.com/line/armeria/pull/1191#discussion_r190132611"},"pull_request":{"href":"https://api.github.com/repos/line/armeria/pulls/1191"}}},"pull_request":{"url":"https://api.github.com/repos/line/armeria/pulls/1191","id":187160270,"html_url":"https://github.com/line/armeria/pull/1191","diff_url":"https://github.com/line/armeria/pull/1191.diff","patch_url":"https://github.com/line/armeria/pull/1191.patch","issue_url":"https://api.github.com/repos/line/armeria/issues/1191","number":1191,"state":"open","locked":false,"title":"Refactor annotated HTTP service","user":{"login":"hyangtack","id":8417787,"avatar_url":"https://avatars1.githubusercontent.com/u/8417787?v=4","gravatar_id":"","url":"https://api.github.com/users/hyangtack","html_url":"https://github.com/hyangtack","followers_url":"https://api.github.com/users/hyangtack/followers","following_url":"https://api.github.com/users/hyangtack/following{/other_user}","gists_url":"https://api.github.com/users/hyangtack/gists{/gist_id}","starred_url":"https://api.github.com/users/hyangtack/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/hyangtack/subscriptions","organizations_url":"https://api.github.com/users/hyangtack/orgs","repos_url":"https://api.github.com/users/hyangtack/repos","events_url":"https://api.github.com/users/hyangtack/events{/privacy}","received_events_url":"https://api.github.com/users/hyangtack/received_events","type":"User","site_admin":false},"body":"Motivation:\r\nThe first goal is to extract a common code from AnnotatedHttpServiceMethod and BeanRequestConverterFunction to support `@Param`, `@Header` and `@Default` annotations in a same way.\r\nNext, converting a bean in a recursive way would be useful. e.g. a bean inside another bean.\r\n\r\nModifications:\r\n- Add `AnnotatedValueResolver` in order to resolve values in a same way between annotated service method and bean request converter.\r\n- Change `AnnotatedHttpServices` class to `AnnotatedHttpServiceFactory` and refactor it.\r\n- Add `AnnotatedBeanFactory` which replaces `BeanRequestConverterFunction`.\r\n- Now `@Default` and `@RequestObject` can be also put on a field, method and constructor.\r\n- Now `List<?>`, `Set<?>` and `Optional<List<?>>` can be specified with `@Param` and `@Header`.\r\n  - `@Param` is only allowed to HTTP parameters. A path variable cannot be specified as a collection.\r\n  - The element type of a collection is not limited to `String` anymore. The elements would be automatically converted to the desired type.\r\n    e.g. `List<Integer>`, `Set<UserEnumType>`, etc.\r\n\r\nResult:\r\n- Closes #1175","created_at":"2018-05-10T10:47:33Z","updated_at":"2018-05-23T06:31:56Z","closed_at":null,"merged_at":null,"merge_commit_sha":"13b4d93538c28585a0181008107e5d3d70ffa2e2","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[{"id":287373881,"url":"https://api.github.com/repos/line/armeria/labels/new-feature","name":"new-feature","color":"009800","default":false}],"milestone":{"url":"https://api.github.com/repos/line/armeria/milestones/103","html_url":"https://github.com/line/armeria/milestone/103","labels_url":"https://api.github.com/repos/line/armeria/milestones/103/labels","id":3359895,"number":103,"title":"0.65.0","description":null,"creator":{"login":"trustin","id":173918,"avatar_url":"https://avatars0.githubusercontent.com/u/173918?v=4","gravatar_id":"","url":"https://api.github.com/users/trustin","html_url":"https://github.com/trustin","followers_url":"https://api.github.com/users/trustin/followers","following_url":"https://api.github.com/users/trustin/following{/other_user}","gists_url":"https://api.github.com/users/trustin/gists{/gist_id}","starred_url":"https://api.github.com/users/trustin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/trustin/subscriptions","organizations_url":"https://api.github.com/users/trustin/orgs","repos_url":"https://api.github.com/users/trustin/repos","events_url":"https://api.github.com/users/trustin/events{/privacy}","received_events_url":"https://api.github.com/users/trustin/received_events","type":"User","site_admin":false},"open_issues":9,"closed_issues":1,"state":"open","created_at":"2018-05-21T01:00:38Z","updated_at":"2018-05-23T04:49:36Z","due_on":null,"closed_at":null},"commits_url":"https://api.github.com/repos/line/armeria/pulls/1191/commits","review_comments_url":"https://api.github.com/repos/line/armeria/pulls/1191/comments","review_comment_url":"https://api.github.com/repos/line/armeria/pulls/comments{/number}","comments_url":"https://api.github.com/repos/line/armeria/issues/1191/comments","statuses_url":"https://api.github.com/repos/line/armeria/statuses/1b1f29eb40aa0f6ebfddea873543ac210b8c6481","head":{"label":"hyangtack:feature/refactor_annotated_service2","ref":"feature/refactor_annotated_service2","sha":"1b1f29eb40aa0f6ebfddea873543ac210b8c6481","user":{"login":"hyangtack","id":8417787,"avatar_url":"https://avatars1.githubusercontent.com/u/8417787?v=4","gravatar_id":"","url":"https://api.github.com/users/hyangtack","html_url":"https://github.com/hyangtack","followers_url":"https://api.github.com/users/hyangtack/followers","following_url":"https://api.github.com/users/hyangtack/following{/other_user}","gists_url":"https://api.github.com/users/hyangtack/gists{/gist_id}","starred_url":"https://api.github.com/users/hyangtack/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/hyangtack/subscriptions","organizations_url":"https://api.github.com/users/hyangtack/orgs","repos_url":"https://api.github.com/users/hyangtack/repos","events_url":"https://api.github.com/users/hyangtack/events{/privacy}","received_events_url":"https://api.github.com/users/hyangtack/received_events","type":"User","site_admin":false},"repo":{"id":48430251,"name":"armeria","full_name":"hyangtack/armeria","owner":{"login":"hyangtack","id":8417787,"avatar_url":"https://avatars1.githubusercontent.com/u/8417787?v=4","gravatar_id":"","url":"https://api.github.com/users/hyangtack","html_url":"https://github.com/hyangtack","followers_url":"https://api.github.com/users/hyangtack/followers","following_url":"https://api.github.com/users/hyangtack/following{/other_user}","gists_url":"https://api.github.com/users/hyangtack/gists{/gist_id}","starred_url":"https://api.github.com/users/hyangtack/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/hyangtack/subscriptions","organizations_url":"https://api.github.com/users/hyangtack/orgs","repos_url":"https://api.github.com/users/hyangtack/repos","events_url":"https://api.github.com/users/hyangtack/events{/privacy}","received_events_url":"https://api.github.com/users/hyangtack/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/hyangtack/armeria","description":"Asynchronous RPC/API client/server library built on top of Java 8, Netty 4.1, HTTP/2, and Thrift","fork":true,"url":"https://api.github.com/repos/hyangtack/armeria","forks_url":"https://api.github.com/repos/hyangtack/armeria/forks","keys_url":"https://api.github.com/repos/hyangtack/armeria/keys{/key_id}","collaborators_url":"https://api.github.com/repos/hyangtack/armeria/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/hyangtack/armeria/teams","hooks_url":"https://api.github.com/repos/hyangtack/armeria/hooks","issue_events_url":"https://api.github.com/repos/hyangtack/armeria/issues/events{/number}","events_url":"https://api.github.com/repos/hyangtack/armeria/events","assignees_url":"https://api.github.com/repos/hyangtack/armeria/assignees{/user}","branches_url":"https://api.github.com/repos/hyangtack/armeria/branches{/branch}","tags_url":"https://api.github.com/repos/hyangtack/armeria/tags","blobs_url":"https://api.github.com/repos/hyangtack/armeria/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/hyangtack/armeria/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/hyangtack/armeria/git/refs{/sha}","trees_url":"https://api.github.com/repos/hyangtack/armeria/git/trees{/sha}","statuses_url":"https://api.github.com/repos/hyangtack/armeria/statuses/{sha}","languages_url":"https://api.github.com/repos/hyangtack/armeria/languages","stargazers_url":"https://api.github.com/repos/hyangtack/armeria/stargazers","contributors_url":"https://api.github.com/repos/hyangtack/armeria/contributors","subscribers_url":"https://api.github.com/repos/hyangtack/armeria/subscribers","subscription_url":"https://api.github.com/repos/hyangtack/armeria/subscription","commits_url":"https://api.github.com/repos/hyangtack/armeria/commits{/sha}","git_commits_url":"https://api.github.com/repos/hyangtack/armeria/git/commits{/sha}","comments_url":"https://api.github.com/repos/hyangtack/armeria/comments{/number}","issue_comment_url":"https://api.github.com/repos/hyangtack/armeria/issues/comments{/number}","contents_url":"https://api.github.com/repos/hyangtack/armeria/contents/{+path}","compare_url":"https://api.github.com/repos/hyangtack/armeria/compare/{base}...{head}","merges_url":"https://api.github.com/repos/hyangtack/armeria/merges","archive_url":"https://api.github.com/repos/hyangtack/armeria/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/hyangtack/armeria/downloads","issues_url":"https://api.github.com/repos/hyangtack/armeria/issues{/number}","pulls_url":"https://api.github.com/repos/hyangtack/armeria/pulls{/number}","milestones_url":"https://api.github.com/repos/hyangtack/armeria/milestones{/number}","notifications_url":"https://api.github.com/repos/hyangtack/armeria/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/hyangtack/armeria/labels{/name}","releases_url":"https://api.github.com/repos/hyangtack/armeria/releases{/id}","deployments_url":"https://api.github.com/repos/hyangtack/armeria/deployments","created_at":"2015-12-22T12:16:46Z","updated_at":"2016-02-18T10:05:21Z","pushed_at":"2018-05-23T05:45:11Z","git_url":"git://github.com/hyangtack/armeria.git","ssh_url":"git@github.com:hyangtack/armeria.git","clone_url":"https://github.com/hyangtack/armeria.git","svn_url":"https://github.com/hyangtack/armeria","homepage":"http://line.github.io/armeria/","size":51489,"stargazers_count":0,"watchers_count":0,"language":"Java","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":0,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0"},"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"line:master","ref":"master","sha":"11203fe4a0ddd18af6d7322f7a6fd202aded3870","user":{"login":"line","id":13128444,"avatar_url":"https://avatars2.githubusercontent.com/u/13128444?v=4","gravatar_id":"","url":"https://api.github.com/users/line","html_url":"https://github.com/line","followers_url":"https://api.github.com/users/line/followers","following_url":"https://api.github.com/users/line/following{/other_user}","gists_url":"https://api.github.com/users/line/gists{/gist_id}","starred_url":"https://api.github.com/users/line/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/line/subscriptions","organizations_url":"https://api.github.com/users/line/orgs","repos_url":"https://api.github.com/users/line/repos","events_url":"https://api.github.com/users/line/events{/privacy}","received_events_url":"https://api.github.com/users/line/received_events","type":"Organization","site_admin":false},"repo":{"id":46251204,"name":"armeria","full_name":"line/armeria","owner":{"login":"line","id":13128444,"avatar_url":"https://avatars2.githubusercontent.com/u/13128444?v=4","gravatar_id":"","url":"https://api.github.com/users/line","html_url":"https://github.com/line","followers_url":"https://api.github.com/users/line/followers","following_url":"https://api.github.com/users/line/following{/other_user}","gists_url":"https://api.github.com/users/line/gists{/gist_id}","starred_url":"https://api.github.com/users/line/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/line/subscriptions","organizations_url":"https://api.github.com/users/line/orgs","repos_url":"https://api.github.com/users/line/repos","events_url":"https://api.github.com/users/line/events{/privacy}","received_events_url":"https://api.github.com/users/line/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/line/armeria","description":"Asynchronous RPC/REST library built on top of Java 8, Netty, HTTP/2, Thrift and gRPC","fork":false,"url":"https://api.github.com/repos/line/armeria","forks_url":"https://api.github.com/repos/line/armeria/forks","keys_url":"https://api.github.com/repos/line/armeria/keys{/key_id}","collaborators_url":"https://api.github.com/repos/line/armeria/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/line/armeria/teams","hooks_url":"https://api.github.com/repos/line/armeria/hooks","issue_events_url":"https://api.github.com/repos/line/armeria/issues/events{/number}","events_url":"https://api.github.com/repos/line/armeria/events","assignees_url":"https://api.github.com/repos/line/armeria/assignees{/user}","branches_url":"https://api.github.com/repos/line/armeria/branches{/branch}","tags_url":"https://api.github.com/repos/line/armeria/tags","blobs_url":"https://api.github.com/repos/line/armeria/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/line/armeria/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/line/armeria/git/refs{/sha}","trees_url":"https://api.github.com/repos/line/armeria/git/trees{/sha}","statuses_url":"https://api.github.com/repos/line/armeria/statuses/{sha}","languages_url":"https://api.github.com/repos/line/armeria/languages","stargazers_url":"https://api.github.com/repos/line/armeria/stargazers","contributors_url":"https://api.github.com/repos/line/armeria/contributors","subscribers_url":"https://api.github.com/repos/line/armeria/subscribers","subscription_url":"https://api.github.com/repos/line/armeria/subscription","commits_url":"https://api.github.com/repos/line/armeria/commits{/sha}","git_commits_url":"https://api.github.com/repos/line/armeria/git/commits{/sha}","comments_url":"https://api.github.com/repos/line/armeria/comments{/number}","issue_comment_url":"https://api.github.com/repos/line/armeria/issues/comments{/number}","contents_url":"https://api.github.com/repos/line/armeria/contents/{+path}","compare_url":"https://api.github.com/repos/line/armeria/compare/{base}...{head}","merges_url":"https://api.github.com/repos/line/armeria/merges","archive_url":"https://api.github.com/repos/line/armeria/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/line/armeria/downloads","issues_url":"https://api.github.com/repos/line/armeria/issues{/number}","pulls_url":"https://api.github.com/repos/line/armeria/pulls{/number}","milestones_url":"https://api.github.com/repos/line/armeria/milestones{/number}","notifications_url":"https://api.github.com/repos/line/armeria/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/line/armeria/labels{/name}","releases_url":"https://api.github.com/repos/line/armeria/releases{/id}","deployments_url":"https://api.github.com/repos/line/armeria/deployments","created_at":"2015-11-16T04:04:01Z","updated_at":"2018-05-23T02:54:18Z","pushed_at":"2018-05-23T05:58:26Z","git_url":"git://github.com/line/armeria.git","ssh_url":"git@github.com:line/armeria.git","clone_url":"https://github.com/line/armeria.git","svn_url":"https://github.com/line/armeria","homepage":"https://line.github.io/armeria/","size":56303,"stargazers_count":1244,"watchers_count":1244,"language":"Java","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":true,"forks_count":284,"mirror_url":null,"archived":false,"open_issues_count":112,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0"},"forks":284,"open_issues":112,"watchers":1244,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/line/armeria/pulls/1191"},"html":{"href":"https://github.com/line/armeria/pull/1191"},"issue":{"href":"https://api.github.com/repos/line/armeria/issues/1191"},"comments":{"href":"https://api.github.com/repos/line/armeria/issues/1191/comments"},"review_comments":{"href":"https://api.github.com/repos/line/armeria/pulls/1191/comments"},"review_comment":{"href":"https://api.github.com/repos/line/armeria/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/line/armeria/pulls/1191/commits"},"statuses":{"href":"https://api.github.com/repos/line/armeria/statuses/1b1f29eb40aa0f6ebfddea873543ac210b8c6481"}},"author_association":"CONTRIBUTOR"}}
{"actor":{"display_login":"minwoox"}}