/* * Copyright 2016 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: * * http://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.http.dynamic; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.Sets.toImmutableEnumSet; import java.lang.reflect.Method; import java.util.Arrays; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Sets; import com.linecorp.armeria.common.http.HttpMethod; import com.linecorp.armeria.common.http.HttpRequest; import com.linecorp.armeria.common.http.PathParamExtractor; import com.linecorp.armeria.server.ServiceRequestContext; /** * Provides various utility functions for {@link Method} object, related to {@link DynamicHttpService}. */ final class Methods { /** * Mapping from HTTP method annotation to {@link HttpMethod}, like following. * <ul> * <li>{@link Options} -> {@link HttpMethod#OPTIONS} * <li>{@link Get} -> {@link HttpMethod#GET} * <li>{@link Head} -> {@link HttpMethod#HEAD} * <li>{@link Post} -> {@link HttpMethod#POST} * <li>{@link Put} -> {@link HttpMethod#PUT} * <li>{@link Patch} -> {@link HttpMethod#PATCH} * <li>{@link Delete} -> {@link HttpMethod#DELETE} * <li>{@link Trace} -> {@link HttpMethod#TRACE} * </ul> */ private static final Map<Class<?>, HttpMethod> HTTP_METHOD_MAP = ImmutableMap.<Class<?>, HttpMethod>builder() .put(Options.class, HttpMethod.OPTIONS) .put(Get.class, HttpMethod.GET) .put(Head.class, HttpMethod.HEAD) .put(Post.class, HttpMethod.POST) .put(Put.class, HttpMethod.PUT) .put(Patch.class, HttpMethod.PATCH) .put(Delete.class, HttpMethod.DELETE) .put(Trace.class, HttpMethod.TRACE) .build(); /** * Returns the list of {@link Path} annotated methods. */ private static List<Method> requestMappingMethods(Object object) { return Arrays.stream(object.getClass().getMethods()) .filter(m -> m.getAnnotation(Path.class) != null) .collect(toImmutableList()); } /** * Returns {@link EnumSet} instance of {@link HttpMethod} mapped to {@code method}. If no specific HTTP * Method is mapped to given {@code method}, it is regarded as all HTTP Methods are mapped to on it. * * @see Options * @see Get * @see Head * @see Post * @see Put * @see Patch * @see Delete * @see Trace */ private static Set<HttpMethod> httpMethods(Method method) { return Arrays.stream(method.getAnnotations()) .map(annotation -> HTTP_METHOD_MAP.get(annotation.annotationType())) .filter(Objects::nonNull) .collect(toImmutableEnumSet()); } /** * Returns the {@link PathParamExtractor} instance mapped to {@code method}. */ private static PathParamExtractor pathParamExtractor(Method method) { Path mapping = method.getAnnotation(Path.class); String mappedTo = mapping.value(); return new PathParamExtractor(mappedTo); } /** * Returns the {@link ResponseConverter} instance from {@link Converter} annotation of the given * {@code method}. The {@link Converter} annotation marked on a method can't be repeated and should not * specify the target class. */ private static ResponseConverter converter(Method method) { Converter[] converters = method.getAnnotationsByType(Converter.class); if (converters.length == 0) { return null; } if (converters.length == 1) { Converter converter = converters[0]; if (converter.target() != Object.class) { throw new IllegalArgumentException( "@Converter annotation can't be marked on a method with a target specified."); } try { return converter.value().newInstance(); } catch (Exception e) { throw new IllegalStateException(e); } } throw new IllegalArgumentException("@Converter annotation can't be repeated on a method."); } /** * Returns a mapping from {@link Class} to {@link ResponseConverter} instances from {@link Converter} * annotations of the given {@code clazz}. The {@link Converter} annotation marked on {@code clazz} must * specify the target class, except {@link Object}.class. */ private static Map<Class<?>, ResponseConverter> converters(Class<?> clazz) { Converter[] converters = clazz.getAnnotationsByType(Converter.class); ImmutableMap.Builder<Class<?>, ResponseConverter> builder = ImmutableMap.builder(); for (Converter converter : converters) { Class<?> target = converter.target(); if (target == Object.class) { throw new IllegalArgumentException( "@Converter annotation must have a target type specified."); } try { ResponseConverter instance = converter.value().newInstance(); builder.put(target, instance); } catch (Exception e) { throw new IllegalStateException(e); } } return builder.build(); } /** * Returns the array of {@link ParameterEntry}, which holds the type and {@link PathParam} value. */ static List<ParameterEntry> parameterEntries(Method method) { return Arrays.stream(method.getParameters()) .map(p -> { if (p.getType().isAssignableFrom(ServiceRequestContext.class) || p.getType().isAssignableFrom(HttpRequest.class)) { return new ParameterEntry(p.getType(), null); } else { return new ParameterEntry(p.getType(), p.getAnnotation(PathParam.class).value()); } }) .collect(toImmutableList()); } /** * Returns a {@link DynamicHttpFunctionEntry} instance defined to {@code method} of {@code object} using * {@link Path} annotation. */ private static DynamicHttpFunctionEntry entry(Object object, Method method, Map<Class<?>, ResponseConverter> converters) { Set<HttpMethod> methods = httpMethods(method); if (methods.isEmpty()) { throw new IllegalArgumentException("HTTP Method specification is missing: " + method.getName()); } PathParamExtractor pathParamExtractor = pathParamExtractor(method); DynamicHttpFunctionImpl function = new DynamicHttpFunctionImpl(object, method); Set<String> parameterNames = function.pathParamNames(); Set<String> pathVariableNames = pathParamExtractor.variables(); if (!pathVariableNames.containsAll(parameterNames)) { Set<String> missing = Sets.difference(parameterNames, pathVariableNames); throw new IllegalArgumentException("Missing @PathParam exists: " + missing); } ResponseConverter converter = converter(method); if (converter != null) { return new DynamicHttpFunctionEntry(methods, pathParamExtractor, DynamicHttpFunctions.of(function, converter)); } else { Map<Class<?>, ResponseConverter> converterMap = new HashMap<>(); // Pre-defined converters converterMap.putAll(converters); // Converters given by @Converter annotation converterMap.putAll(converters(method.getDeclaringClass())); return new DynamicHttpFunctionEntry(methods, pathParamExtractor, DynamicHttpFunctions.of(function, converterMap)); } } /** * Returns the list of {@link DynamicHttpFunctionEntry} defined to {@code object} using {@link Path} * annotation. */ static List<DynamicHttpFunctionEntry> entries(Object object, Map<Class<?>, ResponseConverter> converters) { return Methods.requestMappingMethods(object) .stream() .map((Method method) -> entry(object, method, converters)) .collect(toImmutableList()); } private Methods() {} }