| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF 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 org.apache.servicecomb.swagger.generator.core; |
| |
| import com.fasterxml.jackson.databind.BeanDescription; |
| import com.fasterxml.jackson.databind.JavaType; |
| import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; |
| import com.fasterxml.jackson.databind.type.TypeFactory; |
| import com.google.common.reflect.TypeToken; |
| import io.swagger.annotations.ApiImplicitParam; |
| import io.swagger.annotations.ApiImplicitParams; |
| import io.swagger.annotations.ApiParam; |
| import io.swagger.converter.ModelConverters; |
| import io.swagger.models.HttpMethod; |
| import io.swagger.models.Model; |
| import io.swagger.models.ModelImpl; |
| import io.swagger.models.Operation; |
| import io.swagger.models.Path; |
| import io.swagger.models.Response; |
| import io.swagger.models.Swagger; |
| import io.swagger.models.parameters.AbstractSerializableParameter; |
| import io.swagger.models.parameters.BodyParameter; |
| import io.swagger.models.parameters.CookieParameter; |
| import io.swagger.models.parameters.FormParameter; |
| import io.swagger.models.parameters.HeaderParameter; |
| import io.swagger.models.parameters.Parameter; |
| import io.swagger.models.parameters.PathParameter; |
| import io.swagger.models.parameters.QueryParameter; |
| import io.swagger.models.properties.Property; |
| import io.swagger.models.properties.StringProperty; |
| import io.swagger.util.Json; |
| import io.swagger.util.ReflectionUtils; |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.servicecomb.config.inject.PlaceholderResolver; |
| import org.apache.servicecomb.swagger.SwaggerUtils; |
| import org.apache.servicecomb.swagger.generator.MethodAnnotationProcessor; |
| import org.apache.servicecomb.swagger.generator.OperationGenerator; |
| import org.apache.servicecomb.swagger.generator.ParameterGenerator; |
| import org.apache.servicecomb.swagger.generator.ParameterProcessor; |
| import org.apache.servicecomb.swagger.generator.ResponseTypeProcessor; |
| import org.apache.servicecomb.swagger.generator.SwaggerConst; |
| import org.apache.servicecomb.swagger.generator.core.model.HttpParameterType; |
| import org.apache.servicecomb.swagger.generator.core.utils.MethodUtils; |
| |
| import javax.servlet.http.HttpServletResponse; |
| import javax.ws.rs.core.MediaType; |
| import java.lang.annotation.Annotation; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.Method; |
| import java.lang.reflect.Type; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashSet; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| |
| import static org.apache.servicecomb.swagger.generator.SwaggerGeneratorUtils.collectAnnotations; |
| import static org.apache.servicecomb.swagger.generator.SwaggerGeneratorUtils.findMethodAnnotationProcessor; |
| import static org.apache.servicecomb.swagger.generator.SwaggerGeneratorUtils.findParameterProcessors; |
| import static org.apache.servicecomb.swagger.generator.SwaggerGeneratorUtils.findResponseTypeProcessor; |
| import static org.apache.servicecomb.swagger.generator.SwaggerGeneratorUtils.isContextParameter; |
| import static org.apache.servicecomb.swagger.generator.SwaggerGeneratorUtils.postProcessOperation; |
| |
| public abstract class AbstractOperationGenerator implements OperationGenerator { |
| protected AbstractSwaggerGenerator swaggerGenerator; |
| |
| protected Swagger swagger; |
| |
| protected Class<?> clazz; |
| |
| protected Method method; |
| |
| protected String httpMethod; |
| |
| protected List<ParameterGenerator> parameterGenerators = new ArrayList<>(); |
| |
| protected String path; |
| |
| protected Operation swaggerOperation; |
| |
| // 根据方法上独立的ResponseHeader(s)标注生成的数据 |
| // 如果Response中不存在对应的header,则会将这些header补充进去 |
| protected Map<String, Property> methodResponseHeaders = new LinkedHashMap<>(); |
| |
| private static final List<String> NOT_NULL_ANNOTATIONS = Arrays.asList("NotBlank", "NotEmpty"); |
| |
| public AbstractOperationGenerator(AbstractSwaggerGenerator swaggerGenerator, Method method) { |
| this.swaggerGenerator = swaggerGenerator; |
| this.swagger = swaggerGenerator.getSwagger(); |
| this.clazz = swaggerGenerator.getClazz(); |
| this.method = method; |
| this.httpMethod = swaggerGenerator.getHttpMethod(); |
| |
| swaggerOperation = new Operation(); |
| } |
| |
| @Override |
| public void addMethodResponseHeader(String name, Property header) { |
| methodResponseHeaders.put(name, header); |
| } |
| |
| @Override |
| public void setHttpMethod(String httpMethod) { |
| if (StringUtils.isEmpty(httpMethod)) { |
| return; |
| } |
| |
| this.httpMethod = httpMethod.toLowerCase(Locale.US); |
| } |
| |
| @Override |
| public Operation getOperation() { |
| return swaggerOperation; |
| } |
| |
| public String getOperationId() { |
| return swaggerOperation.getOperationId(); |
| } |
| |
| @Override |
| public void setPath(String path) { |
| path = new PlaceholderResolver().replaceFirst(path); |
| if (!path.startsWith("/")) { |
| path = "/" + path; |
| } |
| this.path = path; |
| } |
| |
| @Override |
| public void generateResponse() { |
| scanMethodAnnotation(); |
| scanResponse(); |
| correctOperation(); |
| } |
| |
| public void generate() { |
| scanMethodAnnotation(); |
| scanMethodParameters(); |
| scanResponse(); |
| correctOperation(); |
| |
| postProcessOperation(swaggerGenerator, this); |
| } |
| |
| protected void scanMethodAnnotation() { |
| for (Annotation annotation : method.getAnnotations()) { |
| MethodAnnotationProcessor<Annotation> processor = findMethodAnnotationProcessor(annotation.annotationType()); |
| if (processor == null) { |
| continue; |
| } |
| processor.process(swaggerGenerator, this, annotation); |
| } |
| |
| if (StringUtils.isEmpty(swaggerOperation.getOperationId())) { |
| swaggerOperation.setOperationId(MethodUtils.findSwaggerMethodName(method)); |
| } |
| |
| setDefaultTag(); |
| } |
| |
| private void setDefaultTag() { |
| // if tag has been defined, do nothing |
| if (null != swaggerOperation.getTags()) { |
| for (String tag : swaggerOperation.getTags()) { |
| if (StringUtils.isNotEmpty(tag)) { |
| return; |
| } |
| } |
| } |
| |
| // if there is no tag, set default tag |
| if (!swaggerGenerator.getDefaultTags().isEmpty()) { |
| swaggerOperation.setTags(new ArrayList<>(swaggerGenerator.getDefaultTags())); |
| } |
| } |
| |
| protected void scanMethodParameters() { |
| initParameterGenerators(); |
| |
| Set<String> names = new HashSet<>(); |
| for (ParameterGenerator parameterGenerator : parameterGenerators) { |
| scanMethodParameter(parameterGenerator); |
| |
| if (!names.add(parameterGenerator.getParameterName())) { |
| throw new IllegalStateException( |
| String.format("not support duplicated parameter, name=%s.", parameterGenerator.getParameterName())); |
| } |
| swaggerOperation.addParameter(parameterGenerator.getGeneratedParameter()); |
| } |
| } |
| |
| private void initParameterGenerators() { |
| // 1.group method annotations by parameter name |
| // key is parameter name |
| Map<String, List<Annotation>> methodAnnotationMap = initMethodAnnotationByParameterName(); |
| |
| // 2.create ParameterGenerators by method parameters, merge annotations with method annotations |
| initMethodParameterGenerators(methodAnnotationMap); |
| |
| // 3.create ParameterGenerators remains method annotations |
| initRemainMethodAnnotationsParameterGenerators(methodAnnotationMap); |
| |
| // 4.check |
| // httpParameterType should not be null |
| long bodyCount = parameterGenerators.stream().filter(p -> p.getHttpParameterType().equals(HttpParameterType.BODY)) |
| .count(); |
| if (bodyCount > 1) { |
| throw new IllegalStateException(String.format("defined %d body parameter.", bodyCount)); |
| } |
| } |
| |
| protected void initMethodParameterGenerators(Map<String, List<Annotation>> methodAnnotationMap) { |
| for (java.lang.reflect.Parameter methodParameter : method.getParameters()) { |
| Type genericType = TypeToken.of(clazz) |
| .resolveType(methodParameter.getParameterizedType()) |
| .getType(); |
| ParameterGenerator parameterGenerator = new ParameterGenerator(method, methodAnnotationMap, methodParameter, |
| genericType); |
| validateParameter(parameterGenerator.getGenericType()); |
| if (isContextParameter(parameterGenerator.getGenericType())) { |
| continue; |
| } |
| |
| // jaxrs: @BeanParam |
| // springmvc: is query, and is bean type |
| if (isAggregatedParameter(parameterGenerator, methodParameter)) { |
| extractAggregatedParameterGenerators(methodAnnotationMap, methodParameter); |
| continue; |
| } |
| |
| parameterGenerators.add(parameterGenerator); |
| } |
| } |
| |
| protected boolean isAggregatedParameter(ParameterGenerator parameterGenerator, |
| java.lang.reflect.Parameter methodParameter) { |
| return false; |
| } |
| |
| protected void extractAggregatedParameterGenerators(Map<String, List<Annotation>> methodAnnotationMap, |
| java.lang.reflect.Parameter methodParameter) { |
| JavaType javaType = TypeFactory.defaultInstance().constructType(methodParameter.getParameterizedType()); |
| BeanDescription beanDescription = Json.mapper().getSerializationConfig().introspect(javaType); |
| for (BeanPropertyDefinition propertyDefinition : beanDescription.findProperties()) { |
| if (!propertyDefinition.couldSerialize()) { |
| continue; |
| } |
| |
| Annotation[] annotations = collectAnnotations(propertyDefinition); |
| ParameterGenerator propertyParameterGenerator = new ParameterGenerator(method, |
| methodAnnotationMap, |
| propertyDefinition.getName(), |
| annotations, |
| propertyDefinition.getPrimaryType()); |
| parameterGenerators.add(propertyParameterGenerator); |
| } |
| } |
| |
| protected void initRemainMethodAnnotationsParameterGenerators(Map<String, List<Annotation>> methodAnnotationMap) { |
| for (Entry<String, List<Annotation>> entry : methodAnnotationMap.entrySet()) { |
| ParameterGenerator parameterGenerator = new ParameterGenerator(entry.getKey(), entry.getValue()); |
| parameterGenerators.add(parameterGenerator); |
| } |
| } |
| |
| private Map<String, List<Annotation>> initMethodAnnotationByParameterName() { |
| Map<String, List<Annotation>> methodAnnotations = new LinkedHashMap<>(); |
| for (Annotation annotation : method.getAnnotations()) { |
| if (annotation instanceof ApiImplicitParams) { |
| for (ApiImplicitParam apiImplicitParam : ((ApiImplicitParams) annotation).value()) { |
| addMethodAnnotationByParameterName(methodAnnotations, apiImplicitParam.name(), apiImplicitParam); |
| } |
| continue; |
| } |
| |
| if (annotation instanceof ApiParam) { |
| addMethodAnnotationByParameterName(methodAnnotations, ((ApiParam) annotation).name(), annotation); |
| } |
| } |
| return methodAnnotations; |
| } |
| |
| private void addMethodAnnotationByParameterName(Map<String, List<Annotation>> methodAnnotations, String name, |
| Annotation annotation) { |
| if (StringUtils.isEmpty(name)) { |
| throw new IllegalStateException(String.format("%s.name should not be empty. method=%s:%s", |
| annotation.annotationType().getSimpleName(), |
| method.getDeclaringClass().getName(), |
| method.getName())); |
| } |
| |
| methodAnnotations.computeIfAbsent(name, n -> new ArrayList<>()) |
| .add(annotation); |
| } |
| |
| protected void validateParameter(JavaType type) { |
| if (type.isTypeOrSubTypeOf(HttpServletResponse.class)) { |
| // not support, log the reason |
| throw new IllegalStateException( |
| "all input/output of ServiceComb operation are models, not allow to use HttpServletResponse."); |
| } |
| } |
| |
| protected void scanMethodParameter(ParameterGenerator parameterGenerator) { |
| Parameter parameter = createParameter(parameterGenerator); |
| |
| try { |
| fillParameter(swagger, |
| parameter, |
| parameterGenerator.getParameterName(), |
| parameterGenerator.getGenericType(), |
| parameterGenerator.getAnnotations()); |
| } catch (Throwable e) { |
| throw new IllegalStateException( |
| String.format("failed to fill parameter, parameterName=%s.", |
| parameterGenerator.getParameterName()), |
| e); |
| } |
| } |
| |
| protected Parameter createParameter(ParameterGenerator parameterGenerator) { |
| if (parameterGenerator.getGeneratedParameter() == null) { |
| Parameter parameter = createParameter(parameterGenerator.getHttpParameterType()); |
| parameterGenerator.setGeneratedParameter(parameter); |
| } |
| parameterGenerator.getGeneratedParameter().setName(parameterGenerator.getParameterName()); |
| return parameterGenerator.getGeneratedParameter(); |
| } |
| |
| protected Parameter createParameter(HttpParameterType httpParameterType) { |
| switch (httpParameterType) { |
| case PATH: |
| return new PathParameter(); |
| case QUERY: |
| return new QueryParameter(); |
| case HEADER: |
| return new HeaderParameter(); |
| case FORM: |
| return new FormParameter(); |
| case COOKIE: |
| return new CookieParameter(); |
| case BODY: |
| return new BodyParameter(); |
| default: |
| throw new IllegalStateException("not support httpParameterType " + httpParameterType); |
| } |
| } |
| |
| protected void fillParameter(Swagger swagger, Parameter parameter, String parameterName, JavaType type, |
| List<Annotation> annotations) { |
| for (Annotation annotation : annotations) { |
| ParameterProcessor<Parameter, Annotation> processor = findParameterProcessors(annotation.annotationType()); |
| if (processor != null) { |
| processor.fillParameter(swagger, swaggerOperation, parameter, type, annotation); |
| } |
| } |
| |
| if (type == null) { |
| return; |
| } |
| |
| ParameterProcessor<Parameter, Annotation> processor = findParameterProcessors(type); |
| if (processor != null) { |
| processor.fillParameter(swagger, swaggerOperation, parameter, type, null); |
| } |
| |
| if (parameter instanceof AbstractSerializableParameter) { |
| io.swagger.util.ParameterProcessor.applyAnnotations(swagger, parameter, type, annotations); |
| annotations.stream().forEach(annotation -> { |
| if (NOT_NULL_ANNOTATIONS.contains(annotation.annotationType().getSimpleName())){ |
| parameter.setRequired(true); |
| } |
| }); |
| return; |
| } |
| |
| fillBodyParameter(swagger, parameter, type, annotations); |
| } |
| |
| protected void fillBodyParameter(Swagger swagger, Parameter parameter, Type type, List<Annotation> annotations) { |
| // so strange, for bodyParameter, swagger return a new instance |
| // that will cause lost some information |
| // so we must merge them |
| BodyParameter newBodyParameter = (BodyParameter) io.swagger.util.ParameterProcessor.applyAnnotations( |
| swagger, parameter, type, annotations); |
| |
| // swagger missed enum data, fix it |
| ModelImpl model = SwaggerUtils.getModelImpl(swagger, newBodyParameter); |
| if (model != null) { |
| Property property = ModelConverters.getInstance().readAsProperty(type); |
| if (property instanceof StringProperty) { |
| model.setEnum(((StringProperty) property).getEnum()); |
| } |
| } |
| |
| // swagger 2.0 do not support NotBlank and NotEmpty annotations, fix it |
| if (((JavaType) type).getBindings().getTypeParameters().isEmpty()) { |
| convertAnnotationProperty(((JavaType) type).getRawClass()); |
| } else { |
| ((JavaType) type).getBindings().getTypeParameters(). |
| forEach(javaType -> convertAnnotationProperty(javaType.getRawClass())); |
| } |
| |
| mergeBodyParameter((BodyParameter) parameter, newBodyParameter); |
| } |
| |
| private void convertAnnotationProperty(Class<?> beanClass) { |
| Map<String, Model> definitions = swagger.getDefinitions(); |
| if (definitions == null){ |
| return; |
| } |
| Field[] fields = beanClass.getDeclaredFields(); |
| Model model = definitions.get(beanClass.getSimpleName()); |
| if (model == null) { |
| return; |
| } |
| Map<String, Property> properties = model.getProperties(); |
| if (properties != null) { |
| Arrays.stream(fields).forEach(field -> { |
| boolean requireItem = Arrays.stream(field.getAnnotations()). |
| anyMatch(annotation -> NOT_NULL_ANNOTATIONS.contains(annotation.annotationType().getSimpleName())); |
| if (requireItem) { |
| Property property = properties.get(field.getName()); |
| if (property != null) { |
| property.setRequired(true); |
| } |
| } |
| }); |
| } |
| } |
| |
| |
| private void mergeBodyParameter(BodyParameter bodyParameter, BodyParameter fromBodyParameter) { |
| if (fromBodyParameter.getExamples() != null) { |
| bodyParameter.setExamples(fromBodyParameter.getExamples()); |
| } |
| if (fromBodyParameter.getRequired()) { |
| bodyParameter.setRequired(true); |
| } |
| if (StringUtils.isNotEmpty(fromBodyParameter.getDescription())) { |
| bodyParameter.setDescription(fromBodyParameter.getDescription()); |
| } |
| if (StringUtils.isNotEmpty(fromBodyParameter.getAccess())) { |
| bodyParameter.setAccess(fromBodyParameter.getAccess()); |
| } |
| if (fromBodyParameter.getSchema() != null) { |
| bodyParameter.setSchema(fromBodyParameter.getSchema()); |
| } |
| } |
| |
| @Override |
| public void addOperationToSwagger() { |
| if (StringUtils.isEmpty(httpMethod)) { |
| return; |
| } |
| |
| Path pathObj = swagger.getPath(path); |
| if (pathObj == null) { |
| pathObj = new Path(); |
| swagger.path(path, pathObj); |
| } |
| |
| HttpMethod hm = HttpMethod.valueOf(httpMethod.toUpperCase(Locale.US)); |
| if (pathObj.getOperationMap().get(hm) != null) { |
| throw new IllegalStateException(String.format("Only allowed one default path. method=%s:%s.", |
| method.getDeclaringClass().getName(), |
| method.getName())); |
| } |
| pathObj.set(httpMethod, swaggerOperation); |
| } |
| |
| public void correctOperation() { |
| if (swaggerOperation.getConsumes() == null) { |
| if (swaggerOperation.getParameters().stream() |
| .anyMatch(SwaggerUtils::isFileParameter)) { |
| swaggerOperation.addConsumes(MediaType.MULTIPART_FORM_DATA); |
| } |
| } |
| |
| SwaggerUtils.correctResponses(swaggerOperation); |
| addHeaderToResponse(); |
| } |
| |
| private void addHeaderToResponse() { |
| for (Entry<String, Response> responseEntry : swaggerOperation.getResponses().entrySet()) { |
| Response response = responseEntry.getValue(); |
| |
| for (Entry<String, Property> entry : methodResponseHeaders.entrySet()) { |
| if (response.getHeaders() != null && response.getHeaders().containsKey(entry.getKey())) { |
| continue; |
| } |
| |
| response.addHeader(entry.getKey(), entry.getValue()); |
| } |
| } |
| } |
| |
| public void scanResponse() { |
| if (swaggerOperation.getResponses() != null) { |
| Response successResponse = swaggerOperation.getResponses().get(SwaggerConst.SUCCESS_KEY); |
| if (successResponse != null) { |
| if (successResponse.getResponseSchema() == null) { |
| // 标注已经定义了response,但是是void,这可能是在标注上未定义 |
| // 根据函数原型来处理response |
| Model model = createResponseModel(); |
| successResponse.setResponseSchema(model); |
| } |
| return; |
| } |
| } |
| |
| Model model = createResponseModel(); |
| Response response = new Response(); |
| response.setResponseSchema(model); |
| swaggerOperation.addResponse(SwaggerConst.SUCCESS_KEY, response); |
| } |
| |
| protected Model createResponseModel() { |
| Type responseType = |
| TypeToken.of(clazz) |
| .resolveType(method.getGenericReturnType()) |
| .getType(); |
| if (ReflectionUtils.isVoid(responseType)) { |
| return null; |
| } |
| |
| ResponseTypeProcessor processor = findResponseTypeProcessor(responseType); |
| return processor.process(swaggerGenerator, this, responseType); |
| } |
| |
| public Method getMethod() { |
| return method; |
| } |
| |
| public List<ParameterGenerator> getParameterGenerators() { |
| return parameterGenerators; |
| } |
| |
| public Operation getSwaggerOperation() { |
| return swaggerOperation; |
| } |
| } |