From c8fab5b3c08e6425452f060a1b022e1802ed7951 Mon Sep 17 00:00:00 2001 From: noear Date: Sun, 30 Jul 2023 23:19:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20solon.docs.openapi2=20?= =?UTF-8?q?=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- UPDATE_LOG.md | 1 + __release/solon-base-bundle/pom.xml | 2 +- solon-parent/pom.xml | 7 +- ...gger2Builder.java => OpenApi2Builder.java} | 51 +- .../solon/docs/openapi2/OpenApi2Utils.java | 2 +- .../openapi2/{ => impl}/ActionHolder.java | 2 +- .../openapi2/{ => impl}/BuilderHelper.java | 2 +- .../docs/openapi2/{ => impl}/ParamHolder.java | 2 +- .../solon-base/solon.docs.openapi3/pom.xml | 45 + .../solon/docs/openapi3/OpenApi3Builder.java | 857 ++++++++++++++++++ .../solon/docs/openapi3/OpenApi3Utils.java | 64 ++ .../docs/openapi3/impl/ActionHolder.java | 76 ++ .../docs/openapi3/impl/BuilderHelper.java | 131 +++ .../solon/docs/openapi3/impl/ParamHolder.java | 209 +++++ solon-projects/solon-base/solon.docs/pom.xml | 44 - .../java/org/noear/solon/docs/DocDocket.java | 35 +- .../noear/solon/docs/models/ApiContact.java | 48 + .../solon/docs/models/ApiExternalDocs.java | 5 +- .../solon/docs/models/ApiGroupResource.java | 4 +- .../org/noear/solon/docs/models/ApiInfo.java | 23 +- .../noear/solon/docs/models/ApiLicense.java | 39 + .../noear/solon/docs/models/ApiResource.java | 5 +- .../noear/solon/docs/models/ApiScheme.java | 4 +- .../solon/docs/models/ApiVendorExtension.java | 2 + .../test/java/com/swagger/demo/Config.java | 21 +- 25 files changed, 1557 insertions(+), 124 deletions(-) rename solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/{Swagger2Builder.java => OpenApi2Builder.java} (94%) rename solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/{ => impl}/ActionHolder.java (97%) rename solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/{ => impl}/BuilderHelper.java (98%) rename solon-projects/solon-base/solon.docs.openapi2/src/main/java/org/noear/solon/docs/openapi2/{ => impl}/ParamHolder.java (98%) create mode 100644 solon-projects/solon-base/solon.docs.openapi3/pom.xml create mode 100644 solon-projects/solon-base/solon.docs.openapi3/src/main/java/org/noear/solon/docs/openapi3/OpenApi3Builder.java create mode 100644 solon-projects/solon-base/solon.docs.openapi3/src/main/java/org/noear/solon/docs/openapi3/OpenApi3Utils.java create mode 100644 solon-projects/solon-base/solon.docs.openapi3/src/main/java/org/noear/solon/docs/openapi3/impl/ActionHolder.java create mode 100644 solon-projects/solon-base/solon.docs.openapi3/src/main/java/org/noear/solon/docs/openapi3/impl/BuilderHelper.java create mode 100644 solon-projects/solon-base/solon.docs.openapi3/src/main/java/org/noear/solon/docs/openapi3/impl/ParamHolder.java create mode 100644 solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiContact.java create mode 100644 solon-projects/solon-base/solon.docs/src/main/java/org/noear/solon/docs/models/ApiLicense.java 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 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"); } }