diff --git a/UPDATE_LOG.md b/UPDATE_LOG.md
index 4e6e89311a..323fa5e71a 100644
--- a/UPDATE_LOG.md
+++ b/UPDATE_LOG.md
@@ -14,6 +14,7 @@
### 2.4.2
* 新增 lettuce-solon-plugin 插件
+* 新增 solon.docs.openapi2 插件
* 新增 solon.cloud.metrics 插件?
* 升级 solon-maven-plugin 的相关依赖
* 增加 solon-admin-server 对 basic auth 配置的支持
diff --git a/__release/solon-base-bundle/pom.xml b/__release/solon-base-bundle/pom.xml
index 7e2c318679..64b638aa43 100644
--- a/__release/solon-base-bundle/pom.xml
+++ b/__release/solon-base-bundle/pom.xml
@@ -31,7 +31,7 @@
../../solon-projects/solon-base/solon.docs
../../solon-projects/solon-base/solon.docs.openapi2
- ../../solon-projects/solon-base/solon.docs.openapi3
+
../../solon-projects/solon-base/solon.auth
../../solon-projects/solon-base/solon.test
diff --git a/solon-parent/pom.xml b/solon-parent/pom.xml
index 350d59b0ed..3cf3193ca1 100644
--- a/solon-parent/pom.xml
+++ b/solon-parent/pom.xml
@@ -55,7 +55,7 @@
2.0
1.6.11
- 2.2.15
+ 2.1.13
4.1.0
2.14.3
@@ -409,11 +409,6 @@
solon.docs.openapi2
${solon.version}
-
- org.noear
- solon.docs.openapi3
- ${solon.version}
-
org.noear
solon.auth
diff --git a/solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/Swagger2Builder.java b/solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/OpenApi2Builder.java
similarity index 94%
rename from solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/Swagger2Builder.java
rename to solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/OpenApi2Builder.java
index 4eceeda96d..74a221ab33 100644
--- a/solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/Swagger2Builder.java
+++ b/solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/OpenApi2Builder.java
@@ -1,15 +1,16 @@
package org.noear.solon.docs.openapi2;
import io.swagger.annotations.*;
+import io.swagger.models.Contact;
import io.swagger.models.ExternalDocs;
import io.swagger.models.Info;
+import io.swagger.models.License;
import io.swagger.models.Tag;
import io.swagger.models.*;
import io.swagger.models.parameters.Parameter;
import io.swagger.models.parameters.*;
import io.swagger.models.properties.*;
import io.swagger.models.refs.RefFormat;
-import io.swagger.solon.annotation.ApiNoAuthorize;
import io.swagger.solon.annotation.ApiRes;
import io.swagger.solon.annotation.ApiResProperty;
import org.noear.solon.Solon;
@@ -23,7 +24,12 @@
import org.noear.solon.docs.ApiEnum;
import org.noear.solon.docs.DocDocket;
import org.noear.solon.docs.exception.DocException;
+import org.noear.solon.docs.models.ApiContact;
+import org.noear.solon.docs.models.ApiLicense;
import org.noear.solon.docs.models.ApiScheme;
+import org.noear.solon.docs.openapi2.impl.ActionHolder;
+import org.noear.solon.docs.openapi2.impl.BuilderHelper;
+import org.noear.solon.docs.openapi2.impl.ParamHolder;
import java.lang.reflect.*;
import java.text.Collator;
@@ -35,7 +41,7 @@
* @author noear
* @since 2.3
*/
-public class Swagger2Builder {
+public class OpenApi2Builder {
private final Swagger swagger = new Swagger();
private final DocDocket docket;
@@ -44,7 +50,7 @@ public class Swagger2Builder {
*/
private ModelImpl globalResultModel;
- public Swagger2Builder(DocDocket docket) {
+ public OpenApi2Builder(DocDocket docket) {
this.docket = docket;
}
@@ -57,15 +63,34 @@ public Swagger build() {
// 解析JSON
this.parseGroupPackage();
+ ApiLicense apiLicense = docket.info().license();
+ ApiContact apiContact = docket.info().contact();
swagger.setSwagger(docket.version());
swagger.info(new Info()
.title(docket.info().title())
.description(docket.info().description())
.termsOfService(docket.info().termsOfService())
- .version(docket.info().version())
- .license(docket.info().license())
- .contact(docket.info().contact()));
+ .version(docket.info().version()));
+
+ if (apiLicense != null) {
+ License license = new License()
+ .url(apiLicense.url())
+ .name(apiLicense.name());
+ license.setVendorExtensions(apiLicense.vendorExtensions());
+
+ swagger.getInfo().setLicense(license);
+ }
+
+ if (apiContact != null) {
+ Contact contact = new Contact()
+ .email(apiContact.email())
+ .name(apiContact.name())
+ .url(apiContact.url());
+ contact.setVendorExtensions(apiContact.vendorExtensions());
+ swagger.getInfo().contact(contact);
+ }
+
swagger.host(BuilderHelper.getHost(docket));
swagger.basePath(docket.basePath());
@@ -81,7 +106,7 @@ public Swagger build() {
}
swagger.vendorExtensions(docket.vendorExtensions());
- swagger.setSecurityDefinitions(docket.securityDefinitions());
+ //swagger.setSecurityDefinitions(docket.securityDefinitions());
if (swagger.getTags() != null) {
//排序
@@ -253,12 +278,12 @@ private void parseAction(List actionHolders) {
operation.setDescription(apiAction.notes());
operation.setDeprecated(actionHolder.isAnnotationPresent(Deprecated.class));
- if ((actionHolder.isAnnotationPresent(ApiNoAuthorize.class) ||
- actionHolder.controllerClz().isAnnotationPresent(ApiNoAuthorize.class)) == false) {
- for (String securityName : docket.securityDefinitions().keySet()) {
- operation.security(new SecurityRequirement(securityName).scope("global"));
- }
- }
+// if ((actionHolder.isAnnotationPresent(ApiNoAuthorize.class) ||
+// actionHolder.controllerClz().isAnnotationPresent(ApiNoAuthorize.class)) == false) {
+// for (String securityName : docket.securityDefinitions().keySet()) {
+// operation.security(new SecurityRequirement(securityName).scope("global"));
+// }
+// }
String operationMethod = BuilderHelper.getHttpMethod(actionHolder, apiAction);
diff --git a/solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/OpenApi2Utils.java b/solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/OpenApi2Utils.java
index e879786f48..08f4aa6a59 100644
--- a/solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/OpenApi2Utils.java
+++ b/solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/OpenApi2Utils.java
@@ -59,7 +59,7 @@ public static String getApiJson(Context ctx, String group) throws IOException {
docket.globalResponseCodes().put(200, "");
}
- Swagger swagger = new Swagger2Builder(docket).build();
+ Swagger swagger = new OpenApi2Builder(docket).build();
return JsonUtil.toJson(swagger);
}
}
diff --git a/solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/ActionHolder.java b/solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/impl/ActionHolder.java
similarity index 97%
rename from solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/ActionHolder.java
rename to solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/impl/ActionHolder.java
index 893974a2ca..841b182d44 100644
--- a/solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/ActionHolder.java
+++ b/solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/impl/ActionHolder.java
@@ -1,4 +1,4 @@
-package org.noear.solon.docs.openapi2;
+package org.noear.solon.docs.openapi2.impl;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
diff --git a/solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/BuilderHelper.java b/solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/impl/BuilderHelper.java
similarity index 98%
rename from solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/BuilderHelper.java
rename to solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/impl/BuilderHelper.java
index aeb89759f9..350d1b98d0 100644
--- a/solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/BuilderHelper.java
+++ b/solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/impl/BuilderHelper.java
@@ -1,4 +1,4 @@
-package org.noear.solon.docs.openapi2;
+package org.noear.solon.docs.openapi2.impl;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiOperation;
diff --git a/solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/ParamHolder.java b/solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/impl/ParamHolder.java
similarity index 98%
rename from solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/ParamHolder.java
rename to solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/impl/ParamHolder.java
index f0af11d47f..a62faeba29 100644
--- a/solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/ParamHolder.java
+++ b/solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/impl/ParamHolder.java
@@ -1,4 +1,4 @@
-package org.noear.solon.docs.openapi2;
+package org.noear.solon.docs.openapi2.impl;
import io.swagger.annotations.ApiImplicitParam;
import org.noear.solon.Utils;
diff --git a/solon-projects/solon-base/solon.docs.openapi3/pom.xml b/solon-projects/solon-base/solon.docs.openapi3/pom.xml
new file mode 100644
index 0000000000..7532ae0961
--- /dev/null
+++ b/solon-projects/solon-base/solon.docs.openapi3/pom.xml
@@ -0,0 +1,45 @@
+
+
+ 4.0.0
+
+
+ org.noear
+ solon-parent
+ 2.4.2-SNAPSHOT
+ ../../../solon-parent/pom.xml
+
+
+ solon.docs.openapi3
+ jar
+
+
+
+ org.noear
+ solon.docs
+
+
+
+ io.swagger.core.v3
+ swagger-annotations
+ ${swagger2.version}
+
+
+
+ io.swagger.core.v3
+ swagger-models
+ ${swagger2.version}
+
+
+ org.slf4j
+ slf4j-api
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+
+
+
+
+
\ No newline at end of file
diff --git a/solon-projects/solon-base/solon.docs.openapi3/src/main/java/org/noear/solon/docs/openapi3/OpenApi3Builder.java b/solon-projects/solon-base/solon.docs.openapi3/src/main/java/org/noear/solon/docs/openapi3/OpenApi3Builder.java
new file mode 100644
index 0000000000..0d686adbb3
--- /dev/null
+++ b/solon-projects/solon-base/solon.docs.openapi3/src/main/java/org/noear/solon/docs/openapi3/OpenApi3Builder.java
@@ -0,0 +1,857 @@
+package org.noear.solon.docs.openapi3;
+
+
+import io.swagger.solon.annotation.ApiNoAuthorize;
+import io.swagger.solon.annotation.ApiRes;
+import io.swagger.solon.annotation.ApiResProperty;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Contact;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.info.License;
+import io.swagger.v3.oas.models.servers.Server;
+import org.noear.solon.Solon;
+import org.noear.solon.Utils;
+import org.noear.solon.core.handle.*;
+import org.noear.solon.core.route.Routing;
+import org.noear.solon.core.util.GenericUtil;
+import org.noear.solon.core.wrap.ClassWrap;
+import org.noear.solon.core.wrap.FieldWrap;
+import org.noear.solon.core.wrap.ParamWrap;
+import org.noear.solon.docs.ApiEnum;
+import org.noear.solon.docs.DocDocket;
+import org.noear.solon.docs.exception.DocException;
+import org.noear.solon.docs.models.ApiContact;
+import org.noear.solon.docs.models.ApiLicense;
+import org.noear.solon.docs.models.ApiScheme;
+import org.noear.solon.docs.openapi3.impl.ActionHolder;
+import org.noear.solon.docs.openapi3.impl.BuilderHelper;
+import org.noear.solon.docs.openapi3.impl.ParamHolder;
+
+import java.lang.reflect.*;
+import java.text.Collator;
+import java.util.*;
+
+/**
+ * openapi v3 json builder
+ *
+ * @author noear
+ * @since 2.3
+ */
+public class OpenApi3Builder {
+ private final OpenAPI swagger = new OpenAPI();
+ private final DocDocket docket;
+
+ /**
+ * 公共返回模型
+ */
+ private ModelImpl globalResultModel;
+
+ public OpenApi3Builder(DocDocket docket) {
+ this.docket = docket;
+ }
+
+ public OpenAPI build() {
+ // 解析通用返回
+ if (docket.globalResult() != null) {
+ this.globalResultModel = (ModelImpl) this.parseSwaggerModel(docket.globalResult(), docket.globalResult());
+ }
+
+ // 解析JSON
+ this.parseGroupPackage();
+
+ ApiLicense apiLicense = docket.info().license();
+ ApiContact apiContact = docket.info().contact();
+
+ //swagger.setSwagger(docket.version());
+ swagger.info(new Info()
+ .title(docket.info().title())
+ .description(docket.info().description())
+ .termsOfService(docket.info().termsOfService())
+ .version(docket.info().version()));
+
+ if (apiLicense != null) {
+ License license = new License()
+ .url(apiLicense.url())
+ .name(apiLicense.name());
+ license.setExtensions(apiLicense.vendorExtensions());
+
+ swagger.getInfo().setLicense(license);
+ }
+
+ if (apiContact != null) {
+ Contact contact = new Contact()
+ .email(apiContact.email())
+ .name(apiContact.name())
+ .url(apiContact.url());
+ contact.setExtensions(apiContact.vendorExtensions());
+ swagger.getInfo().contact(contact);
+ }
+
+ swagger.addServersItem(new Server().url(BuilderHelper.getHost(docket)));
+ //swagger.basePath(docket.basePath());
+
+ if (docket.schemes() != null) {
+ for (ApiScheme scheme : docket.schemes()) {
+ swagger.scheme(Scheme.forValue(scheme.toValue()));
+ }
+ }
+
+ if (docket.externalDocs() != null) {
+ swagger.externalDocs(new ExternalDocs(docket.externalDocs().description(), docket.externalDocs().url()));
+ }
+
+ swagger.vendorExtensions(docket.vendorExtensions());
+ swagger.setSecurityDefinitions(docket.securityDefinitions());
+
+ if (swagger.getTags() != null) {
+ //排序
+ swagger.getTags().sort((t1, t2) -> {
+ String name1 = t1.getDescription();
+ String name2 = t2.getDescription();
+
+ return Collator.getInstance(Locale.UK).compare(name1, name2);
+ });
+ }
+
+ if (swagger.getDefinitions() != null) {
+ //排序
+ List definitionKeys = new ArrayList<>(swagger.getDefinitions().keySet());
+ Map definitionMap = new LinkedHashMap<>();
+ definitionKeys.sort((name1, name2) -> Collator.getInstance(Locale.UK).compare(name1, name2));
+ for (String name : definitionKeys) {
+ definitionMap.put(name, swagger.getDefinitions().get(name));
+ }
+ swagger.setDefinitions(definitionMap);
+ }
+
+ return swagger;
+ }
+
+ /**
+ * 解析分组包
+ */
+ private void parseGroupPackage() {
+ //获取所有控制器及动作
+ Map, List> classMap = this.getApiAction();
+
+ for (Map.Entry, List> kv : classMap.entrySet()) {
+ // 解析controller
+ this.parseController(kv.getKey(), kv.getValue());
+ }
+ }
+
+
+ /**
+ * 获取全部Action
+ */
+ private Map, List> getApiAction() {
+ Map, List> apiMap = new HashMap<>(16);
+
+ Collection> routingCollection = Solon.app().router().getAll(Endpoint.main);
+ for (Routing routing : routingCollection) {
+ if (routing.target() instanceof Action) {
+ //如果是 Action
+ resolveAction(apiMap, routing);
+ }
+
+ if (routing.target() instanceof Gateway) {
+ //如果是 Gateway (网关)
+ for (Routing routing2 : ((Gateway) routing.target()).getMainRouting().getAll()) {
+ if (routing2.target() instanceof Action) {
+ resolveAction(apiMap, routing2);
+ }
+ }
+ }
+ }
+
+ List> ctlList = new ArrayList<>(apiMap.keySet());
+ ctlList.sort(Comparator.comparingInt(clazz -> clazz.getAnnotation(Api.class).position()));
+
+ Map, List> result = new LinkedHashMap<>();
+ ctlList.forEach(i -> {
+ List actionHolders = apiMap.get(i);
+ actionHolders.sort(Comparator.comparingInt(ah -> ah.getAnnotation(ApiOperation.class).position()));
+ result.put(i, actionHolders);
+ });
+
+ return result;
+ }
+
+ private void resolveAction(Map, List> apiMap, Routing routing) {
+ Action action = (Action) routing.target();
+ Class> controller = action.controller().clz();
+
+ boolean matched = docket.apis().stream().anyMatch(res -> res.test(action));
+ if (matched == false) {
+ return;
+ }
+
+ ActionHolder actionHolder = new ActionHolder(routing, action);
+
+ if (apiMap.containsKey(controller)) {
+ if (action.method().isAnnotationPresent(ApiOperation.class)) {
+ List actionHolders = apiMap.get(controller);
+ if (!actionHolders.contains(actionHolder)) {
+ actionHolders.add(actionHolder);
+ apiMap.put(controller, actionHolders);
+ }
+ }
+ } else {
+ if (controller.isAnnotationPresent(Api.class)) {
+ if (action.method().isAnnotationPresent(ApiOperation.class)) {
+ List actionHolders = new ArrayList<>();
+ actionHolders.add(actionHolder);
+ apiMap.put(controller, actionHolders);
+ }
+ }
+ }
+ }
+
+ /**
+ * 解析controller
+ */
+ private void parseController(Class> clazz, List actionHolders) {
+ // controller 信息
+ Api api = clazz.getAnnotation(Api.class);
+ boolean hidden = api.hidden();
+ if (hidden) {
+ return;
+ }
+
+ String controllerKey = BuilderHelper.getControllerKey(clazz);
+ Set apiTags = new LinkedHashSet<>();
+ apiTags.add(api.value());
+ apiTags.addAll(Arrays.asList(api.tags()));
+ apiTags.remove("");
+
+ for (String tagName : apiTags) {
+ Tag tag = new Tag();
+ tag.setName(tagName);
+ tag.setDescription(controllerKey + " (" + clazz.getSimpleName() + ")");
+
+ swagger.addTag(tag);
+ }
+
+
+ // 解析action
+ this.parseAction(actionHolders);
+ }
+
+ /**
+ * 解析action
+ */
+ private void parseAction(List actionHolders) {
+ for (ActionHolder actionHolder : actionHolders) {
+
+ Operation apiAction = actionHolder.getAnnotation(Operation.class);
+
+ if (apiAction.hidden()) {
+ return;
+ }
+
+ String controllerKey = BuilderHelper.getControllerKey(actionHolder.controllerClz());
+ String actionName = actionHolder.action().name();//action.getMethodName();
+ Method actionMethod = actionHolder.action().method().getMethod();
+
+ Set actionTags = actionHolder.getTags(apiAction);
+
+
+ String pathKey = actionHolder.routing().path(); //PathUtil.mergePath(controllerKey, actionName);
+
+ Path path = swagger.getPath(pathKey);
+ if (path == null) {
+ //path 要重复可用
+ path = new Path();
+ swagger.path(pathKey, path);
+ }
+
+
+ Operation operation = new Operation();
+
+ operation.setTags(new ArrayList<>(actionTags));
+ operation.setSummary(apiAction.value());
+ operation.setDescription(apiAction.notes());
+ operation.setDeprecated(actionHolder.isAnnotationPresent(Deprecated.class));
+
+ if ((actionHolder.isAnnotationPresent(ApiNoAuthorize.class) ||
+ actionHolder.controllerClz().isAnnotationPresent(ApiNoAuthorize.class)) == false) {
+ for (String securityName : docket.securityDefinitions().keySet()) {
+ operation.security(new SecurityRequirement(securityName).scope("global"));
+ }
+ }
+
+
+ String operationMethod = BuilderHelper.getHttpMethod(actionHolder, apiAction);
+
+
+ operation.setParameters(this.parseActionParameters(actionHolder));
+ operation.setResponses(this.parseActionResponse(controllerKey, actionName, actionMethod));
+ operation.setVendorExtension("controllerKey", controllerKey);
+ operation.setVendorExtension("actionName", actionName);
+
+ if (Utils.isBlank(apiAction.consumes())) {
+ if (operationMethod.equals(ApiEnum.METHOD_GET)) {
+ operation.consumes(ApiEnum.CONSUMES_URLENCODED); //如果是 get ,则没有 content-type
+ } else {
+ operation.consumes(ApiEnum.CONSUMES_URLENCODED);
+ }
+ } else {
+ operation.consumes(apiAction.consumes());
+ }
+
+ operation.produces(Utils.isBlank(apiAction.produces()) ? ApiEnum.PRODUCES_DEFAULT : apiAction.produces());
+
+ operation.setOperationId(operationMethod + "_" + pathKey.replace("/", "_"));
+
+ path.set(operationMethod, operation);
+ }
+ }
+
+ /**
+ * 解析action 参数文档
+ */
+ private List parseActionParameters(ActionHolder actionHolder) {
+ Map actionParamMap = new LinkedHashMap<>();
+ for (ParamWrap p1 : actionHolder.action().method().getParamWraps()) {
+ actionParamMap.put(p1.getName(), new ParamHolder(p1));
+ }
+
+ // 获取参数注解信息
+ {
+ List apiParams = new ArrayList<>();
+ if (actionHolder.isAnnotationPresent(ApiImplicitParams.class)) {
+ apiParams.addAll(Arrays.asList(actionHolder.getAnnotation(ApiImplicitParams.class).value()));
+ }
+
+ if (actionHolder.isAnnotationPresent(ApiImplicitParams.class)) {
+ ApiImplicitParam[] paramArray = actionHolder.getAnnotationsByType(ApiImplicitParam.class);
+ apiParams.addAll(Arrays.asList(paramArray));
+ }
+
+ for (ApiImplicitParam a1 : apiParams) {
+ ParamHolder paramHolder = actionParamMap.get(a1.name());
+ if (paramHolder == null) {
+ paramHolder = new ParamHolder(null);
+ actionParamMap.put(a1.name(), paramHolder);
+ }
+ paramHolder.binding(a1);
+ }
+ }
+
+ // 构建参数列表(包含全局参数)
+ List paramList = new ArrayList<>();
+
+ for (ParamHolder paramHolder : actionParamMap.values()) {
+ if (paramHolder.isIgnore()) {
+ continue;
+ }
+
+ String paramSchema = this.getParameterSchema(paramHolder);
+ String dataType = paramHolder.dataType();
+
+ Parameter parameter;
+
+ if (paramHolder.allowMultiple()) {
+ if (Utils.isNotEmpty(paramSchema)) {
+ //array model
+ BodyParameter modelParameter = new BodyParameter();
+ modelParameter.setSchema(new ArrayModel().items(new RefProperty(paramSchema)));
+ if (paramHolder.getParam() != null && paramHolder.getParam().isRequiredBody() == false) {
+ modelParameter.setIn(ApiEnum.PARAM_TYPE_QUERY);
+ }
+
+ parameter = modelParameter;
+ } else if ("file".equals(dataType)) {
+ //array file
+ FormParameter formParameter = new FormParameter();
+ formParameter.type("array");
+ formParameter.items(new FileProperty());
+
+ parameter = formParameter;
+ } else {
+ //array
+ ObjectProperty objectProperty = new ObjectProperty();
+ objectProperty.setType(dataType);
+
+ QueryParameter queryParameter = new QueryParameter();
+ queryParameter.type("array");
+ queryParameter.items(objectProperty);
+
+ parameter = queryParameter;
+ }
+ } else {
+ if (Utils.isNotEmpty(paramSchema)) {
+ //model
+ if (paramHolder.isRequiredBody() || paramHolder.getParam() == null) {
+ //做为 body
+ BodyParameter modelParameter = new BodyParameter();
+
+ if (paramHolder.isMap()) {
+ modelParameter.setSchema(new ModelImpl().type("object"));
+ } else {
+ modelParameter.setSchema(new RefModel(paramSchema));
+ }
+
+ if (paramHolder.getParam() != null && paramHolder.getParam().isRequiredBody() == false) {
+ modelParameter.setIn(ApiEnum.PARAM_TYPE_QUERY);
+ }
+
+ parameter = modelParameter;
+ } else {
+ parseActionParametersByFields(paramHolder, paramList);
+
+ continue;
+ }
+
+ } else if ("file".equals(dataType)) {
+ //array file
+ FormParameter formParameter = new FormParameter();
+ formParameter.items(new FileProperty());
+
+ parameter = formParameter;
+ } else {
+ if (paramHolder.isRequiredHeader()) {
+ parameter = new HeaderParameter();
+ } else if (paramHolder.isRequiredCookie()) {
+ parameter = new CookieParameter();
+ } else if (paramHolder.isRequiredPath()) {
+ parameter = new PathParameter();
+ } else {
+ QueryParameter queryParameter = new QueryParameter();
+ queryParameter.setType(dataType);
+
+ if (paramHolder.getAnno() != null) {
+ queryParameter.setFormat(paramHolder.getAnno().format());
+ queryParameter.setDefaultValue(paramHolder.getAnno().defaultValue());
+ }
+
+ parameter = queryParameter;
+ }
+ }
+ }
+
+ parameter.setName(paramHolder.getName());
+ parameter.setDescription(paramHolder.getDescription());
+ parameter.setRequired(paramHolder.isRequired());
+ parameter.setReadOnly(paramHolder.isReadOnly());
+
+ if (Utils.isEmpty(parameter.getIn())) {
+ parameter.setIn(paramHolder.paramType());
+ }
+
+ paramList.add(parameter);
+ }
+
+ return paramList;
+ }
+
+
+ private void parseActionParametersByFields(ParamHolder paramHolder, List paramList) {
+ //做为 字段
+ ClassWrap classWrap = ClassWrap.get(paramHolder.getParam().getType());
+ for (FieldWrap fw : classWrap.getFieldAllWraps().values()) {
+ if (Modifier.isTransient(fw.field.getModifiers())) {
+ continue;
+ }
+
+ QueryParameter parameter = new QueryParameter();
+ parameter.setType(fw.type.getSimpleName());
+
+ ApiModelProperty anno = fw.field.getAnnotation(ApiModelProperty.class);
+
+ if (anno != null) {
+ parameter.setName(anno.name());
+ parameter.setDescription(anno.value());
+ parameter.setRequired(anno.required());
+ parameter.setReadOnly(anno.readOnly());
+
+ if (Utils.isNotEmpty(anno.dataType())) {
+ parameter.setType(anno.dataType());
+ }
+ }
+
+ if (Utils.isEmpty(parameter.getName())) {
+ parameter.setName(fw.field.getName());
+ }
+
+
+ paramList.add(parameter);
+ }
+ }
+
+
+ /**
+ * 解析action 返回文档
+ */
+ private Map parseActionResponse(String controllerKey, String actionName, Method method) {
+ Map responseMap = new LinkedHashMap<>();
+
+ docket.globalResponseCodes().forEach((key, value) -> {
+ Response response = new Response();
+ response.description(value);
+
+ if (key == 200) {
+ String schema = this.parseResponse(controllerKey, actionName, method);
+ if (schema != null) {
+ response.setResponseSchema(new RefModel(schema));
+ }
+ }
+
+ responseMap.put(String.valueOf(key), response);
+
+ });
+
+ return responseMap;
+ }
+
+ /**
+ * 解析返回值
+ */
+ private String parseResponse(String controllerKey, String actionName, Method method) {
+ // swagger model 引用
+ String swaggerModelName = null;
+
+ List responses = new ArrayList<>();
+ if (method.isAnnotationPresent(ApiRes.class)) {
+ responses.addAll(Arrays.asList(method.getAnnotation(ApiRes.class).value()));
+ }
+ if (method.isAnnotationPresent(ApiRes.class)) {
+ ApiResProperty[] paramArray = method.getAnnotationsByType(ApiResProperty.class);
+ responses.addAll(Arrays.asList(paramArray));
+ }
+
+ // 2.9.1 实验性质 自定义返回值
+ Class> apiResClz = method.getReturnType();
+ if (apiResClz != Void.class) {
+ if (BuilderHelper.isModel(apiResClz)) {
+ try {
+ ModelImpl commonResKv = (ModelImpl) this.parseSwaggerModel(apiResClz, method.getGenericReturnType());
+ swaggerModelName = commonResKv.getName();
+ return swaggerModelName;
+ } catch (Exception e) {
+ String hint = method.getDeclaringClass().getName() + ":" + method.getName() + "->" + apiResClz.getSimpleName();
+ throw new DocException("Response model parsing failure: " + hint, e);
+ }
+ }
+ }
+
+
+ if (responses.size() == 0) {
+ if (globalResultModel != null) {
+ swaggerModelName = globalResultModel.getName();
+ }
+ } else {
+ // 将参数放入commonRes中,作为新的swagger Model引用(knife4j 约定)
+ ModelImpl swaggerModelKv = (ModelImpl) this.parseSwaggerModel(controllerKey, actionName, responses);
+ swaggerModelName = swaggerModelKv.getName();
+
+ // 在data中返回参数
+ if (docket.globalResponseInData()) {
+ swaggerModelName = this.toResponseInData(swaggerModelName);
+ }
+ }
+
+ return swaggerModelName;
+ }
+
+ /**
+ * 在data中返回
+ */
+ private String toResponseInData(String swaggerModelName) {
+ if (globalResultModel != null) {
+ Map propertyMap = new LinkedHashMap<>();
+
+ propertyMap.putAll(this.globalResultModel.getProperties());
+
+ RefProperty property = new RefProperty(swaggerModelName, RefFormat.INTERNAL);
+ property.setDescription("返回值");
+
+ propertyMap.put("data", property);
+
+ swaggerModelName = this.globalResultModel.getName() + "«" + swaggerModelName + "»";
+
+ Model model = new ModelImpl();
+ model.setTitle(swaggerModelName);
+ model.setProperties(propertyMap);
+
+ swagger.addDefinition(swaggerModelName, model);
+ }
+
+ return swaggerModelName;
+ }
+
+
+ /**
+ * 将class解析为swagger model
+ */
+ private Model parseSwaggerModel(Class> clazz, Type type) {
+ final String modelName = BuilderHelper.getModelName(clazz, type);
+
+ // 1.已存在,不重复解析
+ if (swagger.getDefinitions() != null) {
+ Model model = swagger.getDefinitions().get(modelName);
+
+ if (null != model) {
+ return model;
+ }
+ }
+
+ // 2.创建模型
+ ApiModel apiModel = clazz.getAnnotation(ApiModel.class);
+ String title;
+ if (apiModel != null) {
+ title = apiModel.description();
+ } else {
+ title = modelName;
+ }
+
+ Map fieldList = new LinkedHashMap<>();
+
+
+ ModelImpl model = new ModelImpl();
+ model.setName(modelName);
+ model.setTitle(title);
+ model.setType(ApiEnum.RES_OBJECT);
+
+ swagger.addDefinition(modelName, model);
+
+
+ // 3.完成模型解析
+ Field[] fields = clazz.getDeclaredFields();
+ for (Field field : fields) {
+ if (Modifier.isStatic(field.getModifiers())) {
+ //静态的跳过
+ continue;
+ }
+
+ ApiModelProperty apiField = field.getAnnotation(ApiModelProperty.class);
+
+ Class> typeClazz = field.getType();
+ Type typeGenericType = field.getGenericType();
+ if (typeGenericType instanceof TypeVariable) {
+ if (type instanceof ParameterizedType) {
+ Map genericMap = GenericUtil.getGenericInfo(type);
+ Type typeClazz2 = genericMap.get(typeGenericType.getTypeName());
+ if (typeClazz2 instanceof Class) {
+ typeClazz = (Class>) typeClazz2;
+ }
+
+ if (typeClazz2 instanceof ParameterizedType) {
+ ParameterizedType typeGenericType2 = (ParameterizedType) typeClazz2;
+ typeClazz = (Class>) typeGenericType2.getRawType();
+ typeGenericType = typeClazz2;
+ }
+ }
+ }
+
+ // List 类型
+ if (Collection.class.isAssignableFrom(typeClazz)) {
+ // 如果是List类型,得到其Generic的类型
+ if (typeGenericType == null) {
+ continue;
+ }
+
+ // 如果是泛型参数的类型
+ if (typeGenericType instanceof ParameterizedType) {
+ ArrayProperty fieldPr = new ArrayProperty();
+ if (apiField != null) {
+ fieldPr.setDescription(apiField.value());
+ }
+
+
+ ParameterizedType pt = (ParameterizedType) typeGenericType;
+ //得到泛型里的class类型对象
+ Type itemClazz = pt.getActualTypeArguments()[0];
+
+ if (itemClazz instanceof ParameterizedType) {
+ itemClazz = ((ParameterizedType) itemClazz).getRawType();
+ }
+
+ if (itemClazz instanceof TypeVariable) {
+ Map genericMap = GenericUtil.getGenericInfo(type);
+ Type itemClazz2 = genericMap.get(itemClazz.getTypeName());
+ if (itemClazz2 instanceof Class) {
+ itemClazz = itemClazz2;
+ }
+ }
+
+ if (itemClazz instanceof Class) {
+ if (itemClazz.equals(type)) {
+ //避免出现循环依赖,然后 oom
+ RefProperty itemPr = new RefProperty(modelName, RefFormat.INTERNAL);
+ fieldPr.setItems(itemPr);
+ } else {
+ ModelImpl swaggerModel = (ModelImpl) this.parseSwaggerModel((Class>) itemClazz, itemClazz);
+
+ RefProperty itemPr = new RefProperty(swaggerModel.getName(), RefFormat.INTERNAL);
+ fieldPr.setItems(itemPr);
+ }
+ }
+
+
+ fieldList.put(field.getName(), fieldPr);
+ }
+ continue;
+ }
+
+
+ if (BuilderHelper.isModel(typeClazz)) {
+ if (typeClazz.equals(type)) {
+ //避免出现循环依赖,然后 oom
+ RefProperty fieldPr = new RefProperty(modelName, RefFormat.INTERNAL);
+ if (apiField != null) {
+ fieldPr.setDescription(apiField.value());
+ }
+
+ fieldList.put(field.getName(), fieldPr);
+ } else {
+ ModelImpl swaggerModel = (ModelImpl) this.parseSwaggerModel(typeClazz, typeGenericType);
+
+ RefProperty fieldPr = new RefProperty(swaggerModel.getName(), RefFormat.INTERNAL);
+ if (apiField != null) {
+ fieldPr.setDescription(apiField.value());
+ }
+
+ fieldList.put(field.getName(), fieldPr);
+ }
+ } else {
+ ObjectProperty fieldPr = new ObjectProperty();
+ fieldPr.setName(field.getName());
+
+ if (apiField != null) {
+ fieldPr.setDescription(apiField.value());
+ fieldPr.setType(Utils.isBlank(apiField.dataType()) ? typeClazz.getSimpleName().toLowerCase() : apiField.dataType());
+ fieldPr.setExample(apiField.example());
+ } else {
+ fieldPr.setType(typeClazz.getSimpleName().toLowerCase());
+ }
+
+ fieldList.put(field.getName(), fieldPr);
+ }
+ }
+
+ model.setProperties(fieldList);
+ return model;
+ }
+
+ /**
+ * 将action response解析为swagger model
+ */
+ private Model parseSwaggerModel(String controllerKey, String actionName, List responses) {
+ final String modelName = controllerKey + "_" + actionName;
+
+ Map propertiesList = new LinkedHashMap<>();
+
+ ModelImpl model = new ModelImpl();
+ model.setName(modelName);
+
+ swagger.addDefinition(modelName, model);
+
+
+ //todo: 不在Data中返回参数
+ if (!docket.globalResponseInData()) {
+ if (globalResultModel != null) {
+ propertiesList.putAll(this.globalResultModel.getProperties());
+ }
+ }
+
+ for (ApiResProperty apiResponse : responses) {
+
+ if (apiResponse.dataTypeClass() != Void.class) {
+ ModelImpl swaggerModel = (ModelImpl) this.parseSwaggerModel(apiResponse.dataTypeClass(), apiResponse.dataTypeClass());
+
+ if (apiResponse.allowMultiple()) {
+ ArrayProperty fieldPr = new ArrayProperty();
+ fieldPr.setName(swaggerModel.getName());
+ fieldPr.setDescription(apiResponse.value());
+ fieldPr.items(new RefProperty(swaggerModel.getName(), RefFormat.INTERNAL));
+
+ propertiesList.put(apiResponse.name(), fieldPr);
+ } else {
+ RefProperty fieldPr = new RefProperty(swaggerModel.getName(), RefFormat.INTERNAL);
+ fieldPr.setDescription(apiResponse.value());
+
+ propertiesList.put(apiResponse.name(), fieldPr);
+ }
+ } else {
+ if (apiResponse.allowMultiple()) {
+ ArrayProperty fieldPr = new ArrayProperty();
+
+ fieldPr.setName(apiResponse.name());
+ fieldPr.setDescription(apiResponse.value());
+ fieldPr.setFormat(Utils.isBlank(apiResponse.format()) ? ApiEnum.FORMAT_STRING : apiResponse.format());
+ fieldPr.setExample(apiResponse.example());
+
+ UntypedProperty itemsProperty = new UntypedProperty();
+ itemsProperty.setType(Utils.isBlank(apiResponse.dataType()) ? ApiEnum.RES_STRING : apiResponse.dataType());
+ fieldPr.items(itemsProperty);
+
+ propertiesList.put(apiResponse.name(), fieldPr);
+ } else {
+ UntypedProperty fieldPr = new UntypedProperty();
+
+ fieldPr.setName(apiResponse.name());
+ fieldPr.setDescription(apiResponse.value());
+ fieldPr.setType(Utils.isBlank(apiResponse.dataType()) ? ApiEnum.RES_STRING : apiResponse.dataType());
+ fieldPr.setFormat(Utils.isBlank(apiResponse.format()) ? ApiEnum.FORMAT_STRING : apiResponse.format());
+ fieldPr.setExample(apiResponse.example());
+
+ propertiesList.put(apiResponse.name(), fieldPr);
+ }
+ }
+ }
+
+ model.setProperties(propertiesList);
+ return model;
+ }
+
+
+ /**
+ * 解析对象参数
+ */
+ private String getParameterSchema(ParamHolder paramHolder) {
+ if (paramHolder.getAnno() != null) {
+ Class> dataTypeClass = paramHolder.getAnno().dataTypeClass();
+
+ if (dataTypeClass != Void.class) {
+ ModelImpl swaggerModel = (ModelImpl) this.parseSwaggerModel(dataTypeClass, dataTypeClass);
+
+ return swaggerModel.getName();
+ }
+ }
+
+ if (paramHolder.getParam() != null) {
+ Class> dataTypeClass = paramHolder.getParam().getType();
+ if (dataTypeClass.isPrimitive()) {
+ return null;
+ }
+
+ if (UploadedFile.class.equals(dataTypeClass)) {
+ return null;
+ }
+
+ if (dataTypeClass.getName().startsWith("java.lang")) {
+ return null;
+ }
+
+ Type dataGenericType = paramHolder.getParam().getGenericType();
+
+ if (dataTypeClass != Void.class) {
+ if (Collection.class.isAssignableFrom(dataTypeClass) && dataGenericType instanceof ParameterizedType) {
+ Type itemType = ((ParameterizedType) dataGenericType).getActualTypeArguments()[0];
+
+ if (itemType instanceof Class) {
+ ModelImpl swaggerModel = (ModelImpl) this.parseSwaggerModel((Class>) itemType, itemType);
+ return swaggerModel.getName();
+ }
+ }
+
+ ModelImpl swaggerModel = (ModelImpl) this.parseSwaggerModel(dataTypeClass, dataGenericType);
+ return swaggerModel.getName();
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/solon-projects/solon-base/solon.docs.openapi3/src/main/java/org/noear/solon/docs/openapi3/OpenApi3Utils.java b/solon-projects/solon-base/solon.docs.openapi3/src/main/java/org/noear/solon/docs/openapi3/OpenApi3Utils.java
new file mode 100644
index 0000000000..0358b63e15
--- /dev/null
+++ b/solon-projects/solon-base/solon.docs.openapi3/src/main/java/org/noear/solon/docs/openapi3/OpenApi3Utils.java
@@ -0,0 +1,64 @@
+package org.noear.solon.docs.openapi3;
+
+import org.noear.solon.Solon;
+import org.noear.solon.Utils;
+import org.noear.solon.core.BeanWrap;
+import org.noear.solon.core.handle.Context;
+import org.noear.solon.docs.DocDocket;
+import org.noear.solon.docs.models.ApiGroupResource;
+import org.noear.solon.docs.util.BasicAuthUtil;
+import org.noear.solon.docs.util.JsonUtil;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Open Api v2 工具类
+ *
+ * @author noear
+ * @since 2.4
+ */
+public class OpenApi3Utils {
+ /**
+ * 获取接口分组资源
+ */
+ public static String getApiGroupResourceJson() throws IOException {
+ List list = Solon.context().getWrapsOfType(DocDocket.class);
+
+ List resourceList = list.stream().filter(bw -> Utils.isNotEmpty(bw.name()))
+ .map(bw -> {
+ String group = bw.name();
+ String groupName = ((DocDocket) bw.raw()).groupName();
+ String url = "/swagger/v2?group=" + group;
+
+ return new ApiGroupResource(groupName, "2.0", url);
+ })
+ .collect(Collectors.toList());
+
+ return JsonUtil.toJson(resourceList);
+ }
+
+ /**
+ * 获取接口
+ */
+ public static String getApiJson(Context ctx, String group) throws IOException {
+ DocDocket docket = Solon.context().getBean(group);
+
+ if (docket == null) {
+ return null;
+ }
+
+ if (!BasicAuthUtil.basicAuth(ctx, docket)) {
+ BasicAuthUtil.response401(ctx);
+ return null;
+ }
+
+ if (docket.globalResponseCodes().containsKey(200) == false) {
+ docket.globalResponseCodes().put(200, "");
+ }
+
+ Swagger swagger = new OpenApi3Builder(docket).build();
+ return JsonUtil.toJson(swagger);
+ }
+}
diff --git a/solon-projects/solon-base/solon.docs.openapi3/src/main/java/org/noear/solon/docs/openapi3/impl/ActionHolder.java b/solon-projects/solon-base/solon.docs.openapi3/src/main/java/org/noear/solon/docs/openapi3/impl/ActionHolder.java
new file mode 100644
index 0000000000..0920137dba
--- /dev/null
+++ b/solon-projects/solon-base/solon.docs.openapi3/src/main/java/org/noear/solon/docs/openapi3/impl/ActionHolder.java
@@ -0,0 +1,76 @@
+package org.noear.solon.docs.openapi3.impl;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.noear.solon.core.handle.Action;
+import org.noear.solon.core.handle.Handler;
+import org.noear.solon.core.handle.MethodType;
+import org.noear.solon.core.route.Routing;
+
+import java.lang.annotation.Annotation;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author noear
+ * @since 2.4
+ */
+public class ActionHolder {
+ private final Routing routing;
+ private final Action action;
+
+ public Routing routing(){
+ return routing;
+ }
+
+ public Action action(){
+ return action;
+ }
+
+
+ public Class> controllerClz(){
+ return action.controller().clz();
+ }
+
+ public Set getTags(Operation apiOperationAnno) {
+ Tag apiAnno = controllerClz().getAnnotation(Tag.class);
+
+ Set actionTags = new HashSet<>();
+
+ actionTags.add(apiAnno.name());
+ actionTags.addAll(Arrays.asList(apiOperationAnno.tags()));
+ actionTags.remove("");
+
+ return actionTags;
+ }
+
+ public boolean isGet() {
+ return routing().method() == MethodType.GET
+ || (action.method().getParamWraps().length == 0 && routing().method() == MethodType.ALL);
+ }
+
+
+ public boolean isAnnotationPresent(Class extends Annotation> annoClz){
+ return action.method().isAnnotationPresent(annoClz);
+ }
+
+ public T getAnnotation(Class annoClz){
+ return action.method().getAnnotation(annoClz);
+ }
+
+
+ public T[] getAnnotationsByType(Class annoClz){
+ return action.method().getMethod().getAnnotationsByType(annoClz);
+ }
+
+ public ActionHolder(Routing routing, Action action){
+ this.routing = routing;
+ this.action =action;
+ }
+
+ @Override
+ public String toString() {
+ return action.fullName();
+ }
+}
diff --git a/solon-projects/solon-base/solon.docs.openapi3/src/main/java/org/noear/solon/docs/openapi3/impl/BuilderHelper.java b/solon-projects/solon-base/solon.docs.openapi3/src/main/java/org/noear/solon/docs/openapi3/impl/BuilderHelper.java
new file mode 100644
index 0000000000..8d7a10fc33
--- /dev/null
+++ b/solon-projects/solon-base/solon.docs.openapi3/src/main/java/org/noear/solon/docs/openapi3/impl/BuilderHelper.java
@@ -0,0 +1,131 @@
+package org.noear.solon.docs.openapi3.impl;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Schema;
+import org.noear.solon.Solon;
+import org.noear.solon.Utils;
+import org.noear.solon.annotation.Mapping;
+import org.noear.solon.core.handle.MethodType;
+import org.noear.solon.docs.ApiEnum;
+import org.noear.solon.docs.DocDocket;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * @author noear
+ * @since 2.4
+ */
+public class BuilderHelper {
+ public static boolean isModel(Class> clz){
+ if(clz.isAnnotationPresent(Schema.class)){
+ return true;
+ }
+
+ if(clz.getName().startsWith("java")){
+ return false;
+ }
+
+ if(clz.isPrimitive()){
+ return false;
+ }
+
+ if(Map.class.isAssignableFrom(clz) || Collection.class.isAssignableFrom(clz)){
+ return false;
+ }
+
+ return true;
+ }
+
+ public static String getModelName(Class> clazz, Type type) {
+ String modelName = clazz.getSimpleName();
+
+ if (type instanceof ParameterizedType) {
+ //支持泛型
+ Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments();
+
+ if (typeArguments != null && typeArguments.length > 0) {
+ StringBuilder buf = new StringBuilder();
+ for (Type v : typeArguments) {
+ if (v instanceof Class>) {
+ buf.append(((Class>) v).getSimpleName()).append(",");
+ }
+
+ if (v instanceof ParameterizedType) {
+ ParameterizedType v2 = (ParameterizedType) v;
+ Type v22 = v2.getRawType();
+
+ if (v22 instanceof Class>) {
+ String name2 = getModelName((Class>) v22, v2);
+ buf.append(name2).append(",");
+ }
+ }
+ }
+
+ if (buf.length() > 0) {
+ buf.setLength(buf.length() - 1);
+
+ modelName = modelName + "«" + buf + "»";
+ }
+ }
+ }
+
+ return modelName;
+ }
+
+ public static String getHttpMethod(ActionHolder actionHolder, Operation apiAction) {
+ if (Utils.isBlank(apiAction.method())) {
+ MethodType methodType = actionHolder.routing().method();
+
+ if (methodType == null) {
+ return ApiEnum.METHOD_GET;
+ } else {
+ if (actionHolder.isGet()) {
+ return ApiEnum.METHOD_GET;
+ }
+
+ if (methodType.ordinal() < MethodType.UNKNOWN.ordinal()) {
+ return methodType.name.toLowerCase();
+ } else {
+ return ApiEnum.METHOD_POST;
+ }
+ }
+ } else {
+ return apiAction.method();
+ }
+ }
+
+ /**
+ * 获取host配置
+ */
+ public static String getHost(DocDocket swaggerDock) {
+ String host = swaggerDock.host();
+ if (Utils.isBlank(host)) {
+ host = "localhost";
+ if (Solon.cfg().serverPort() != 80) {
+ host += ":" + Solon.cfg().serverPort();
+ }
+ }
+
+ return host;
+ }
+
+ /**
+ * 避免ControllerKey 设置前缀后,与swagger basePath 设置导致前端生成2次
+ */
+ public static String getControllerKey(Class> controllerClz) {
+ Mapping mapping = controllerClz.getAnnotation(Mapping.class);
+ if (mapping == null) {
+ return "";
+ }
+
+ String path = Utils.annoAlias(mapping.value(), mapping.path());
+ if (path.startsWith("/")) {
+ return path.substring(1);
+ } else {
+ return path;
+ }
+ }
+}
diff --git a/solon-projects/solon-base/solon.docs.openapi3/src/main/java/org/noear/solon/docs/openapi3/impl/ParamHolder.java b/solon-projects/solon-base/solon.docs.openapi3/src/main/java/org/noear/solon/docs/openapi3/impl/ParamHolder.java
new file mode 100644
index 0000000000..89b376070b
--- /dev/null
+++ b/solon-projects/solon-base/solon.docs.openapi3/src/main/java/org/noear/solon/docs/openapi3/impl/ParamHolder.java
@@ -0,0 +1,209 @@
+package org.noear.solon.docs.openapi3.impl;
+
+import io.swagger.v3.oas.annotations.Parameter;
+import org.noear.solon.Utils;
+import org.noear.solon.core.handle.Context;
+import org.noear.solon.core.handle.SessionState;
+import org.noear.solon.core.handle.UploadedFile;
+import org.noear.solon.core.wrap.ParamWrap;
+import org.noear.solon.docs.ApiEnum;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * @author noear
+ * @since 2.4
+ */
+public class ParamHolder {
+ private ParamWrap param;
+ private Parameter anno;
+
+ public ParamHolder(ParamWrap param){
+ this.param = param;
+ }
+
+ public ParamHolder binding(Parameter anno) {
+ this.anno = anno;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return getName();
+ }
+
+ public ParamWrap getParam() {
+ return param;
+ }
+
+ public Parameter getAnno() {
+ return anno;
+ }
+
+ /**
+ * 名字
+ * */
+ public String getName() {
+ if(param != null){
+ return param.getName();
+ }
+
+ if(anno != null){
+ return anno.name();
+ }
+
+ return null;
+ }
+
+ /**
+ * 描述
+ * */
+ public String getDescription(){
+ if(anno != null){
+ return anno.description();
+ }
+
+ return null;
+ }
+
+ public boolean isMap(){
+ if(param != null){
+ return Map.class.isAssignableFrom(param.getType());
+ }
+
+ return false;
+ }
+
+ public boolean isArray(){
+ if(param != null){
+ return Collection.class.isAssignableFrom(param.getType());
+ }
+
+ return false;
+ }
+
+ /**
+ * 获取数据类型
+ * */
+ public String dataType() {
+ if (param != null) {
+ if (UploadedFile.class.equals(param.getType())) {
+ return ApiEnum.FILE;
+ }
+
+ return param.getType().getSimpleName();
+ }
+
+ String tmp = null;
+// if (anno != null) {
+// tmp = anno.dataType();
+// }
+
+ if (Utils.isBlank(tmp)) {
+ return ApiEnum.STRING;
+ } else {
+ return tmp;
+ }
+ }
+
+ public String paramType(){
+ if(param != null) {
+ if (param.isRequiredBody()) {
+ return ApiEnum.PARAM_TYPE_BODY;
+ }
+ }
+
+ String tmp = null;
+ if (anno != null) {
+ tmp = anno.in().toString();
+ }
+
+ if (Utils.isBlank(tmp)) {
+ return ApiEnum.PARAM_TYPE_QUERY;
+ } else {
+ return tmp;
+ }
+ }
+
+ public boolean allowMultiple() {
+ if (param != null) {
+ return param.getType().isArray() ||
+ Collection.class.isAssignableFrom(param.getType());
+ }
+
+// if (anno != null) {
+// return anno.allowMultiple();
+// }
+
+ return false;
+ }
+
+ public boolean isRequired() {
+ if (param != null) {
+ if (param.isRequiredInput()) {
+ return true;
+ }
+ }
+
+ if (anno != null) {
+ return anno.required();
+ }
+
+ return false;
+ }
+
+ public boolean isRequiredBody(){
+ if (param != null) {
+ return param.isRequiredBody();
+ }
+
+ return false;
+ }
+
+ public boolean isRequiredHeader(){
+ if (param != null) {
+ return param.isRequiredHeader();
+ }
+
+ return false;
+ }
+
+ public boolean isRequiredCookie(){
+ if (param != null) {
+ return param.isRequiredCookie();
+ }
+
+ return false;
+ }
+
+ public boolean isRequiredPath(){
+ if (param != null) {
+ return param.isRequiredPath();
+ }
+
+ return false;
+ }
+
+ public boolean isReadOnly(){
+// if(anno != null){
+// return anno.readOnly();
+// }
+
+ return false;
+ }
+
+ public boolean isIgnore(){
+ if(param !=null){
+ if(Context.class.equals(param.getType())){
+ return true;
+ }
+
+ if(SessionState.class.equals(param.getType())){
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/solon-projects/solon-base/solon.docs/pom.xml b/solon-projects/solon-base/solon.docs/pom.xml
index 783e00fbd9..b5603b7945 100644
--- a/solon-projects/solon-base/solon.docs/pom.xml
+++ b/solon-projects/solon-base/solon.docs/pom.xml
@@ -20,50 +20,6 @@
solon
-
- io.swagger
- swagger-annotations
- ${swagger.version}
-
-
-
- io.swagger
- swagger-models
- ${swagger.version}
-
-
- org.slf4j
- slf4j-api
-
-
- com.fasterxml.jackson.core
- jackson-annotations
-
-
-
-
-
- io.swagger.core.v3
- swagger-annotations
- ${swagger2.version}
-
-
-
- io.swagger.core.v3
- swagger-models
- ${swagger2.version}
-
-
- org.slf4j
- slf4j-api
-
-
- com.fasterxml.jackson.core
- jackson-annotations
-
-
-
-
com.fasterxml.jackson.core
jackson-core
diff --git a/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/DocDocket.java b/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/DocDocket.java
index 0a73cbe1c0..32035d9eef 100644
--- a/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/DocDocket.java
+++ b/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/DocDocket.java
@@ -1,9 +1,5 @@
package org.noear.solon.docs;
-
-import io.swagger.models.auth.ApiKeyAuthDefinition;
-import io.swagger.models.auth.In;
-import io.swagger.models.auth.SecuritySchemeDefinition;
import org.noear.solon.Utils;
import org.noear.solon.docs.models.*;
@@ -32,10 +28,6 @@ public class DocDocket {
private ApiInfo info = new ApiInfo();
private List apis = new ArrayList<>();
- /**
- * 安全定义
- * */
- private Map securityDefinitions = new LinkedHashMap<>();
/**
* 外部文件
* */
@@ -46,14 +38,17 @@ public class DocDocket {
private Map vendorExtensions = new LinkedHashMap<>();
- public DocDocket(DocType docType) {
- this.version = docType.getVersion();
- }
public String version() {
return version;
}
+ public DocDocket version(String version) {
+ this.version = version;
+ return this;
+ }
+
+
public String host() {
return host;
}
@@ -171,24 +166,6 @@ public DocDocket globalResult(Class> clz) {
return this;
}
- public Map securityDefinitions() {
- return securityDefinitions;
- }
-
- public DocDocket securityDefinition(String name, SecuritySchemeDefinition securityDefinition) {
- securityDefinitions.put(name, securityDefinition);
- return this;
- }
-
- public DocDocket securityDefinitionInHeader(String name) {
- securityDefinitions.put(name, new ApiKeyAuthDefinition().in(In.HEADER));
- return this;
- }
-
- public DocDocket securityDefinitionInQuery(String name) {
- securityDefinitions.put(name, new ApiKeyAuthDefinition().in(In.QUERY));
- return this;
- }
public ApiExternalDocs externalDocs() {
return externalDocs;
diff --git a/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiContact.java b/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiContact.java
new file mode 100644
index 0000000000..4173f062b5
--- /dev/null
+++ b/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiContact.java
@@ -0,0 +1,48 @@
+package org.noear.solon.docs.models;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 接口联系信息
+ *
+ * @author noear
+ * @since 2.4
+ */
+public class ApiContact {
+ private String name;
+ private String url;
+ private String email;
+ private Map vendorExtensions = new LinkedHashMap();
+
+ public ApiContact name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public ApiContact url(String url) {
+ this.url = url;
+ return this;
+ }
+
+ public ApiContact email(String email) {
+ this.email = email;
+ return this;
+ }
+
+ public String name(){
+ return this.name;
+ }
+
+ public String url(){
+ return this.url;
+ }
+
+ public String email(){
+ return this.email;
+ }
+
+ public Map vendorExtensions(){
+ return vendorExtensions;
+ }
+}
diff --git a/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiExternalDocs.java b/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiExternalDocs.java
index d522faf22b..7eaabe8c90 100644
--- a/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiExternalDocs.java
+++ b/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiExternalDocs.java
@@ -1,7 +1,10 @@
package org.noear.solon.docs.models;
/**
- * @author noear 2023/5/25 created
+ * 接口扩展文档
+ *
+ * @author noear
+ * @since 2.2
*/
public class ApiExternalDocs {
private String description;
diff --git a/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiGroupResource.java b/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiGroupResource.java
index 67eca477cf..856f2770a5 100644
--- a/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiGroupResource.java
+++ b/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiGroupResource.java
@@ -3,8 +3,10 @@
import java.io.Serializable;
/**
+ * 接口组资源
+ *
* @author noear
- * @since 2.3
+ * @since 2.2
*/
public class ApiGroupResource implements Serializable {
private String name;
diff --git a/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiInfo.java b/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiInfo.java
index 5b9cfdc82f..30bfff8ec7 100644
--- a/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiInfo.java
+++ b/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiInfo.java
@@ -1,19 +1,18 @@
package org.noear.solon.docs.models;
-import io.swagger.models.Contact;
-import io.swagger.models.License;
-
/**
+ * 接口信息
+ *
* @author noear
- * @since 2.3
+ * @since 2.2
*/
public class ApiInfo {
private String description;
private String version;
private String title;
private String termsOfService;
- private Contact contact;
- private License license;
+ private ApiContact contact;
+ private ApiLicense license;
public String description() {
return description;
@@ -51,31 +50,31 @@ public ApiInfo termsOfService(String url) {
return this;
}
- public Contact contact() {
+ public ApiContact contact() {
return contact;
}
- public ApiInfo contact(Contact contact) {
+ public ApiInfo contact(ApiContact contact) {
this.contact = contact;
return this;
}
public ApiInfo contact(String name, String url, String email) {
- this.contact = new Contact().name(name).url(url).email(email);
+ this.contact = new ApiContact().name(name).url(url).email(email);
return this;
}
- public License license() {
+ public ApiLicense license() {
return license;
}
- public ApiInfo license(License license) {
+ public ApiInfo license(ApiLicense license) {
this.license = license;
return this;
}
public ApiInfo license(String name, String url) {
- this.license = new License().name(name).url(url);
+ this.license = new ApiLicense().name(name).url(url);
return this;
}
}
diff --git a/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiLicense.java b/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiLicense.java
new file mode 100644
index 0000000000..7617ff1cfe
--- /dev/null
+++ b/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiLicense.java
@@ -0,0 +1,39 @@
+package org.noear.solon.docs.models;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 接口许可证
+ *
+ * @author noear
+ * @since 2.4
+ */
+public class ApiLicense {
+ private String name;
+ private String url;
+ private Map vendorExtensions = new LinkedHashMap();
+
+ public ApiLicense name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public ApiLicense url(String url) {
+ this.url = url;
+ return this;
+ }
+
+
+ public String name(){
+ return this.name;
+ }
+
+ public String url(){
+ return this.url;
+ }
+
+ public Map vendorExtensions(){
+ return vendorExtensions;
+ }
+}
diff --git a/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiResource.java b/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiResource.java
index e17d48718d..3081d98b8d 100644
--- a/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiResource.java
+++ b/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiResource.java
@@ -7,7 +7,10 @@
import java.util.function.Predicate;
/**
- * Swagger 资源信息
+ * 接口资源信息
+ *
+ * @author noear
+ * @since 2.2
* */
public class ApiResource implements Predicate {
diff --git a/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiScheme.java b/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiScheme.java
index 9df2319f8d..049ae37de4 100644
--- a/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiScheme.java
+++ b/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiScheme.java
@@ -1,8 +1,10 @@
package org.noear.solon.docs.models;
/**
+ * 接口协议架构
+ *
* @author noear
- * @since 2.3
+ * @since 2.2
*/
public enum ApiScheme {
HTTP("http"),
diff --git a/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiVendorExtension.java b/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiVendorExtension.java
index 401d9310fc..d8668f8406 100644
--- a/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiVendorExtension.java
+++ b/solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiVendorExtension.java
@@ -3,6 +3,8 @@
import java.io.Serializable;
/**
+ * 接口供应商扩展
+ *
* @author noear
* @since 2.2
*/
diff --git a/solon-projects/solon-tool/solon-openapi2-knife4j/src/test/java/com/swagger/demo/Config.java b/solon-projects/solon-tool/solon-openapi2-knife4j/src/test/java/com/swagger/demo/Config.java
index babf097393..d9aeeb7bd0 100644
--- a/solon-projects/solon-tool/solon-openapi2-knife4j/src/test/java/com/swagger/demo/Config.java
+++ b/solon-projects/solon-tool/solon-openapi2-knife4j/src/test/java/com/swagger/demo/Config.java
@@ -4,7 +4,6 @@
import com.swagger.demo.model.HttpCodes;
import org.noear.solon.docs.ApiEnum;
-import org.noear.solon.docs.DocType;
import org.noear.solon.docs.models.ApiInfo;
import org.noear.solon.docs.DocDocket;
@@ -25,7 +24,7 @@ public class Config {
public DocDocket adminApi(@Inject("${swagger.adminApi}") DocDocket docket) {
//docket.globalResult(SwaggerRes.class);
docket.globalResponseCodes(new HttpCodes());
- docket.securityDefinitionInHeader("token");
+ //docket.securityDefinitionInHeader("token");
docket.basicAuth(openApiExtensionResolver.getSetting().getBasic());
docket.vendorExtensions(openApiExtensionResolver.buildExtensions());
@@ -37,13 +36,13 @@ public DocDocket adminApi(@Inject("${swagger.adminApi}") DocDocket docket) {
*/
@Bean("appApi")
public DocDocket appApi() {
- return new DocDocket(DocType.SWAGGER_2)
+ return new DocDocket()
.groupName("app端接口")
.schemes(ApiEnum.SCHEMES_HTTP)
.globalResult(Result.class)
.globalResponseInData(true)
- .apis("com.swagger.demo.controller.app")
- .securityDefinitionInHeader("token");
+ .apis("com.swagger.demo.controller.app");
+ //.securityDefinitionInHeader("token");
}
@@ -52,19 +51,19 @@ public DocDocket appApi() {
*/
@Bean("gatewayApi")
public DocDocket gatewayApi() {
- return new DocDocket(DocType.SWAGGER_2)
+ return new DocDocket()
.groupName("gateway端接口")
.schemes(ApiEnum.SCHEMES_HTTP)
.globalResult(Result.class)
.globalResponseInData(true)
- .apis("com.swagger.demo.controller.api2")
- .securityDefinitionInHeader("token");
+ .apis("com.swagger.demo.controller.api2");
+ //.securityDefinitionInHeader("token");
}
// @Bean("appApi")
public DocDocket appApi2() {
- return new DocDocket(DocType.SWAGGER_2)
+ return new DocDocket()
.groupName("app端接口")
.info(new ApiInfo().title("在线文档")
.description("在线API文档")
@@ -74,8 +73,8 @@ public DocDocket appApi2() {
.schemes(ApiEnum.SCHEMES_HTTP)
.globalResponseInData(true)
.globalResult(Result.class)
- .apis("com.swagger.demo.controller.app")
- .securityDefinitionInHeader("token");
+ .apis("com.swagger.demo.controller.app");
+ //.securityDefinitionInHeader("token");
}
}