diff --git a/src/main/java/com/ly/doc/constants/ApiParamEnum.java b/src/main/java/com/ly/doc/constants/ApiParamEnum.java new file mode 100644 index 00000000..fe44e896 --- /dev/null +++ b/src/main/java/com/ly/doc/constants/ApiParamEnum.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2018-2024 smart-doc + * + * 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 com.ly.doc.constants; + +/** + * ParamEnum + * + * @author linwumingshi + * @since 3.0.10 + */ +public enum ApiParamEnum { + + /** + * PathVariable,when param use `@PathVariable` annotation + */ + PATH, + + /** + * RequestParam + */ + QUERY, + + /** + * Body Param(from-data, x-www-form-urlencoded, raw(json)) + */ + BODY, + + ; + +} diff --git a/src/main/java/com/ly/doc/model/ApiMethodDoc.java b/src/main/java/com/ly/doc/model/ApiMethodDoc.java index cff58e70..a60a6c8e 100644 --- a/src/main/java/com/ly/doc/model/ApiMethodDoc.java +++ b/src/main/java/com/ly/doc/model/ApiMethodDoc.java @@ -18,6 +18,7 @@ * specific language governing permissions and limitations * under the License. */ + package com.ly.doc.model; import com.ly.doc.constants.MediaType; @@ -27,7 +28,13 @@ import com.thoughtworks.qdox.model.JavaClass; import java.io.Serializable; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; /** * java api method info model. @@ -340,6 +347,9 @@ public void setOrder(int order) { } public List getRequestParams() { + if (Objects.isNull(this.requestParams)) { + return new ArrayList<>(); + } return requestParams; } @@ -412,6 +422,9 @@ public void setDeprecated(boolean deprecated) { } public List getPathParams() { + if (Objects.isNull(this.pathParams)) { + return new ArrayList<>(); + } return pathParams; } @@ -420,6 +433,9 @@ public void setPathParams(List pathParams) { } public List getQueryParams() { + if (Objects.isNull(this.queryParams)) { + return new ArrayList<>(); + } return queryParams; } diff --git a/src/main/java/com/ly/doc/model/ApiMethodReqParam.java b/src/main/java/com/ly/doc/model/ApiMethodReqParam.java index e6e1a37d..d1b14495 100644 --- a/src/main/java/com/ly/doc/model/ApiMethodReqParam.java +++ b/src/main/java/com/ly/doc/model/ApiMethodReqParam.java @@ -18,14 +18,20 @@ * specific language governing permissions and limitations * under the License. */ + package com.ly.doc.model; +import java.io.Serializable; import java.util.List; /** + * Api request params + * * @author yu 2020/11/26. */ -public class ApiMethodReqParam { +public class ApiMethodReqParam implements Serializable { + + private static final long serialVersionUID = 1140834362473560188L; /** * path params @@ -38,7 +44,7 @@ public class ApiMethodReqParam { private List queryParams; /** - * http request params + * http request params(body param) */ private List requestParams; diff --git a/src/main/java/com/ly/doc/model/ApiParam.java b/src/main/java/com/ly/doc/model/ApiParam.java index 3dcbd24c..f43de290 100644 --- a/src/main/java/com/ly/doc/model/ApiParam.java +++ b/src/main/java/com/ly/doc/model/ApiParam.java @@ -18,22 +18,36 @@ * specific language governing permissions and limitations * under the License. */ + package com.ly.doc.model; import com.ly.doc.model.torna.EnumInfo; import com.ly.doc.model.torna.EnumInfoAndValues; +import com.power.common.util.CollectionUtil; import org.apache.commons.lang3.StringUtils; +import java.io.Serializable; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Stack; +import java.util.function.Consumer; +import java.util.stream.Stream; import static com.ly.doc.constants.DocGlobalConstants.PARAM_PREFIX; /** + * Api Parameter + * * @author yu 2019/9/27. + * @since 1.7.2 */ -public class ApiParam { +public class ApiParam implements Serializable { + + /** + * serialVersionUID + */ + private static final long serialVersionUID = -714676579813604423L; /** * param class name @@ -249,6 +263,11 @@ public ApiParam setQueryParam(boolean queryParam) { return this; } + public ApiParam setQueryParamTrue() { + this.queryParam = true; + return this; + } + public String getValue() { return value; } @@ -351,6 +370,43 @@ public ApiParam setEnumInfoAndValues(EnumInfoAndValues enumInfoAndValues) { return this; } + /** + * Returns a stream containing the current parameter and all its child parameters. + * @return a stream of the current parameter and all its descendants + */ + public Stream flattenStream() { + Stream selfStream = Stream.of(this); + Stream childrenStream = (this.children == null) ? Stream.empty() + : this.children.stream().flatMap(ApiParam::flattenStream); + return Stream.concat(selfStream, childrenStream); + } + + /** + * Traverses this {@link ApiParam} and all its child parameters using a stack-based + * depth-first traversal, applying the given consumer to each parameter. + * @param consumer the operation to be performed on each {@link ApiParam} + */ + public void traverseAndConsume(Consumer consumer) { + // Initialize a stack with the current instance + Stack stack = new Stack<>(); + stack.push(this); + + // Traverse the parameter tree + while (!stack.isEmpty()) { + // Pop the current parameter from the stack + ApiParam current = stack.pop(); + // Apply the provided consumer to the current parameter + consumer.accept(current); + + // If the current parameter has children, push them onto the stack + if (CollectionUtil.isNotEmpty(current.getChildren())) { + for (ApiParam child : current.getChildren()) { + stack.push(child); + } + } + } + } + @Override public String toString() { return "ApiParam{" + "className='" + className + '\'' + ", id=" + id + ", field='" + field + '\'' + ", type='" diff --git a/src/main/java/com/ly/doc/model/DocJavaParameter.java b/src/main/java/com/ly/doc/model/DocJavaParameter.java index be7e481d..f8f6393e 100644 --- a/src/main/java/com/ly/doc/model/DocJavaParameter.java +++ b/src/main/java/com/ly/doc/model/DocJavaParameter.java @@ -18,17 +18,23 @@ * specific language governing permissions and limitations * under the License. */ -package com.ly.doc.model; -import java.util.List; +package com.ly.doc.model; import com.thoughtworks.qdox.model.JavaAnnotation; import com.thoughtworks.qdox.model.JavaParameter; +import java.io.Serializable; +import java.util.List; +import java.util.Objects; + /** + * Doc Java Parameter + * * @author yu3.sun on 2022/10/15 + * @since 2.6.0 */ -public class DocJavaParameter { +public class DocJavaParameter implements Serializable { private JavaParameter javaParameter; @@ -40,7 +46,7 @@ public class DocJavaParameter { private String typeValue; - List annotations; + private List annotations; public JavaParameter getJavaParameter() { return javaParameter; @@ -90,4 +96,39 @@ public void setAnnotations(List annotations) { this.annotations = annotations; } + @Override + public String toString() { + return "DocJavaParameter{" + "javaParameter=" + javaParameter + ", genericCanonicalName='" + + genericCanonicalName + '\'' + ", genericFullyQualifiedName='" + genericFullyQualifiedName + '\'' + + ", fullyQualifiedName='" + fullyQualifiedName + '\'' + ", typeValue='" + typeValue + '\'' + + ", annotations=" + annotations + '}'; + } + + @Override + public int hashCode() { + int result = javaParameter != null ? javaParameter.hashCode() : 0; + result = 31 * result + (genericCanonicalName != null ? genericCanonicalName.hashCode() : 0); + result = 31 * result + (genericFullyQualifiedName != null ? genericFullyQualifiedName.hashCode() : 0); + result = 31 * result + (fullyQualifiedName != null ? fullyQualifiedName.hashCode() : 0); + result = 31 * result + (typeValue != null ? typeValue.hashCode() : 0); + result = 31 * result + (annotations != null ? annotations.hashCode() : 0); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + DocJavaParameter that = (DocJavaParameter) obj; + return (Objects.equals(javaParameter, that.javaParameter)) + && (Objects.equals(genericCanonicalName, that.genericCanonicalName)) + && (Objects.equals(genericFullyQualifiedName, that.genericFullyQualifiedName)) + && (Objects.equals(fullyQualifiedName, that.fullyQualifiedName)) + && (Objects.equals(typeValue, that.typeValue)) && (Objects.equals(annotations, that.annotations)); + } + } diff --git a/src/main/java/com/ly/doc/template/IRestDocTemplate.java b/src/main/java/com/ly/doc/template/IRestDocTemplate.java index beeb8f15..713ab790 100644 --- a/src/main/java/com/ly/doc/template/IRestDocTemplate.java +++ b/src/main/java/com/ly/doc/template/IRestDocTemplate.java @@ -18,9 +18,11 @@ * specific language governing permissions and limitations * under the License. */ + package com.ly.doc.template; import com.ly.doc.builder.ProjectDocConfigBuilder; +import com.ly.doc.constants.ApiParamEnum; import com.ly.doc.constants.ApiReqParamInTypeEnum; import com.ly.doc.constants.DocAnnotationConstants; import com.ly.doc.constants.DocGlobalConstants; @@ -941,10 +943,21 @@ default ApiMethodReqParam requestParams(final DocJavaMethod docJavaMethod, Proje Map mappingParams = new HashMap<>(16); List methodAnnotations = javaMethod.getAnnotations(); Map mappingAnnotationMap = frameworkAnnotations.getMappingAnnotations(); + String methodMediaType = null; for (JavaAnnotation annotation : methodAnnotations) { String annotationName = annotation.getType().getName(); MappingAnnotation mappingAnnotation = mappingAnnotationMap.get(annotationName); - if (Objects.nonNull(mappingAnnotation) && StringUtil.isNotEmpty(mappingAnnotation.getParamsProp())) { + if (Objects.nonNull(mappingAnnotation)) { + if (Objects.nonNull(mappingAnnotation.getConsumesProp())) { + List consumes = JavaClassUtil.getAnnotationValueStrings(builder, annotation, + mappingAnnotation.getConsumesProp()); + if (CollectionUtil.isNotEmpty(consumes)) { + methodMediaType = consumes.get(0); + } + } + if (StringUtil.isEmpty(mappingAnnotation.getParamsProp())) { + continue; + } Object paramsObjects = annotation.getNamedParameter(mappingAnnotation.getParamsProp()); if (Objects.isNull(paramsObjects)) { continue; @@ -986,6 +999,14 @@ default ApiMethodReqParam requestParams(final DocJavaMethod docJavaMethod, Proje } boolean requestFieldToUnderline = builder.getApiConfig().isRequestFieldToUnderline(); int requestBodyCounter = 0; + // requestBodyParam Collection + Set requestBodyParam = parameterList.stream() + .filter(parameter -> parameter.getAnnotations() + .stream() + .anyMatch(annotation -> frameworkAnnotations.getRequestBodyAnnotation() + .getAnnotationName() + .equals(annotation.getType().getValue()))) + .collect(Collectors.toSet()); out: for (DocJavaParameter apiParameter : parameterList) { JavaParameter parameter = apiParameter.getJavaParameter(); String paramName = parameter.getName(); @@ -1006,20 +1027,18 @@ default ApiMethodReqParam requestParams(final DocJavaMethod docJavaMethod, Proje JavaClass javaClass = builder.getJavaProjectBuilder().getClassByName(genericFullyQualifiedName); String mockValue = JavaFieldUtil.createMockValue(paramsComments, paramName, typeName, simpleTypeName); - List annotations = parameter.getAnnotations(); - Set groupClasses = JavaClassUtil.getParamGroupJavaClass(annotations, + List paramAnnotations = parameter.getAnnotations(); + Set groupClasses = JavaClassUtil.getParamGroupJavaClass(paramAnnotations, builder.getJavaProjectBuilder()); String strRequired = "false"; - boolean isPathVariable = false; - boolean isRequestBody = false; boolean required = false; - boolean isRequestParam = false; boolean isRequestPart = false; - if (annotations.isEmpty() && (Methods.GET.getValue().equals(docJavaMethod.getMethodType()) + ApiParamEnum apiParamEnum = null; + if (paramAnnotations.isEmpty() && (Methods.GET.getValue().equals(docJavaMethod.getMethodType()) || Methods.DELETE.getValue().equals(docJavaMethod.getMethodType()))) { - isRequestParam = true; + apiParamEnum = ApiParamEnum.QUERY; } - for (JavaAnnotation annotation : annotations) { + for (JavaAnnotation annotation : paramAnnotations) { String annotationName = annotation.getType().getValue(); if (this.ignoreMvcParamWithAnnotation(annotationName)) { continue out; @@ -1029,28 +1048,36 @@ default ApiMethodReqParam requestParams(final DocJavaMethod docJavaMethod, Proje || frameworkAnnotations.getRequestPartAnnotation().getAnnotationName().equals(annotationName)) { String defaultValueProp = DocAnnotationConstants.DEFAULT_VALUE_PROP; String requiredProp = DocAnnotationConstants.REQUIRED_PROP; + // RequestParam annotation if (frameworkAnnotations.getRequestParamAnnotation().getAnnotationName().equals(annotationName)) { defaultValueProp = frameworkAnnotations.getRequestParamAnnotation().getDefaultValueProp(); requiredProp = frameworkAnnotations.getRequestParamAnnotation().getRequiredProp(); - isRequestParam = true; + apiParamEnum = ApiParamEnum.QUERY; } - if (frameworkAnnotations.getPathVariableAnnotation().getAnnotationName().equals(annotationName)) { + // PathVariable annotation + else if (frameworkAnnotations.getPathVariableAnnotation() + .getAnnotationName() + .equals(annotationName)) { defaultValueProp = frameworkAnnotations.getPathVariableAnnotation().getDefaultValueProp(); requiredProp = frameworkAnnotations.getPathVariableAnnotation().getRequiredProp(); - isPathVariable = true; + apiParamEnum = ApiParamEnum.PATH; } - if (frameworkAnnotations.getRequestPartAnnotation().getAnnotationName().equals(annotationName)) { + // RequestPart annotation + else if (frameworkAnnotations.getRequestPartAnnotation() + .getAnnotationName() + .equals(annotationName)) { requiredProp = frameworkAnnotations.getRequestPartAnnotation().getRequiredProp(); isRequestPart = true; mockValue = JsonBuildHelper.buildJson(fullyQualifiedName, typeName, Boolean.FALSE, 0, new HashMap<>(16), groupClasses, docJavaMethod.getJsonViewClasses(), builder); requestBodyCounter++; + apiParamEnum = ApiParamEnum.BODY; } AnnotationValue annotationDefaultVal = annotation.getProperty(defaultValueProp); if (Objects.nonNull(annotationDefaultVal)) { mockValue = DocUtil.resolveAnnotationValue(classLoader, annotationDefaultVal); } - paramName = getParamName(classLoader, paramName, annotation); + paramName = this.getParamName(classLoader, paramName, annotation); AnnotationValue annotationRequired = annotation.getProperty(requiredProp); if (Objects.nonNull(annotationRequired)) { strRequired = annotationRequired.toString(); @@ -1059,24 +1086,37 @@ default ApiMethodReqParam requestParams(final DocJavaMethod docJavaMethod, Proje strRequired = "true"; } } + // when annotation is Jsr303 required annotation if (JavaClassValidateUtil.isJSR303Required(annotationName)) { strRequired = "true"; } + // RequestBody annotation if (frameworkAnnotations.getRequestBodyAnnotation().getAnnotationName().equals(annotationName)) { - // if (requestBodyCounter > 0) { - // throw new RuntimeException("You have use @RequestBody Passing - // multiple variables for method " - // + javaMethod.getName() + " in " + className + ",@RequestBody - // annotation could only bind one variables."); - // } mockValue = JsonBuildHelper.buildJson(fullyQualifiedName, typeName, Boolean.FALSE, 0, new HashMap<>(16), groupClasses, docJavaMethod.getJsonViewClasses(), builder); requestBodyCounter++; - isRequestBody = true; + apiParamEnum = ApiParamEnum.BODY; } required = Boolean.parseBoolean(strRequired); } - comment.append(JavaFieldUtil.getJsrComment(isShowValidation, classLoader, annotations)); + // not get and delete method and has MediaType + boolean bodyMediaType = !(Methods.GET.getValue().equals(docJavaMethod.getMethodType()) + || Methods.DELETE.getValue().equals(docJavaMethod.getMethodType())) + && StringUtil.isNotEmpty(methodMediaType) + && (MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(methodMediaType) + || MediaType.APPLICATION_JSON_VALUE.equals(methodMediaType) + || MediaType.MULTIPART_FORM_DATA_VALUE.equals(methodMediaType)); + if (bodyMediaType) { + apiParamEnum = ApiParamEnum.BODY; + } + // If the parameter is not in the request body, it is a query parameter + // Fixed issue #965 + if (apiParamEnum == null && (!requestBodyParam.isEmpty() && !requestBodyParam.contains(apiParameter))) { + apiParamEnum = ApiParamEnum.QUERY; + } + boolean isQueryParam = ApiParamEnum.QUERY.equals(apiParamEnum); + boolean isPathVariable = ApiParamEnum.PATH.equals(apiParamEnum); + comment.append(JavaFieldUtil.getJsrComment(isShowValidation, classLoader, paramAnnotations)); if (requestFieldToUnderline && !isPathVariable) { paramName = StringUtil.camelToUnderline(paramName); } @@ -1100,8 +1140,6 @@ default ApiMethodReqParam requestParams(final DocJavaMethod docJavaMethod, Proje continue; } - boolean queryParam = !isRequestBody && !isPathVariable; - String[] gicNameArr = DocClassUtil.getSimpleGicName(typeName); // Handle if it is collection types if (JavaClassValidateUtil.isCollection(fullyQualifiedName) @@ -1124,7 +1162,7 @@ default ApiMethodReqParam requestParams(final DocJavaMethod docJavaMethod, Proje .setDesc(comment + ",[array of enum]") .setRequired(required) .setPathParam(isPathVariable) - .setQueryParam(queryParam) + .setQueryParam(isQueryParam) .setId(paramList.size() + 1) .setType(ParamTypeConstants.PARAM_TYPE_ARRAY); EnumInfoAndValues enumInfoAndValue = JavaClassUtil.getEnumInfoAndValue(gicJavaClass, builder, @@ -1148,7 +1186,7 @@ else if (JavaClassValidateUtil.isPrimitive(gicName)) { .setDesc(comment + ",[array of " + shortSimple + "]") .setRequired(required) .setPathParam(isPathVariable) - .setQueryParam(queryParam) + .setQueryParam(isQueryParam) .setId(paramList.size() + 1) .setType(ParamTypeConstants.PARAM_TYPE_ARRAY) .setVersion(DocGlobalConstants.DEFAULT_VERSION) @@ -1173,7 +1211,7 @@ else if (JavaClassValidateUtil.isFile(gicName)) { paramList.add(param); } else { - if (requestBodyCounter > 0 || !isRequestParam) { + if (requestBodyCounter > 0 || !ApiParamEnum.QUERY.equals(apiParamEnum)) { // for json paramList.addAll(ParamsBuildHelper.buildParams(gicNameArr[0], DocGlobalConstants.EMPTY, 0, String.valueOf(required), Boolean.FALSE, new HashMap<>(16), builder, groupClasses, @@ -1188,7 +1226,7 @@ else if (JavaClassValidateUtil.isPrimitive(fullyQualifiedName)) { .setType(DocClassUtil.processTypeNameForParams(simpleName)) .setId(paramList.size() + 1) .setPathParam(isPathVariable) - .setQueryParam(queryParam) + .setQueryParam(isQueryParam) .setValue(mockValue) .setDesc(comment.toString()) .setRequired(required) @@ -1231,14 +1269,14 @@ else if (javaClass.isEnum()) { .setField(paramName) .setId(paramList.size() + 1) .setPathParam(isPathVariable) - .setQueryParam(queryParam) + .setQueryParam(isQueryParam) .setType(ParamTypeConstants.PARAM_TYPE_ENUM) .setDesc(comment.toString()) .setRequired(required) .setVersion(DocGlobalConstants.DEFAULT_VERSION); EnumInfoAndValues enumInfoAndValue = JavaClassUtil.getEnumInfoAndValue(javaClass, builder, - isPathVariable || queryParam || isRequestParam); + isPathVariable || isQueryParam); if (Objects.nonNull(enumInfoAndValue)) { param.setValue(StringUtil.removeDoubleQuotes(String.valueOf(enumInfoAndValue.getValue()))) .setEnumInfoAndValues(enumInfoAndValue) @@ -1254,7 +1292,7 @@ else if (isRequestPart) { .setField(paramName) .setId(paramList.size() + 1) .setPathParam(isPathVariable) - .setQueryParam(queryParam) + .setQueryParam(isQueryParam) .setValue(mockValue) .setType(ParamTypeConstants.PARAM_TYPE_OBJECT) .setDesc(comment.toString()) @@ -1263,12 +1301,22 @@ else if (isRequestPart) { paramList.add(param); paramList.addAll(ParamsBuildHelper.buildParams(typeName, DocGlobalConstants.PARAM_PREFIX, 1, String.valueOf(required), Boolean.FALSE, new HashMap<>(16), builder, groupClasses, - docJavaMethod.getJsonViewClasses(), 1, isRequestBody, null)); + docJavaMethod.getJsonViewClasses(), 1, ApiParamEnum.BODY.equals(apiParamEnum), null)); } else { - paramList.addAll(ParamsBuildHelper.buildParams(typeName, DocGlobalConstants.EMPTY, 0, + List apiParams = ParamsBuildHelper.buildParams(typeName, DocGlobalConstants.EMPTY, 0, String.valueOf(required), Boolean.FALSE, new HashMap<>(16), builder, groupClasses, - docJavaMethod.getJsonViewClasses(), 0, isRequestBody, null)); + docJavaMethod.getJsonViewClasses(), 0, ApiParamEnum.BODY.equals(apiParamEnum), null); + + boolean hasFile = apiParams.stream() + .anyMatch(param -> ParamTypeConstants.PARAM_TYPE_FILE.equals(param.getType())); + // if it does not have file and query param, set query param true + if (!hasFile && ApiParamEnum.QUERY.equals(apiParamEnum)) { + for (ApiParam apiParam : apiParams) { + apiParam.traverseAndConsume(ApiParam::setQueryParamTrue); + } + } + paramList.addAll(apiParams); } } return ApiParamTreeUtil.buildMethodReqParam(paramList, queryReqParamMap, pathReqParamMap,