AnnotatedServiceFactory { @VisibleForTesting static List<AnnotatedServiceElement> create(String pathPrefix, Object object, Method method, boolean useBlockingTaskExecutor, List<RequestConverterFunction> baseRequestConverters, List<ResponseConverterFunction> baseResponseConverters, List<ExceptionHandlerFunction> baseExceptionHandlers) { if (KotlinUtil.getCallKotlinSuspendingMethod() == null && KotlinUtil.maybeSuspendingFunction(method)) { throw new IllegalArgumentException( "Kotlin suspending functions are supported " + "only when you added 'armeria-kotlin' as a dependency.\n" + "See https: "for more information."); } final Set<Annotation> methodAnnotations = httpMethodAnnotations(method); if (methodAnnotations.isEmpty()) { throw new IllegalArgumentException("HTTP Method specification is missing: " + method.getName()); } final Class<?> clazz = object.getClass(); final Map<HttpMethod, List<String>> httpMethodPatternsMap = getHttpMethodPatternsMap(method, methodAnnotations); final String computedPathPrefix = computePathPrefix(clazz, pathPrefix); final Set<MediaType> consumableMediaTypes = consumableMediaTypes(method, clazz); final Set<MediaType> producibleMediaTypes = producibleMediaTypes(method, clazz); final List<Route> routes = httpMethodPatternsMap.entrySet().stream().flatMap( pattern -> { final HttpMethod httpMethod = pattern.getKey(); final List<String> pathMappings = pattern.getValue(); return pathMappings.stream().map( pathMapping -> Route.builder() .path(computedPathPrefix, pathMapping) .methods(httpMethod) .consumes(consumableMediaTypes) .produces(producibleMediaTypes) .matchesParams( predicates(method, clazz, MatchesParam.class, MatchesParam::value)) .matchesHeaders( predicates(method, clazz, MatchesHeader.class, MatchesHeader::value)) .build()); }).collect(toImmutableList()); final List<RequestConverterFunction> req = getAnnotatedInstances(method, clazz, RequestConverter.class, RequestConverterFunction.class) .addAll(baseRequestConverters).build(); final List<ResponseConverterFunction> res = getAnnotatedInstances(method, clazz, ResponseConverter.class, ResponseConverterFunction.class) .addAll(baseResponseConverters).build(); final List<ExceptionHandlerFunction> eh = getAnnotatedInstances(method, clazz, ExceptionHandler.class, ExceptionHandlerFunction.class) .addAll(baseExceptionHandlers).add(defaultExceptionHandler).build(); final ResponseHeadersBuilder defaultHeaders = ResponseHeaders.builder(defaultResponseStatus(method)); final HttpHeadersBuilder defaultTrailers = HttpHeaders.builder(); final String classAlias = clazz.getName(); final String methodAlias = String.format("%s.%s()", classAlias, method.getName()); setAdditionalHeader(defaultHeaders, clazz, "header", classAlias, "class", AdditionalHeader.class, AdditionalHeader::name, AdditionalHeader::value); setAdditionalHeader(defaultHeaders, method, "header", methodAlias, "method", AdditionalHeader.class, AdditionalHeader::name, AdditionalHeader::value); setAdditionalHeader(defaultTrailers, clazz, "trailer", classAlias, "class", AdditionalTrailer.class, AdditionalTrailer::name, AdditionalTrailer::value); setAdditionalHeader(defaultTrailers, method, "trailer", methodAlias, "method", AdditionalTrailer.class, AdditionalTrailer::name, AdditionalTrailer::value); if (defaultHeaders.status().isContentAlwaysEmpty() && !defaultTrailers.isEmpty()) { logger.warn("A response with HTTP status code '{}' cannot have a content. " + "Trailers defined at '{}' might be ignored if HTTP/1.1 is used.", defaultHeaders.status().code(), methodAlias); } final ResponseHeaders responseHeaders = defaultHeaders.build(); final HttpHeaders responseTrailers = defaultTrailers.build(); final boolean needToUseBlockingTaskExecutor = useBlockingTaskExecutor || AnnotationUtil.findFirst(method, Blocking.class) != null || AnnotationUtil.findFirst(object.getClass(), Blocking.class) != null; return routes.stream().map(route -> { final List<AnnotatedValueResolver> resolvers = getAnnotatedValueResolvers(req, route, method, clazz); return new AnnotatedServiceElement( route, new AnnotatedService(object, method, resolvers, eh, res, route, responseHeaders, responseTrailers, needToUseBlockingTaskExecutor), decorator(method, clazz)); }).collect(toImmutableList()); } private  AnnotatedServiceFactory(); static List<AnnotatedServiceElement> find(
            String pathPrefix, Object object, boolean useBlockingTaskExecutor,
            List<RequestConverterFunction> requestConverterFunctions,
            List<ResponseConverterFunction> responseConverterFunctions,
            List<ExceptionHandlerFunction> exceptionHandlerFunctions);  }
@Test void testCreateAnnotatedServiceElementWithoutExplicitPathOnMethod() { final ServiceObjectWithoutPathOnAnnotatedMethod serviceObject = new ServiceObjectWithoutPathOnAnnotatedMethod(); final List<Route> routes = Stream.of(HttpMethod.GET, HttpMethod.DELETE, HttpMethod.HEAD, HttpMethod.PUT, HttpMethod.OPTIONS, HttpMethod.PATCH, HttpMethod.POST, HttpMethod.TRACE) .map(m -> Route.builder().path("/").methods(m).build()) .collect(toImmutableList()); final List<Route> actualRoutes = getMethods(ServiceObjectWithoutPathOnAnnotatedMethod.class, HttpResponse.class) .map(method -> create("/", serviceObject, method, false, ImmutableList.of(), ImmutableList.of(), ImmutableList.of())) .flatMap(Collection::stream) .map(AnnotatedServiceElement::route) .collect(toImmutableList()); assertThat(actualRoutes).containsAll(routes); }
@Test void testMultiPathFailingService() { final MultiPathFailingService serviceObject = new MultiPathFailingService(); getMethods(MultiPathFailingService.class, HttpResponse.class).forEach(method -> { assertThatThrownBy(() -> { create("/", serviceObject, method, false, ImmutableList.of(), ImmutableList.of(), ImmutableList.of()); }, method.getName()).isInstanceOf(IllegalArgumentException.class); }); }