From 56cb965729964805f3a45def490a943ce6871406 Mon Sep 17 00:00:00 2001
From: AprilWind <2100166581@qq.com>
Date: Thu, 5 Mar 2026 11:57:46 +0800
Subject: [PATCH 1/4] =?UTF-8?q?update=20Sa-Token=20=E6=9D=83=E9=99=90?=
=?UTF-8?q?=E7=A0=81=E5=B1=95=E7=A4=BA=EF=BC=8C=E6=8E=A5=E5=8F=A3=E8=AF=A6?=
=?UTF-8?q?=E6=83=85=E6=98=BE=E7=A4=BA=E6=9D=83=E9=99=90=E7=A0=81=EF=BC=8C?=
=?UTF-8?q?=E6=84=9F=E8=B0=A2nextdoc4j?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ruoyi-common/ruoyi-common-doc/pom.xml | 5 +
.../common/doc/config/SpringDocConfig.java | 15 +-
.../core/enhancer/SaTokenJavadocResolver.java | 200 +++
.../enhancer/SaTokenMetadataResolver.java | 38 +
.../core/model/SaTokenSecurityMetadata.java | 175 ++
.../common/doc/handler/OpenApiHandler.java | 19 +-
.../api/AbstractOpenApiResource.java | 1552 +++++++++++++++++
.../controller/SaTokenTestController.java | 198 +++
8 files changed, 2199 insertions(+), 3 deletions(-)
create mode 100644 ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/enhancer/SaTokenJavadocResolver.java
create mode 100644 ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/enhancer/SaTokenMetadataResolver.java
create mode 100644 ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/model/SaTokenSecurityMetadata.java
create mode 100644 ruoyi-common/ruoyi-common-doc/src/main/java/org/springdoc/api/AbstractOpenApiResource.java
create mode 100644 ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/SaTokenTestController.java
diff --git a/ruoyi-common/ruoyi-common-doc/pom.xml b/ruoyi-common/ruoyi-common-doc/pom.xml
index c6199a17c..77f8d703f 100644
--- a/ruoyi-common/ruoyi-common-doc/pom.xml
+++ b/ruoyi-common/ruoyi-common-doc/pom.xml
@@ -21,6 +21,11 @@
ruoyi-common-core
+
+ org.dromara
+ ruoyi-common-satoken
+
+
org.springdoc
springdoc-openapi-starter-webmvc-api
diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocConfig.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocConfig.java
index 35b6ce9ea..b22d91139 100644
--- a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocConfig.java
+++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocConfig.java
@@ -7,6 +7,8 @@ import io.swagger.v3.oas.models.security.SecurityRequirement;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.doc.config.properties.SpringDocProperties;
+import org.dromara.common.doc.core.enhancer.SaTokenJavadocResolver;
+import org.dromara.common.doc.core.enhancer.SaTokenMetadataResolver;
import org.dromara.common.doc.handler.OpenApiHandler;
import org.springdoc.core.configuration.SpringDocConfiguration;
import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
@@ -84,8 +86,9 @@ public class SpringDocConfig {
SecurityService securityParser,
SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils,
Optional> openApiBuilderCustomisers,
- Optional> serverBaseUrlCustomisers, Optional javadocProvider) {
- return new OpenApiHandler(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomisers, serverBaseUrlCustomisers, javadocProvider);
+ Optional> serverBaseUrlCustomisers, Optional javadocProvider,
+ SaTokenMetadataResolver saTokenMetadataResolver) {
+ return new OpenApiHandler(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomisers, serverBaseUrlCustomisers, javadocProvider, saTokenMetadataResolver);
}
/**
@@ -112,6 +115,14 @@ public class SpringDocConfig {
};
}
+ /**
+ * 注册JavaDoc权限解析器
+ */
+ @Bean
+ public SaTokenMetadataResolver saTokenJavadocResolver() {
+ return new SaTokenJavadocResolver();
+ }
+
/**
* 单独使用一个类便于判断 解决springdoc路径拼接重复问题
*
diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/enhancer/SaTokenJavadocResolver.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/enhancer/SaTokenJavadocResolver.java
new file mode 100644
index 000000000..426f26a1e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/enhancer/SaTokenJavadocResolver.java
@@ -0,0 +1,200 @@
+package org.dromara.common.doc.core.enhancer;
+
+import cn.dev33.satoken.annotation.SaCheckLogin;
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import cn.dev33.satoken.annotation.SaCheckRole;
+import cn.dev33.satoken.annotation.SaIgnore;
+import io.swagger.v3.oas.models.Operation;
+import org.dromara.common.doc.core.model.SaTokenSecurityMetadata;
+import org.springframework.web.method.HandlerMethod;
+
+import java.lang.annotation.Annotation;
+
+/**
+ * 基于JavaDoc的SaToken权限解析器
+ *
+ * @author echo
+ */
+public class SaTokenJavadocResolver implements SaTokenMetadataResolver {
+
+ public static final Class SA_CHECK_ROLE_CLASS = SaCheckRole.class;
+ public static final Class SA_CHECK_PERMISSION_CLASS = SaCheckPermission.class;
+ public static final Class SA_IGNORE_CLASS = SaIgnore.class;
+ public static final Class SA_CHECK_LOGIN = SaCheckLogin.class;
+
+ /**
+ * 核心解析方法
+ */
+ @Override
+ public void resolve(HandlerMethod handlerMethod, Operation operation, SaTokenSecurityMetadata metadata) {
+ // 检查是否忽略校验
+ if (isIgnore(handlerMethod)) {
+ metadata.setIgnore(true);
+ return;
+ }
+
+ // 解析权限校验
+ resolvePermissionCheck(handlerMethod, metadata);
+
+ // 解析角色校验
+ resolveRoleCheck(handlerMethod, metadata);
+ }
+
+ /**
+ * 解析器优先级
+ */
+ @Override
+ public int getOrder() {
+ return 100;
+ }
+
+ /**
+ * 判断是否支持当前HandlerMethod
+ */
+ @Override
+ public boolean supports(HandlerMethod handlerMethod) {
+ return hasAnnotation(handlerMethod
+ .getMethodAnnotation(SA_CHECK_PERMISSION_CLASS)) || hasAnnotation(handlerMethod
+ .getMethodAnnotation(SA_CHECK_ROLE_CLASS)) || hasAnnotation(handlerMethod
+ .getMethodAnnotation(SA_IGNORE_CLASS)) || hasAnnotation(handlerMethod
+ .getBeanType()
+ .getAnnotation(SA_CHECK_PERMISSION_CLASS)) || hasAnnotation(handlerMethod
+ .getBeanType()
+ .getAnnotation(SA_CHECK_ROLE_CLASS)) || hasAnnotation(handlerMethod
+ .getBeanType()
+ .getAnnotation(SA_IGNORE_CLASS));
+ }
+
+ @Override
+ public String getName() {
+ return "SaTokenJavadocResolver";
+ }
+
+ /**
+ * 检查是否忽略校验
+ */
+ private boolean isIgnore(HandlerMethod handlerMethod) {
+ // 检查方法上的注解
+ if (hasAnnotation(handlerMethod.getMethodAnnotation(SA_IGNORE_CLASS))) {
+ return true;
+ }
+ // 检查类上的注解
+ return hasAnnotation(handlerMethod.getBeanType().getAnnotation(SA_IGNORE_CLASS));
+ }
+
+ /**
+ * 解析权限校验
+ */
+ private void resolvePermissionCheck(HandlerMethod handlerMethod, SaTokenSecurityMetadata metadata) {
+ // 获取方法上的注解
+ Annotation methodAnnotation = handlerMethod
+ .getMethodAnnotation(SA_CHECK_PERMISSION_CLASS);
+ // 获取类上的注解
+ Annotation classAnnotation = handlerMethod.getBeanType()
+ .getAnnotation(SA_CHECK_PERMISSION_CLASS);
+
+ // 解析权限信息
+ if (hasAnnotation(methodAnnotation)) {
+ resolvePermissionAnnotation(metadata, methodAnnotation);
+ }
+ if (hasAnnotation(classAnnotation)) {
+ resolvePermissionAnnotation(metadata, classAnnotation);
+ }
+ }
+
+ /**
+ * 解析权限注解
+ */
+ private void resolvePermissionAnnotation(SaTokenSecurityMetadata metadata, Annotation annotation) {
+ try {
+ // 反射获取注解属性
+ Object value = getAnnotationValue(annotation, "value");
+ Object mode = getAnnotationValue(annotation, "mode");
+ Object type = getAnnotationValue(annotation, "type");
+ Object orRole = getAnnotationValue(annotation, "orRole");
+
+ String[] values = convertToStringArray(value);
+ String modeStr = mode != null ? mode.toString() : "AND";
+ String typeStr = type != null ? type.toString() : "";
+ String[] orRoles = convertToStringArray(orRole);
+
+ metadata.addPermission(values, modeStr, typeStr, orRoles);
+ } catch (Exception e) {
+ // 忽略解析错误
+ }
+ }
+
+ /**
+ * 解析角色校验
+ */
+ private void resolveRoleCheck(HandlerMethod handlerMethod, SaTokenSecurityMetadata metadata) {
+ // 获取方法上的注解
+ Annotation methodAnnotation = handlerMethod.getMethodAnnotation(SA_CHECK_ROLE_CLASS);
+ // 获取类上的注解
+ Annotation classAnnotation = handlerMethod.getBeanType()
+ .getAnnotation(SA_CHECK_ROLE_CLASS);
+
+ // 解析角色信息
+ if (hasAnnotation(methodAnnotation)) {
+ resolveRoleAnnotation(metadata, methodAnnotation);
+ }
+ if (hasAnnotation(classAnnotation)) {
+ resolveRoleAnnotation(metadata, classAnnotation);
+ }
+ }
+
+ /**
+ * 解析角色注解
+ */
+ private void resolveRoleAnnotation(SaTokenSecurityMetadata metadata, Annotation annotation) {
+ try {
+ // 反射获取注解属性
+ Object value = getAnnotationValue(annotation, "value");
+ Object mode = getAnnotationValue(annotation, "mode");
+ Object type = getAnnotationValue(annotation, "type");
+
+ String[] values = convertToStringArray(value);
+ String modeStr = mode != null ? mode.toString() : "AND";
+ String typeStr = type != null ? type.toString() : "";
+
+ metadata.addRole(values, modeStr, typeStr);
+ } catch (Exception e) {
+ // 忽略解析错误
+ }
+ }
+
+ /**
+ * 检查注解是否存在
+ */
+ private boolean hasAnnotation(Annotation annotation) {
+ return annotation != null;
+ }
+
+ /**
+ * 获取注解属性值
+ */
+ private Object getAnnotationValue(Annotation annotation, String attributeName) {
+ try {
+ return annotation.annotationType().getMethod(attributeName).invoke(annotation);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * 转换为字符串数组
+ */
+ private String[] convertToStringArray(Object value) {
+ if (value == null) {
+ return new String[0];
+ }
+ if (value instanceof String[]) {
+ return (String[])value;
+ }
+ if (value instanceof String) {
+ return new String[] {(String)value};
+ }
+ return new String[0];
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/enhancer/SaTokenMetadataResolver.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/enhancer/SaTokenMetadataResolver.java
new file mode 100644
index 000000000..0b42b23c7
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/enhancer/SaTokenMetadataResolver.java
@@ -0,0 +1,38 @@
+package org.dromara.common.doc.core.enhancer;
+
+import io.swagger.v3.oas.models.Operation;
+import org.dromara.common.doc.core.model.SaTokenSecurityMetadata;
+import org.springframework.web.method.HandlerMethod;
+
+/**
+ * 权限元数据解析器接口
+ *
+ * @author echo
+ */
+public interface SaTokenMetadataResolver {
+
+ /**
+ * 解析权限元数据
+ */
+ void resolve(HandlerMethod handlerMethod, Operation operation, SaTokenSecurityMetadata metadata);
+
+ /**
+ * 获取解析器优先级
+ */
+ int getOrder();
+
+ /**
+ * 判断是否支持当前HandlerMethod
+ */
+ boolean supports(HandlerMethod handlerMethod);
+
+ /**
+ * 获取解析器的名称
+ *
+ * @return 解析器名称
+ */
+ default String getName() {
+ return this.getClass().getSimpleName();
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/model/SaTokenSecurityMetadata.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/model/SaTokenSecurityMetadata.java
new file mode 100644
index 000000000..e0782a20c
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/model/SaTokenSecurityMetadata.java
@@ -0,0 +1,175 @@
+package org.dromara.common.doc.core.model;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 存储权限框架注解解析后的权限和角色信息
+ *
+ * @author AprilWind
+ */
+@Data
+@JsonInclude(Include.NON_EMPTY)
+public class SaTokenSecurityMetadata {
+
+ /**
+ * 权限校验信息列表(对应 @SaCheckPermission 注解)
+ */
+ private List permissions = new ArrayList<>();
+
+ /**
+ * 角色校验信息列表(对应 @SaCheckRole 注解)
+ */
+ private List roles = new ArrayList<>();
+
+ /**
+ * 是否忽略校验(对应 @SaIgnore 注解)
+ */
+ private boolean ignore = false;
+
+ /**
+ * 添加权限信息
+ *
+ * @param values 权限值数组
+ * @param mode 校验模式(AND/OR)
+ * @param type 权限类型
+ * @param orRoles 或角色数组
+ */
+ public void addPermission(String[] values, String mode, String type, String[] orRoles) {
+ if (values != null && values.length > 0) {
+ AuthInfo authInfo = new AuthInfo();
+ authInfo.setValues(values);
+ authInfo.setMode(mode);
+ authInfo.setType(type);
+ if (orRoles != null && orRoles.length > 0) {
+ authInfo.setOrValues(orRoles);
+ authInfo.setOrType("role");
+ }
+ this.permissions.add(authInfo);
+ }
+ }
+
+ /**
+ * 添加角色信息
+ *
+ * @param values 角色值数组
+ * @param mode 校验模式(AND/OR)
+ * @param type 角色类型
+ */
+ public void addRole(String[] values, String mode, String type) {
+ if (values != null && values.length > 0) {
+ AuthInfo authInfo = new AuthInfo();
+ authInfo.setValues(values);
+ authInfo.setMode(mode);
+ authInfo.setType(type);
+ this.roles.add(authInfo);
+ }
+ }
+
+ /**
+ * 生成 Markdown 结构的权限说明
+ *
+ * @return Markdown 文本
+ */
+ public String toMarkdownString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("
访问权限
");
+
+ if (ignore) {
+ sb.append("> **权限策略**:忽略权限检查
");
+ return sb.toString();
+ }
+
+ if (!ignore && permissions.isEmpty() && roles.isEmpty()){
+ sb.append("> **权限策略**:需要登录
");
+ return sb.toString();
+ }
+
+ if (!permissions.isEmpty()) {
+ sb.append("**权限校验:**
");
+
+ permissions.forEach(p -> {
+ String permTags = Arrays.stream(p.getValues())
+ .map(v -> "`" + v + "`")
+ .collect(Collectors.joining(p.getModeSymbol()));
+
+ sb.append("- ").append(permTags).append("
");
+
+ if (p.getOrValues() != null && p.getOrValues().length > 0) {
+ String orTags = Arrays.stream(p.getOrValues())
+ .map(v -> "`" + v + "`")
+ .collect(Collectors.joining(p.getModeSymbol()));
+ sb.append(" - 或角色:").append(orTags).append("
");
+ }
+ });
+
+ sb.append("
");
+ }
+
+ if (!roles.isEmpty()) {
+ sb.append("**角色校验:**
");
+
+ roles.forEach(r -> {
+
+ String roleTags = Arrays.stream(r.getValues())
+ .map(v -> "`" + v + "`")
+ .collect(Collectors.joining(r.getModeSymbol()));
+
+ sb.append("- ").append(roleTags).append("
");
+ });
+ }
+
+ return sb.toString().trim();
+ }
+
+ /**
+ * 认证信息
+ */
+ @Data
+ @JsonInclude(Include.NON_EMPTY)
+ public static class AuthInfo {
+
+ /**
+ * 权限或角色值数组
+ */
+ private String[] values;
+
+ /**
+ * 校验模式(AND/OR)
+ */
+ private String mode;
+
+ /**
+ * 类型说明
+ */
+ private String type;
+
+ /**
+ * 或权限/角色值数组(用于权限校验时的或角色校验)
+ */
+ private String[] orValues;
+
+ /**
+ * 或值的类型(role/permission)
+ */
+ private String orType;
+
+ /**
+ * 重写mode的获取方法,返回符号而非文字
+ * @return AND→&,OR→|,默认→&
+ */
+ public String getModeSymbol() {
+ if (mode == null) {
+ return " & "; // 默认AND,返回&
+ }
+ return "AND".equalsIgnoreCase(mode) ? " & " : " | ";
+ }
+
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/handler/OpenApiHandler.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/handler/OpenApiHandler.java
index 56b73694d..2add6d290 100644
--- a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/handler/OpenApiHandler.java
+++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/handler/OpenApiHandler.java
@@ -11,7 +11,10 @@ import io.swagger.v3.oas.models.Paths;
import io.swagger.v3.oas.models.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
+import org.dromara.common.core.utils.ObjectUtils;
import org.dromara.common.core.utils.StreamUtils;
+import org.dromara.common.doc.core.enhancer.SaTokenMetadataResolver;
+import org.dromara.common.doc.core.model.SaTokenSecurityMetadata;
import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
import org.springdoc.core.properties.SpringDocConfigProperties;
@@ -83,6 +86,11 @@ public class OpenApiHandler extends OpenAPIService {
*/
private final PropertyResolverUtils propertyResolverUtils;
+ /**
+ * 权限元数据解析器接口
+ */
+ private final SaTokenMetadataResolver saTokenJavadocResolver;
+
/**
* The javadoc provider.
*/
@@ -123,7 +131,8 @@ public class OpenApiHandler extends OpenAPIService {
SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils,
Optional> openApiBuilderCustomizers,
Optional> serverBaseUrlCustomizers,
- Optional javadocProvider) {
+ Optional javadocProvider,
+ SaTokenMetadataResolver saTokenJavadocResolver) {
super(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomizers, serverBaseUrlCustomizers, javadocProvider);
if (openAPI.isPresent()) {
this.openAPI = openAPI.get();
@@ -140,6 +149,7 @@ public class OpenApiHandler extends OpenAPIService {
this.openApiBuilderCustomisers = openApiBuilderCustomizers;
this.serverBaseUrlCustomizers = serverBaseUrlCustomizers;
this.javadocProvider = javadocProvider;
+ this.saTokenJavadocResolver = saTokenJavadocResolver;
if (springDocConfigProperties.isUseFqn())
TypeNameResolver.std.setUseFqn(true);
}
@@ -220,6 +230,13 @@ public class OpenApiHandler extends OpenAPIService {
securityParser.buildSecurityRequirement(securityRequirements, operation);
}
+ // 调用SaToken解析器提取JavaDoc中的权限信息
+ if (saTokenJavadocResolver.supports(handlerMethod)) {
+ SaTokenSecurityMetadata metadata = new SaTokenSecurityMetadata();
+ saTokenJavadocResolver.resolve(handlerMethod, operation, metadata);
+ operation.setDescription(metadata.toMarkdownString());
+ }
+
return operation;
}
diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/springdoc/api/AbstractOpenApiResource.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/springdoc/api/AbstractOpenApiResource.java
new file mode 100644
index 000000000..781824efa
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/springdoc/api/AbstractOpenApiResource.java
@@ -0,0 +1,1552 @@
+/*
+ *
+ * *
+ * * *
+ * * * *
+ * * * * *
+ * * * * * * Copyright 2019-2025 the original author or authors.
+ * * * * * *
+ * * * * * * Licensed under the Apache License, Version 2.0 (the "License");
+ * * * * * * you may not use this file except in compliance with the License.
+ * * * * * * You may obtain a copy of the License at
+ * * * * * *
+ * * * * * * https://www.apache.org/licenses/LICENSE-2.0
+ * * * * * *
+ * * * * * * Unless required by applicable law or agreed to in writing, software
+ * * * * * * distributed under the License is distributed on an "AS IS" BASIS,
+ * * * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * * * * * * See the License for the specific language governing permissions and
+ * * * * * * limitations under the License.
+ * * * * *
+ * * * *
+ * * *
+ * *
+ *
+ */
+
+package org.springdoc.api;
+
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Method;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.Executors;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.annotation.JsonView;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature;
+import io.swagger.v3.core.filter.SpecFilter;
+import io.swagger.v3.core.util.ReflectionUtils;
+import io.swagger.v3.oas.annotations.Hidden;
+import io.swagger.v3.oas.annotations.Webhook;
+import io.swagger.v3.oas.annotations.Webhooks;
+import io.swagger.v3.oas.annotations.callbacks.Callback;
+import io.swagger.v3.oas.annotations.enums.ParameterIn;
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.Operation;
+import io.swagger.v3.oas.models.PathItem;
+import io.swagger.v3.oas.models.PathItem.HttpMethod;
+import io.swagger.v3.oas.models.Paths;
+import io.swagger.v3.oas.models.SpecVersion;
+import io.swagger.v3.oas.models.media.Schema;
+import io.swagger.v3.oas.models.media.StringSchema;
+import io.swagger.v3.oas.models.parameters.Parameter;
+import io.swagger.v3.oas.models.responses.ApiResponses;
+import io.swagger.v3.oas.models.servers.Server;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springdoc.core.annotations.RouterOperations;
+import org.springdoc.core.customizers.DataRestRouterOperationCustomizer;
+import org.springdoc.core.customizers.GlobalOperationComponentsCustomizer;
+import org.springdoc.core.customizers.OpenApiLocaleCustomizer;
+import org.springdoc.core.customizers.OperationCustomizer;
+import org.springdoc.core.customizers.RouterOperationCustomizer;
+import org.springdoc.core.customizers.SpringDocCustomizers;
+import org.springdoc.core.fn.AbstractRouterFunctionVisitor;
+import org.springdoc.core.fn.RouterFunctionData;
+import org.springdoc.core.fn.RouterOperation;
+import org.springdoc.core.models.MethodAttributes;
+import org.springdoc.core.properties.SpringDocConfigProperties;
+import org.springdoc.core.properties.SpringDocConfigProperties.ApiDocs.OpenApiVersion;
+import org.springdoc.core.properties.SpringDocConfigProperties.GroupConfig;
+import org.springdoc.core.providers.ActuatorProvider;
+import org.springdoc.core.providers.CloudFunctionProvider;
+import org.springdoc.core.providers.JavadocProvider;
+import org.springdoc.core.providers.ObjectMapperProvider;
+import org.springdoc.core.providers.SpringDocProviders;
+import org.springdoc.core.service.AbstractRequestService;
+import org.springdoc.core.service.GenericParameterService;
+import org.springdoc.core.service.GenericResponseService;
+import org.springdoc.core.service.OpenAPIService;
+import org.springdoc.core.service.OperationService;
+import org.springdoc.core.utils.PropertyResolverUtils;
+import org.springdoc.core.utils.SpringDocAnnotationsUtils;
+import org.springdoc.core.utils.SpringDocUtils;
+
+import org.springframework.aop.support.AopUtils;
+import org.springframework.beans.factory.ObjectFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.annotation.AnnotatedElementUtils;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.env.Environment;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.method.HandlerMethod;
+
+import static org.springdoc.core.converters.SchemaPropertyDeprecatingConverter.isDeprecated;
+import static org.springdoc.core.utils.Constants.ACTUATOR_DEFAULT_GROUP;
+import static org.springdoc.core.utils.Constants.DOT;
+import static org.springdoc.core.utils.Constants.OPERATION_ATTRIBUTE;
+import static org.springdoc.core.utils.Constants.SPRING_MVC_SERVLET_PATH;
+import static org.springdoc.core.utils.SpringDocUtils.cloneViaJson;
+import static org.springframework.util.AntPathMatcher.DEFAULT_PATH_SEPARATOR;
+
+/**
+ * The type Abstract open api resource.
+ *
+ * @author bnasslahsen
+ * @author kevinraddatz
+ * @author hyeonisism
+ * @author doljae
+ * @author zdary
+ * @author Haotian Zhang
+ */
+public abstract class AbstractOpenApiResource extends SpecFilter {
+
+ /**
+ * The constant LOGGER.
+ */
+ private static final Logger LOGGER = LoggerFactory.getLogger(AbstractOpenApiResource.class);
+
+ /**
+ * The constant ADDITIONAL_REST_CONTROLLERS.
+ */
+ private static final List> ADDITIONAL_REST_CONTROLLERS = Collections.synchronizedList(new ArrayList<>());
+
+ /**
+ * The constant HIDDEN_REST_CONTROLLERS.
+ */
+ private static final List> HIDDEN_REST_CONTROLLERS = Collections.synchronizedList(new ArrayList<>());
+
+ /**
+ * The constant MODEL_AND_VIEW_CLASS.
+ */
+ private static Class> modelAndViewClass;
+
+ /**
+ * The Spring doc config properties.
+ */
+ protected final SpringDocConfigProperties springDocConfigProperties;
+
+ /**
+ * The Group name.
+ */
+ protected final String groupName;
+
+ /**
+ * The Spring doc providers.
+ */
+ protected final SpringDocProviders springDocProviders;
+
+ /**
+ * The Spring doc customizers.
+ */
+ protected final SpringDocCustomizers springDocCustomizers;
+
+ /**
+ * The open api builder object factory.
+ */
+ private final ObjectFactory openAPIBuilderObjectFactory;
+
+ /**
+ * The Request builder.
+ */
+ private final AbstractRequestService requestBuilder;
+
+ /**
+ * The Response builder.
+ */
+ private final GenericResponseService responseBuilder;
+
+ /**
+ * The Operation parser.
+ */
+ private final OperationService operationParser;
+
+ /**
+ * The Ant path matcher.
+ */
+ private final AntPathMatcher antPathMatcher = new AntPathMatcher();
+
+ /**
+ * The Reentrant lock.
+ */
+ private final Lock reentrantLock = new ReentrantLock();
+
+ /**
+ * The Path pattern.
+ */
+ private final Pattern pathPattern = Pattern.compile("\\{(.*?)}");
+
+ /**
+ * The Open api builder.
+ */
+ protected OpenAPIService openAPIService;
+
+
+ /**
+ * Instantiates a new Abstract open api resource.
+ *
+ * @param groupName the group name
+ * @param openAPIBuilderObjectFactory the open api builder object factory
+ * @param requestBuilder the request builder
+ * @param responseBuilder the response builder
+ * @param operationParser the operation parser
+ * @param springDocConfigProperties the spring doc config properties
+ * @param springDocProviders the spring doc providers
+ * @param springDocCustomizers the spring doc customizers
+ */
+ protected AbstractOpenApiResource(String groupName, ObjectFactory openAPIBuilderObjectFactory,
+ AbstractRequestService requestBuilder,
+ GenericResponseService responseBuilder, OperationService operationParser,
+ SpringDocConfigProperties springDocConfigProperties, SpringDocProviders springDocProviders, SpringDocCustomizers springDocCustomizers) {
+ super();
+ this.groupName = Objects.requireNonNull(groupName, "groupName");
+ this.openAPIBuilderObjectFactory = openAPIBuilderObjectFactory;
+ this.openAPIService = openAPIBuilderObjectFactory.getObject();
+ this.requestBuilder = requestBuilder;
+ this.responseBuilder = responseBuilder;
+ this.operationParser = operationParser;
+ this.springDocProviders = springDocProviders;
+ this.springDocCustomizers = springDocCustomizers;
+ this.springDocConfigProperties = springDocConfigProperties;
+ if (springDocConfigProperties.isPreLoadingEnabled()) {
+ if (CollectionUtils.isEmpty(springDocConfigProperties.getPreLoadingLocales())) {
+ Executors.newSingleThreadExecutor().execute(this::getOpenApi);
+ }
+ else {
+ for (String locale : springDocConfigProperties.getPreLoadingLocales()) {
+ Executors.newSingleThreadExecutor().execute(() -> this.getOpenApi(null, Locale.forLanguageTag(locale)));
+ }
+ }
+ }
+ }
+
+ /**
+ * Add rest controllers.
+ *
+ * @param classes the classes
+ */
+ public static void addRestControllers(Class>... classes) {
+ ADDITIONAL_REST_CONTROLLERS.addAll(Arrays.asList(classes));
+ }
+
+ /**
+ * Add hidden rest controllers.
+ *
+ * @param classes the classes
+ */
+ public static void addHiddenRestControllers(Class>... classes) {
+ HIDDEN_REST_CONTROLLERS.addAll(Arrays.asList(classes));
+ }
+
+ /**
+ * Add hidden rest controllers.
+ *
+ * @param classes the classes
+ */
+ public static void addHiddenRestControllers(String... classes) {
+ Set> hiddenClasses = new HashSet<>();
+ for (String aClass : classes) {
+ try {
+ hiddenClasses.add(Class.forName(aClass));
+ }
+ catch (ClassNotFoundException e) {
+ LOGGER.warn("The following class doesn't exist and cannot be hidden: {}", aClass);
+ }
+ }
+ HIDDEN_REST_CONTROLLERS.addAll(hiddenClasses);
+ }
+
+ /**
+ * Contains response body boolean.
+ *
+ * @param handlerMethod the handler method
+ * @return the boolean
+ */
+ public static boolean containsResponseBody(HandlerMethod handlerMethod) {
+ ResponseBody responseBodyAnnotation = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), ResponseBody.class);
+ if (responseBodyAnnotation == null)
+ responseBodyAnnotation = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), ResponseBody.class);
+ return responseBodyAnnotation != null;
+ }
+
+ /**
+ * Is hidden rest controllers boolean.
+ *
+ * @param rawClass the raw class
+ * @return the boolean
+ */
+ public static boolean isHiddenRestControllers(Class> rawClass) {
+ return HIDDEN_REST_CONTROLLERS.stream().anyMatch(clazz -> ClassUtils.getUserClass(clazz).isAssignableFrom(rawClass));
+ }
+
+ /**
+ * Sets model and view class.
+ *
+ * @param modelAndViewClass the model and view class
+ */
+ public static void setModelAndViewClass(Class> modelAndViewClass) {
+ AbstractOpenApiResource.modelAndViewClass = modelAndViewClass;
+ }
+
+ /**
+ * Gets open api.
+ */
+ private void getOpenApi() {
+ this.getOpenApi(null, Locale.getDefault());
+ }
+
+ /**
+ * Gets open api.
+ *
+ * @param locale the locale
+ * @return the open api
+ */
+ protected OpenAPI getOpenApi(Locale locale) {
+ return this.getOpenApi(null, locale);
+ }
+
+ /**
+ * Gets open api.
+ *
+ * @param serverBaseUrl the server base url
+ * @param locale the locale
+ * @return the open api
+ */
+ protected OpenAPI getOpenApi(String serverBaseUrl, Locale locale) {
+ this.reentrantLock.lock();
+ try {
+ final OpenAPI openAPI;
+ final Locale finalLocale = selectLocale(locale);
+ if (openAPIService.getCachedOpenAPI(finalLocale) == null || springDocConfigProperties.isCacheDisabled()) {
+ Instant start = Instant.now();
+ openAPI = openAPIService.build(finalLocale);
+ Map mappingsMap = openAPIService.getMappingsMap().entrySet().stream()
+ .filter(controller -> (AnnotationUtils.findAnnotation(controller.getValue().getClass(),
+ Hidden.class) == null))
+ .filter(controller -> !isHiddenRestControllers(controller.getValue().getClass()))
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a1, a2) -> a1));
+
+ Map findControllerAdvice = openAPIService.getControllerAdviceMap();
+ if (OpenApiVersion.OPENAPI_3_1 == springDocConfigProperties.getApiDocs().getVersion()) {
+ openAPI.openapi(OpenApiVersion.OPENAPI_3_1.getVersion());
+ openAPI.specVersion(SpecVersion.V31);
+ calculateWebhooks(openAPI, locale);
+ }
+ if (springDocConfigProperties.isDefaultOverrideWithGenericResponse()) {
+ if (!CollectionUtils.isEmpty(mappingsMap))
+ findControllerAdvice.putAll(mappingsMap);
+ responseBuilder.buildGenericResponse(openAPI.getComponents(), findControllerAdvice, finalLocale);
+ }
+ getPaths(mappingsMap, finalLocale, openAPI);
+
+ if (springDocConfigProperties.isTrimKotlinIndent())
+ this.trimIndent(openAPI);
+
+ Optional cloudFunctionProviderOptional = springDocProviders.getSpringCloudFunctionProvider();
+ cloudFunctionProviderOptional.ifPresent(cloudFunctionProvider -> {
+ List routerOperationList = cloudFunctionProvider.getRouterOperations(openAPI);
+ if (!CollectionUtils.isEmpty(routerOperationList))
+ this.calculatePath(routerOperationList, locale, openAPI);
+ }
+ );
+ if (!CollectionUtils.isEmpty(openAPI.getServers()))
+ openAPIService.setServersPresent(true);
+ else
+ openAPIService.setServersPresent(false);
+ openAPIService.updateServers(serverBaseUrl, openAPI);
+
+ if (springDocConfigProperties.isRemoveBrokenReferenceDefinitions())
+ this.removeBrokenReferenceDefinitions(openAPI);
+
+ // run the optional customizers
+ List servers = openAPI.getServers();
+ List serversCopy = cloneViaJson(servers, new TypeReference>() {}, springDocProviders.jsonMapper());
+
+ openAPIService.getContext().getBeansOfType(OpenApiLocaleCustomizer.class).values().forEach(openApiLocaleCustomizer -> openApiLocaleCustomizer.customise(openAPI, finalLocale));
+ springDocCustomizers.getOpenApiCustomizers().ifPresent(apiCustomizers -> apiCustomizers.forEach(openApiCustomizer -> openApiCustomizer.customise(openAPI)));
+ if (!CollectionUtils.isEmpty(openAPI.getServers()) && !openAPI.getServers().equals(serversCopy))
+ openAPIService.setServersPresent(true);
+
+ openAPIService.setCachedOpenAPI(openAPI, finalLocale);
+
+ LOGGER.info("Init duration for springdoc-openapi is: {} ms",
+ Duration.between(start, Instant.now()).toMillis());
+ }
+ else {
+ LOGGER.debug("Fetching openApi document from cache");
+ openAPI = openAPIService.getCachedOpenAPI(finalLocale);
+ openAPIService.updateServers(serverBaseUrl, openAPI);
+ }
+ return openAPI;
+ }
+ finally {
+ SpringDocAnnotationsUtils.clearCache(operationParser.getJavadocProvider());
+ this.reentrantLock.unlock();
+ }
+ }
+
+ private Locale selectLocale(Locale inputLocale) {
+ List allowedLocales = springDocConfigProperties.getAllowedLocales();
+ if (!CollectionUtils.isEmpty(allowedLocales)) {
+ Locale bestMatchingAllowedLocale = Locale.lookup(
+ Locale.LanguageRange.parse(inputLocale.toLanguageTag()),
+ allowedLocales.stream().map(Locale::forLanguageTag).toList()
+ );
+
+ return bestMatchingAllowedLocale == null ? Locale.forLanguageTag(allowedLocales.get(0)) : bestMatchingAllowedLocale;
+ }
+
+ return inputLocale == null ? Locale.getDefault() : inputLocale;
+ }
+
+ /**
+ * Indents are removed for properties that are mainly used as “explanations” using Open API.
+ *
+ * @param openAPI the open api
+ */
+ private void trimIndent(OpenAPI openAPI) {
+ trimComponents(openAPI);
+ trimPaths(openAPI);
+ }
+
+ /**
+ * Trim the indent for descriptions in the 'components' of open api.
+ *
+ * @param openAPI the open api
+ */
+ private void trimComponents(OpenAPI openAPI) {
+ final PropertyResolverUtils propertyResolverUtils = operationParser.getPropertyResolverUtils();
+ if (openAPI.getComponents() == null || openAPI.getComponents().getSchemas() == null) {
+ return;
+ }
+ for (Schema> schema : openAPI.getComponents().getSchemas().values()) {
+ schema.description(propertyResolverUtils.trimIndent(schema.getDescription()));
+ if (schema.getProperties() == null) {
+ continue;
+ }
+ for (Object prop : schema.getProperties().values()) {
+ if (prop instanceof Schema> schemaProp) {
+ schemaProp.setDescription(propertyResolverUtils.trimIndent(schemaProp.getDescription()));
+ }
+ }
+ }
+ }
+
+ /**
+ * Trim the indent for descriptions in the 'paths' of open api.
+ *
+ * @param openAPI the open api
+ */
+ private void trimPaths(OpenAPI openAPI) {
+ final PropertyResolverUtils propertyResolverUtils = operationParser.getPropertyResolverUtils();
+ if (openAPI.getPaths() == null) {
+ return;
+ }
+ for (PathItem value : openAPI.getPaths().values()) {
+ value.setDescription(propertyResolverUtils.trimIndent(value.getDescription()));
+ trimIndentOperation(value.getGet());
+ trimIndentOperation(value.getPut());
+ trimIndentOperation(value.getPost());
+ trimIndentOperation(value.getDelete());
+ trimIndentOperation(value.getOptions());
+ trimIndentOperation(value.getHead());
+ trimIndentOperation(value.getPatch());
+ trimIndentOperation(value.getTrace());
+ }
+ }
+
+ /**
+ * Trim the indent for 'operation'
+ *
+ * @param operation the operation
+ */
+ private void trimIndentOperation(Operation operation) {
+ final PropertyResolverUtils propertyResolverUtils = operationParser.getPropertyResolverUtils();
+ if (operation == null) {
+ return;
+ }
+ operation.setSummary(propertyResolverUtils.trimIndent(operation.getSummary()));
+ operation.setDescription(propertyResolverUtils.trimIndent(operation.getDescription()));
+ }
+
+ /**
+ * Gets paths.
+ *
+ * @param findRestControllers the find rest controllers
+ * @param locale the locale
+ * @param openAPI the open api
+ */
+ protected abstract void getPaths(Map findRestControllers, Locale locale, OpenAPI openAPI);
+
+
+ /**
+ * Calculate webhooks.
+ *
+ * @param calculatedOpenAPI the calculated open api
+ * @param locale the locale
+ */
+ protected void calculateWebhooks(OpenAPI calculatedOpenAPI, Locale locale) {
+ Class>[] classes = openAPIService.getWebhooksClasses();
+ Class>[] refinedClasses = Arrays.stream(classes)
+ .filter(clazz -> isPackageToScan(clazz.getPackage()))
+ .toArray(Class>[]::new);
+ Webhooks[] webhooksAttr = openAPIService.getWebhooks(refinedClasses);
+ if (ArrayUtils.isEmpty(webhooksAttr))
+ return;
+ var webhooks = Arrays.stream(webhooksAttr).map(Webhooks::value).flatMap(Arrays::stream).toArray(Webhook[]::new);
+ Arrays.stream(webhooks).forEach(webhook -> {
+ io.swagger.v3.oas.annotations.Operation apiOperation = webhook.operation();
+ Operation operation = new Operation();
+ MethodAttributes methodAttributes = new MethodAttributes(springDocConfigProperties.getDefaultConsumesMediaType(),
+ springDocConfigProperties.getDefaultProducesMediaType(), locale);
+ operationParser.parse(apiOperation, operation, calculatedOpenAPI, methodAttributes);
+ PathItem pathItem = new PathItem().post(operation);
+ calculatedOpenAPI.addWebhooks(webhook.name(), pathItem);
+ });
+ }
+
+ /**
+ * Calculate path.
+ *
+ * @param handlerMethod the handler method
+ * @param routerOperation the router operation
+ * @param locale the locale
+ * @param openAPI the open api
+ */
+ protected void calculatePath(HandlerMethod handlerMethod, RouterOperation routerOperation, Locale locale, OpenAPI openAPI) {
+ routerOperation = customizeRouterOperation(routerOperation, handlerMethod);
+
+ String operationPath = routerOperation.getPath();
+ Set requestMethods = new TreeSet<>(Arrays.asList(routerOperation.getMethods()));
+ io.swagger.v3.oas.annotations.Operation apiOperation = routerOperation.getOperation();
+ String[] methodConsumes = routerOperation.getConsumes();
+ String[] methodProduces = routerOperation.getProduces();
+ String[] headers = routerOperation.getHeaders();
+ Map queryParams = routerOperation.getQueryParams();
+
+ Components components = openAPI.getComponents();
+ Paths paths = openAPI.getPaths();
+
+ Map operationMap = null;
+ if (paths.containsKey(operationPath)) {
+ PathItem pathItem = paths.get(operationPath);
+ operationMap = pathItem.readOperationsMap();
+ }
+
+ JavadocProvider javadocProvider = operationParser.getJavadocProvider();
+
+ for (RequestMethod requestMethod : requestMethods) {
+ Operation existingOperation = getExistingOperation(operationMap, requestMethod);
+ Method method = handlerMethod.getMethod();
+ // skip hidden operations
+ if (operationParser.isHidden(method))
+ continue;
+
+ RequestMapping reqMappingClass = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(),
+ RequestMapping.class);
+
+ MethodAttributes methodAttributes = new MethodAttributes(springDocConfigProperties.getDefaultConsumesMediaType(), springDocConfigProperties.getDefaultProducesMediaType(), methodConsumes, methodProduces, headers, locale);
+ methodAttributes.setMethodOverloaded(existingOperation != null);
+ //Use the javadoc return if present
+ if (javadocProvider != null) {
+ methodAttributes.setJavadocReturn(javadocProvider.getMethodJavadocReturn(handlerMethod.getMethod()));
+ }
+
+ if (reqMappingClass != null) {
+ methodAttributes.setClassConsumes(reqMappingClass.consumes());
+ methodAttributes.setClassProduces(reqMappingClass.produces());
+ }
+
+ methodAttributes.calculateHeadersForClass(method.getDeclaringClass());
+ methodAttributes.calculateConsumesProduces(method);
+
+ Operation operation = (existingOperation != null) ? existingOperation : new Operation();
+
+ if (isDeprecated(method))
+ operation.setDeprecated(true);
+
+ // Add documentation from operation annotation
+ if (apiOperation == null || StringUtils.isBlank(apiOperation.operationId()))
+ apiOperation = AnnotatedElementUtils.findMergedAnnotation(method,
+ io.swagger.v3.oas.annotations.Operation.class);
+
+ calculateJsonView(apiOperation, methodAttributes, method);
+ if (apiOperation != null)
+ openAPI = operationParser.parse(apiOperation, operation, openAPI, methodAttributes);
+ fillParametersList(operation, queryParams, methodAttributes);
+
+ // compute tags
+ operation = openAPIService.buildTags(handlerMethod, operation, openAPI, locale);
+
+ io.swagger.v3.oas.annotations.parameters.RequestBody requestBodyDoc = AnnotatedElementUtils.findMergedAnnotation(method,
+ io.swagger.v3.oas.annotations.parameters.RequestBody.class);
+
+ // RequestBody in Operation
+ requestBuilder.getRequestBodyBuilder()
+ .buildRequestBodyFromDoc(requestBodyDoc, methodAttributes, components,
+ methodAttributes.getJsonViewAnnotationForRequestBody(), locale)
+ .ifPresent(operation::setRequestBody);
+ // requests
+ operation = requestBuilder.build(handlerMethod, requestMethod, operation, methodAttributes, openAPI);
+
+ // responses
+ ApiResponses apiResponses = responseBuilder.build(components, handlerMethod, operation, methodAttributes);
+ operation.setResponses(apiResponses);
+
+ // get javadoc method description
+ if (javadocProvider != null) {
+ String description = javadocProvider.getMethodJavadocDescription(handlerMethod.getMethod());
+ String summary = javadocProvider.getFirstSentence(description);
+// boolean emptyOverrideDescription = StringUtils.isEmpty(operation.getDescription());
+ boolean emptyOverrideSummary = StringUtils.isEmpty(operation.getSummary());
+ if (!StringUtils.isEmpty(description)) {
+ operation.setDescription(description + operation.getDescription());
+ }
+ // if there is a previously set description
+ // but no summary then it is intentional
+ // we keep it as is
+ if (!StringUtils.isEmpty(summary) && emptyOverrideSummary) {
+ operation.setSummary(javadocProvider.getFirstSentence(description));
+ }
+ }
+
+ Set apiCallbacks = AnnotatedElementUtils.findMergedRepeatableAnnotations(method, io.swagger.v3.oas.annotations.callbacks.Callback.class);
+
+ // callbacks
+ buildCallbacks(openAPI, methodAttributes, operation, apiCallbacks);
+
+ // allow for customisation
+ operation = customizeOperation(operation, components, handlerMethod);
+
+ if (StringUtils.contains(operationPath, "*")) {
+ Matcher matcher = pathPattern.matcher(operationPath);
+ while (matcher.find()) {
+ String pathParam = matcher.group(1);
+ String newPathParam = pathParam.replace("*", "");
+ operationPath = operationPath.replace("{" + pathParam + "}", "{" + newPathParam + "}");
+ }
+ }
+
+ PathItem pathItemObject = buildPathItem(requestMethod, operation, operationPath, paths);
+ paths.addPathItem(operationPath, pathItemObject);
+ }
+ }
+
+ /**
+ * Build callbacks.
+ *
+ * @param openAPI the open api
+ * @param methodAttributes the method attributes
+ * @param operation the operation
+ * @param apiCallbacks the api callbacks
+ */
+ private void buildCallbacks(OpenAPI openAPI, MethodAttributes methodAttributes, Operation operation, Set apiCallbacks) {
+ if (!CollectionUtils.isEmpty(apiCallbacks))
+ operationParser.buildCallbacks(apiCallbacks, openAPI, methodAttributes)
+ .ifPresent(operation::setCallbacks);
+ }
+
+ /**
+ * Calculate path.
+ *
+ * @param routerOperationList the router operation list
+ * @param locale the locale
+ * @param openAPI the open api
+ */
+ protected void calculatePath(List routerOperationList, Locale locale, OpenAPI openAPI) {
+ ApplicationContext applicationContext = openAPIService.getContext();
+ if (!CollectionUtils.isEmpty(routerOperationList)) {
+ Collections.sort(routerOperationList);
+ for (RouterOperation routerOperation : routerOperationList) {
+ if (routerOperation.getBeanClass() != null && !Void.class.equals(routerOperation.getBeanClass())) {
+ Object handlerBean = applicationContext.getBean(routerOperation.getBeanClass());
+ HandlerMethod handlerMethod = null;
+
+ if (StringUtils.isNotBlank(routerOperation.getBeanMethod())) {
+ try {
+ if (ArrayUtils.isEmpty(routerOperation.getParameterTypes())) {
+ Method[] declaredMethods = org.springframework.util.ReflectionUtils.getAllDeclaredMethods(AopUtils.getTargetClass(handlerBean));
+ Optional methodOptional = Arrays.stream(declaredMethods)
+ .filter(method -> routerOperation.getBeanMethod().equals(method.getName()) && method.getParameters().length == 0)
+ .findAny();
+ if (!methodOptional.isPresent())
+ methodOptional = Arrays.stream(declaredMethods)
+ .filter(method1 -> routerOperation.getBeanMethod().equals(method1.getName()))
+ .findAny();
+ if (methodOptional.isPresent())
+ handlerMethod = new HandlerMethod(handlerBean, methodOptional.get());
+ }
+ else
+ handlerMethod = new HandlerMethod(handlerBean, routerOperation.getBeanMethod(), routerOperation.getParameterTypes());
+ }
+ catch (NoSuchMethodException e) {
+ LOGGER.error(e.getMessage());
+ }
+ if (handlerMethod != null && isFilterCondition(handlerMethod, routerOperation.getPath(), routerOperation.getProduces(), routerOperation.getConsumes(), routerOperation.getHeaders()))
+ calculatePath(handlerMethod, routerOperation, locale, openAPI);
+ }
+ }
+ else if (routerOperation.getOperation() != null && StringUtils.isNotBlank(routerOperation.getOperation().operationId()) && isFilterCondition(routerOperation.getPath(), routerOperation.getProduces(), routerOperation.getConsumes(), routerOperation.getHeaders())) {
+ calculatePath(routerOperation, locale, openAPI);
+ }
+ else if (routerOperation.getOperationModel() != null && StringUtils.isNotBlank(routerOperation.getOperationModel().getOperationId()) && isFilterCondition(routerOperation.getPath(), routerOperation.getProduces(), routerOperation.getConsumes(), routerOperation.getHeaders())) {
+ calculatePath(routerOperation, locale, openAPI);
+ }
+ }
+ }
+ }
+
+ /**
+ * Calculate path.
+ *
+ * @param routerOperation the router operation
+ * @param locale the locale
+ * @param openAPI the open api
+ */
+ protected void calculatePath(RouterOperation routerOperation, Locale locale, OpenAPI openAPI) {
+ routerOperation = customizeDataRestRouterOperation(routerOperation);
+
+ String operationPath = routerOperation.getPath();
+ io.swagger.v3.oas.annotations.Operation apiOperation = routerOperation.getOperation();
+ String[] methodConsumes = routerOperation.getConsumes();
+ String[] methodProduces = routerOperation.getProduces();
+ String[] headers = routerOperation.getHeaders();
+ Map queryParams = routerOperation.getQueryParams();
+
+ Paths paths = openAPI.getPaths();
+ Map operationMap = null;
+ if (paths.containsKey(operationPath)) {
+ PathItem pathItem = paths.get(operationPath);
+ operationMap = pathItem.readOperationsMap();
+ }
+ for (RequestMethod requestMethod : routerOperation.getMethods()) {
+ Operation existingOperation = getExistingOperation(operationMap, requestMethod);
+ MethodAttributes methodAttributes = new MethodAttributes(springDocConfigProperties.getDefaultConsumesMediaType(), springDocConfigProperties.getDefaultProducesMediaType(), methodConsumes, methodProduces, headers, locale);
+ methodAttributes.setMethodOverloaded(existingOperation != null);
+ Operation operation = getOperation(routerOperation, existingOperation);
+ if (apiOperation != null)
+ openAPI = operationParser.parse(apiOperation, operation, openAPI, methodAttributes);
+
+ String operationId = operation.getOperationId();
+ operation.setOperationId(operationId);
+
+ fillParametersList(operation, queryParams, methodAttributes);
+ if (!CollectionUtils.isEmpty(operation.getParameters()))
+ operation.getParameters().stream()
+ .filter(parameter -> StringUtils.isEmpty(parameter.get$ref()))
+ .forEach(parameter -> {
+ if (parameter.getSchema() == null)
+ parameter.setSchema(new StringSchema());
+ if (parameter.getIn() == null)
+ parameter.setIn(ParameterIn.QUERY.toString());
+ }
+ );
+
+ PathItem pathItemObject = buildPathItem(requestMethod, operation, operationPath, paths);
+ paths.addPathItem(operationPath, pathItemObject);
+ }
+ }
+
+ /**
+ * Customize data rest router operation.
+ *
+ * @param routerOperation the router operation
+ * @return the router operation
+ */
+ private RouterOperation customizeDataRestRouterOperation(RouterOperation routerOperation) {
+ Optional> optionalDataRestRouterOperationCustomizers = springDocCustomizers.getDataRestRouterOperationCustomizers();
+ if (optionalDataRestRouterOperationCustomizers.isPresent()) {
+ Set dataRestRouterOperationCustomizerList = optionalDataRestRouterOperationCustomizers.get();
+ for (DataRestRouterOperationCustomizer dataRestRouterOperationCustomizer : dataRestRouterOperationCustomizerList) {
+ routerOperation = dataRestRouterOperationCustomizer.customize(routerOperation);
+ }
+ }
+ return routerOperation;
+ }
+
+ /**
+ * Calculate path.
+ *
+ * @param handlerMethod the handler method
+ * @param operationPath the operation path
+ * @param requestMethods the request methods
+ * @param consumes the consumes
+ * @param produces the produces
+ * @param headers the headers
+ * @param params the params
+ * @param locale the locale
+ * @param openAPI the open api
+ */
+ protected void calculatePath(HandlerMethod handlerMethod, String operationPath,
+ Set requestMethods, String[] consumes, String[] produces, String[] headers, String[] params, Locale locale, OpenAPI openAPI) {
+ this.calculatePath(handlerMethod, new RouterOperation(operationPath, requestMethods.toArray(new RequestMethod[requestMethods.size()]), consumes, produces, headers, params), locale, openAPI);
+ }
+
+ /**
+ * Gets router function paths.
+ *
+ * @param beanName the bean name
+ * @param routerFunctionVisitor the router function visitor
+ * @param locale the locale
+ * @param openAPI the open api
+ */
+ protected void getRouterFunctionPaths(String beanName, AbstractRouterFunctionVisitor routerFunctionVisitor,
+ Locale locale, OpenAPI openAPI) {
+ boolean withRouterOperation = routerFunctionVisitor.getRouterFunctionDatas().stream()
+ .anyMatch(routerFunctionData -> routerFunctionData.getAttributes().containsKey(OPERATION_ATTRIBUTE));
+ if (withRouterOperation) {
+ List operationList = routerFunctionVisitor.getRouterFunctionDatas().stream().map(RouterOperation::new).collect(Collectors.toList());
+ calculatePath(operationList, locale, openAPI);
+ }
+ else {
+ List routerOperationList = new ArrayList<>();
+ ApplicationContext applicationContext = openAPIService.getContext();
+ RouterOperations routerOperations = applicationContext.findAnnotationOnBean(beanName, RouterOperations.class);
+ if (routerOperations == null) {
+ org.springdoc.core.annotations.RouterOperation routerOperation = applicationContext.findAnnotationOnBean(beanName, org.springdoc.core.annotations.RouterOperation.class);
+ if (routerOperation != null)
+ routerOperationList.add(routerOperation);
+ }
+ else
+ routerOperationList.addAll(Arrays.asList(routerOperations.value()));
+ if (routerOperationList.size() == 1)
+ calculatePath(routerOperationList.stream().map(routerOperation -> new RouterOperation(routerOperation, routerFunctionVisitor.getRouterFunctionDatas().get(0))).collect(Collectors.toList()), locale, openAPI);
+ else {
+ List operationList = routerOperationList.stream().map(RouterOperation::new).collect(Collectors.toList());
+ mergeRouters(routerFunctionVisitor.getRouterFunctionDatas(), operationList);
+ calculatePath(operationList, locale, openAPI);
+ }
+ }
+ }
+
+ /**
+ * Is filter condition boolean.
+ *
+ * @param handlerMethod the handler method
+ * @param operationPath the operation path
+ * @param produces the produces
+ * @param consumes the consumes
+ * @param headers the headers
+ * @return the boolean
+ */
+ protected boolean isFilterCondition(HandlerMethod handlerMethod, String operationPath, String[] produces, String[] consumes, String[] headers) {
+ return isMethodToFilter(handlerMethod)
+ && isPackageToScan(handlerMethod.getBeanType().getPackage())
+ && isFilterCondition(operationPath, produces, consumes, headers);
+ }
+
+ /**
+ * Is target method suitable for inclusion in current documentation/
+ *
+ * @param handlerMethod the method to check
+ * @return whether the method should be included in the current OpenAPI definition
+ */
+ protected boolean isMethodToFilter(HandlerMethod handlerMethod) {
+ return this.springDocCustomizers.getMethodFilters()
+ .map(Collection::stream)
+ .map(stream -> stream.allMatch(m -> m.isMethodToInclude(handlerMethod.getMethod())))
+ .orElse(true);
+ }
+
+ /**
+ * Is condition to match boolean.
+ *
+ * @param existingConditions the existing conditions
+ * @param conditionType the condition type
+ * @return the boolean
+ */
+ protected boolean isConditionToMatch(String[] existingConditions, ConditionType conditionType) {
+ List conditionsToMatch = getConditionsToMatch(conditionType);
+ if (CollectionUtils.isEmpty(conditionsToMatch)) {
+ Optional optionalGroupConfig = springDocConfigProperties.getGroupConfigs().stream().filter(groupConfig -> this.groupName.equals(groupConfig.getGroup())).findAny();
+ if (optionalGroupConfig.isPresent())
+ conditionsToMatch = getConditionsToMatch(conditionType, optionalGroupConfig.get());
+ }
+ return CollectionUtils.isEmpty(conditionsToMatch)
+ || (!ArrayUtils.isEmpty(existingConditions) && conditionsToMatch.size() == existingConditions.length && conditionsToMatch.containsAll(Arrays.asList(existingConditions)));
+ }
+
+ /**
+ * Is package to scan boolean.
+ *
+ * @param aPackage the a package
+ * @return the boolean
+ */
+ protected boolean isPackageToScan(Package aPackage) {
+ if (aPackage == null)
+ return true;
+ final String packageName = aPackage.getName();
+ List packagesToScan = springDocConfigProperties.getPackagesToScan();
+ List packagesToExclude = springDocConfigProperties.getPackagesToExclude();
+ if (CollectionUtils.isEmpty(packagesToScan)) {
+ Optional optionalGroupConfig = springDocConfigProperties.getGroupConfigs().stream().filter(groupConfig -> this.groupName.equals(groupConfig.getGroup())).findAny();
+ if (optionalGroupConfig.isPresent())
+ packagesToScan = optionalGroupConfig.get().getPackagesToScan();
+ }
+ if (CollectionUtils.isEmpty(packagesToExclude)) {
+ Optional optionalGroupConfig = springDocConfigProperties.getGroupConfigs().stream().filter(groupConfig -> this.groupName.equals(groupConfig.getGroup())).findAny();
+ if (optionalGroupConfig.isPresent())
+ packagesToExclude = optionalGroupConfig.get().getPackagesToExclude();
+ }
+ boolean include = CollectionUtils.isEmpty(packagesToScan)
+ || packagesToScan.stream().anyMatch(pack -> packageName.equals(pack)
+ || packageName.startsWith(pack + DOT));
+ boolean exclude = !CollectionUtils.isEmpty(packagesToExclude)
+ && (packagesToExclude.stream().anyMatch(pack -> packageName.equals(pack)
+ || packageName.startsWith(pack + DOT)));
+
+ return include && !exclude;
+ }
+
+ /**
+ * Is path to match boolean.
+ *
+ * @param operationPath the operation path
+ * @return the boolean
+ */
+ protected boolean isPathToMatch(String operationPath) {
+ List pathsToMatch = springDocConfigProperties.getPathsToMatch();
+ List pathsToExclude = springDocConfigProperties.getPathsToExclude();
+ if (CollectionUtils.isEmpty(pathsToMatch)) {
+ Optional optionalGroupConfig = springDocConfigProperties.getGroupConfigs().stream().filter(groupConfig -> this.groupName.equals(groupConfig.getGroup())).findAny();
+ if (optionalGroupConfig.isPresent())
+ pathsToMatch = optionalGroupConfig.get().getPathsToMatch();
+ }
+ if (CollectionUtils.isEmpty(pathsToExclude)) {
+ Optional optionalGroupConfig = springDocConfigProperties.getGroupConfigs().stream().filter(groupConfig -> this.groupName.equals(groupConfig.getGroup())).findAny();
+ if (optionalGroupConfig.isPresent())
+ pathsToExclude = optionalGroupConfig.get().getPathsToExclude();
+ }
+ boolean include = CollectionUtils.isEmpty(pathsToMatch) || pathsToMatch.stream().anyMatch(pattern -> antPathMatcher.match(pattern, operationPath));
+ boolean exclude = !CollectionUtils.isEmpty(pathsToExclude) && pathsToExclude.stream().anyMatch(pattern -> antPathMatcher.match(pattern, operationPath));
+ return include && !exclude;
+ }
+
+ /**
+ * Decode string.
+ *
+ * @param requestURI the request uri
+ * @return the string
+ */
+ protected String decode(String requestURI) {
+ try {
+ return URLDecoder.decode(requestURI, StandardCharsets.UTF_8.toString());
+ }
+ catch (UnsupportedEncodingException e) {
+ return requestURI;
+ }
+ }
+
+ /**
+ * Is additional rest controller boolean.
+ *
+ * @param rawClass the raw class
+ * @return the boolean
+ */
+ protected boolean isAdditionalRestController(Class> rawClass) {
+ return ADDITIONAL_REST_CONTROLLERS.stream().anyMatch(clazz -> ClassUtils.getUserClass(clazz).isAssignableFrom(rawClass));
+ }
+
+ /**
+ * Is rest controller boolean.
+ *
+ * @param restControllers the rest controllers
+ * @param handlerMethod the handler method
+ * @param operationPath the operation path
+ * @return the boolean
+ */
+ protected boolean isRestController(Map restControllers, HandlerMethod handlerMethod,
+ String operationPath) {
+ boolean hasOperationAnnotation = AnnotatedElementUtils.hasAnnotation(handlerMethod.getMethod(), io.swagger.v3.oas.annotations.Operation.class);
+
+ return ((containsResponseBody(handlerMethod) || hasOperationAnnotation) && restControllers.containsKey(handlerMethod.getBean().toString()) || isAdditionalRestController(handlerMethod.getBeanType()))
+ && operationPath.startsWith(DEFAULT_PATH_SEPARATOR)
+ && (springDocConfigProperties.isModelAndViewAllowed() || modelAndViewClass == null || !modelAndViewClass.isAssignableFrom(handlerMethod.getMethod().getReturnType()));
+ }
+
+ /**
+ * Gets default allowed http methods.
+ *
+ * @return the default allowed http methods
+ */
+ protected Set getDefaultAllowedHttpMethods() {
+ RequestMethod[] allowedRequestMethods = { RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.PATCH, RequestMethod.DELETE, RequestMethod.OPTIONS, RequestMethod.HEAD };
+ return new HashSet<>(Arrays.asList(allowedRequestMethods));
+ }
+
+ /**
+ * Customise operation.
+ *
+ * @param operation the operation
+ * @param components
+ * @param handlerMethod the handler method
+ * @return the operation
+ */
+ protected Operation customizeOperation(Operation operation, Components components, HandlerMethod handlerMethod) {
+ Optional> optionalOperationCustomizers = springDocCustomizers.getOperationCustomizers();
+ if (optionalOperationCustomizers.isPresent()) {
+ Set operationCustomizerList = optionalOperationCustomizers.get();
+ for (OperationCustomizer operationCustomizer : operationCustomizerList) {
+ if (operationCustomizer instanceof GlobalOperationComponentsCustomizer globalOperationComponentsCustomizer)
+ operation = globalOperationComponentsCustomizer.customize(operation, components, handlerMethod);
+ else
+ operation = operationCustomizer.customize(operation, handlerMethod);
+ }
+ }
+ return operation;
+ }
+
+ /**
+ * Customise router operation
+ *
+ * @param routerOperation the router operation
+ * @param handlerMethod the handler method
+ * @return the router operation
+ */
+ protected RouterOperation customizeRouterOperation(RouterOperation routerOperation, HandlerMethod handlerMethod) {
+ Optional> optionalRouterOperationCustomizers = springDocCustomizers.getRouterOperationCustomizers();
+ if (optionalRouterOperationCustomizers.isPresent()) {
+ Set routerOperationCustomizerList = optionalRouterOperationCustomizers.get();
+ for (RouterOperationCustomizer routerOperationCustomizer : routerOperationCustomizerList) {
+ routerOperation = routerOperationCustomizer.customize(routerOperation, handlerMethod);
+ }
+ }
+ return routerOperation;
+ }
+
+ /**
+ * Merge routers.
+ *
+ * @param routerFunctionDatas the router function datas
+ * @param routerOperationList the router operation list
+ */
+ protected void mergeRouters(List routerFunctionDatas, List routerOperationList) {
+ for (RouterOperation routerOperation : routerOperationList) {
+ if (StringUtils.isNotBlank(routerOperation.getPath())) {
+ // PATH
+ List routerFunctionDataList = routerFunctionDatas.stream()
+ .filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath()))
+ .toList();
+ if (routerFunctionDataList.size() == 1)
+ fillRouterOperation(routerFunctionDataList.get(0), routerOperation);
+ else if (routerFunctionDataList.size() > 1 && ArrayUtils.isNotEmpty(routerOperation.getMethods())) {
+ // PATH + METHOD
+ routerFunctionDataList = routerFunctionDatas.stream()
+ .filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath())
+ && isEqualMethods(routerOperation.getMethods(), routerFunctionData1.getMethods()))
+ .toList();
+ if (routerFunctionDataList.size() == 1)
+ fillRouterOperation(routerFunctionDataList.get(0), routerOperation);
+ else if (routerFunctionDataList.size() > 1 && ArrayUtils.isNotEmpty(routerOperation.getProduces())) {
+ // PATH + METHOD + PRODUCES
+ routerFunctionDataList = routerFunctionDatas.stream()
+ .filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath())
+ && isEqualMethods(routerOperation.getMethods(), routerFunctionData1.getMethods())
+ && isEqualArrays(routerFunctionData1.getProduces(), routerOperation.getProduces()))
+ .toList();
+ if (routerFunctionDataList.size() == 1)
+ fillRouterOperation(routerFunctionDataList.get(0), routerOperation);
+ else if (routerFunctionDataList.size() > 1 && ArrayUtils.isNotEmpty(routerOperation.getConsumes())) {
+ // PATH + METHOD + PRODUCES + CONSUMES
+ routerFunctionDataList = routerFunctionDatas.stream()
+ .filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath())
+ && isEqualMethods(routerOperation.getMethods(), routerFunctionData1.getMethods())
+ && isEqualArrays(routerFunctionData1.getProduces(), routerOperation.getProduces())
+ && isEqualArrays(routerFunctionData1.getConsumes(), routerOperation.getConsumes()))
+ .toList();
+ if (routerFunctionDataList.size() == 1)
+ fillRouterOperation(routerFunctionDataList.get(0), routerOperation);
+ }
+ }
+ else if (routerFunctionDataList.size() > 1 && ArrayUtils.isNotEmpty(routerOperation.getConsumes())) {
+ // PATH + METHOD + CONSUMES
+ routerFunctionDataList = routerFunctionDatas.stream()
+ .filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath())
+ && isEqualMethods(routerOperation.getMethods(), routerFunctionData1.getMethods())
+ && isEqualArrays(routerFunctionData1.getConsumes(), routerOperation.getConsumes()))
+ .toList();
+ if (routerFunctionDataList.size() == 1)
+ fillRouterOperation(routerFunctionDataList.get(0), routerOperation);
+ }
+ }
+ else if (routerFunctionDataList.size() > 1 && ArrayUtils.isNotEmpty(routerOperation.getProduces())) {
+ // PATH + PRODUCES
+ routerFunctionDataList = routerFunctionDatas.stream()
+ .filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath())
+ && isEqualArrays(routerFunctionData1.getProduces(), routerOperation.getProduces()))
+ .toList();
+ if (routerFunctionDataList.size() == 1)
+ fillRouterOperation(routerFunctionDataList.get(0), routerOperation);
+ else if (routerFunctionDataList.size() > 1 && ArrayUtils.isNotEmpty(routerOperation.getConsumes())) {
+ // PATH + PRODUCES + CONSUMES
+ routerFunctionDataList = routerFunctionDatas.stream()
+ .filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath())
+ && isEqualMethods(routerOperation.getMethods(), routerFunctionData1.getMethods())
+ && isEqualArrays(routerFunctionData1.getConsumes(), routerOperation.getConsumes())
+ && isEqualArrays(routerFunctionData1.getProduces(), routerOperation.getProduces()))
+ .toList();
+ if (routerFunctionDataList.size() == 1)
+ fillRouterOperation(routerFunctionDataList.get(0), routerOperation);
+ }
+ }
+ else if (routerFunctionDataList.size() > 1 && ArrayUtils.isNotEmpty(routerOperation.getConsumes())) {
+ // PATH + CONSUMES
+ routerFunctionDataList = routerFunctionDatas.stream()
+ .filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath())
+ && isEqualArrays(routerFunctionData1.getConsumes(), routerOperation.getConsumes()))
+ .toList();
+ if (routerFunctionDataList.size() == 1)
+ fillRouterOperation(routerFunctionDataList.get(0), routerOperation);
+ }
+ }
+ }
+ }
+
+ /**
+ * Calculate json view.
+ *
+ * @param apiOperation the api operation
+ * @param methodAttributes the method attributes
+ * @param method the method
+ */
+ private void calculateJsonView(io.swagger.v3.oas.annotations.Operation apiOperation,
+ MethodAttributes methodAttributes, Method method) {
+ JsonView jsonViewAnnotation;
+ JsonView jsonViewAnnotationForRequestBody;
+ if (apiOperation != null && apiOperation.ignoreJsonView()) {
+ jsonViewAnnotation = null;
+ jsonViewAnnotationForRequestBody = null;
+ }
+ else {
+ jsonViewAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, JsonView.class);
+ /*
+ * If one and only one exists, use the @JsonView annotation from the method
+ * parameter annotated with @RequestBody. Otherwise fall back to the @JsonView
+ * annotation for the method itself.
+ */
+ jsonViewAnnotationForRequestBody = (JsonView) Arrays.stream(ReflectionUtils.getParameterAnnotations(method))
+ .filter(arr -> Arrays.stream(arr)
+ .anyMatch(annotation -> (annotation.annotationType()
+ .equals(io.swagger.v3.oas.annotations.parameters.RequestBody.class) || annotation.annotationType().equals(RequestBody.class))))
+ .flatMap(Arrays::stream).filter(annotation -> annotation.annotationType().equals(JsonView.class))
+ .reduce((a, b) -> null).orElse(jsonViewAnnotation);
+ }
+ methodAttributes.setJsonViewAnnotation(jsonViewAnnotation);
+ methodAttributes.setJsonViewAnnotationForRequestBody(jsonViewAnnotationForRequestBody);
+ }
+
+ /**
+ * Is equal arrays boolean.
+ *
+ * @param array1 the array 1
+ * @param array2 the array 2
+ * @return the boolean
+ */
+ private boolean isEqualArrays(String[] array1, String[] array2) {
+ Arrays.sort(array1);
+ Arrays.sort(array2);
+ return Arrays.equals(array1, array2);
+ }
+
+ /**
+ * Is equal methods boolean.
+ *
+ * @param requestMethods1 the request methods 1
+ * @param requestMethods2 the request methods 2
+ * @return the boolean
+ */
+ private boolean isEqualMethods(RequestMethod[] requestMethods1, RequestMethod[] requestMethods2) {
+ Arrays.sort(requestMethods1);
+ Arrays.sort(requestMethods2);
+ return Arrays.equals(requestMethods1, requestMethods2);
+ }
+
+ /**
+ * Fill parameters list.
+ *
+ * @param operation the operation
+ * @param queryParams the query params
+ * @param methodAttributes the method attributes
+ */
+ private void fillParametersList(Operation operation, Map queryParams, MethodAttributes methodAttributes) {
+ List parametersList = operation.getParameters();
+ if (parametersList == null)
+ parametersList = new ArrayList<>();
+ Collection headersMap = AbstractRequestService.getHeaders(methodAttributes, new LinkedHashMap<>());
+ headersMap.forEach(parameter -> {
+ Optional existingParam;
+ if (!CollectionUtils.isEmpty(operation.getParameters())) {
+ existingParam = operation.getParameters().stream().filter(p -> parameter.getName().equals(p.getName())).findAny();
+ if (existingParam.isEmpty())
+ operation.addParametersItem(parameter);
+ }
+ });
+ if (!CollectionUtils.isEmpty(queryParams)) {
+ for (Map.Entry entry : queryParams.entrySet()) {
+ io.swagger.v3.oas.models.parameters.Parameter parameter = new io.swagger.v3.oas.models.parameters.Parameter();
+ parameter.setName(entry.getKey());
+ parameter.setSchema(new StringSchema()._default(entry.getValue()));
+ parameter.setRequired(true);
+ parameter.setIn(ParameterIn.QUERY.toString());
+ GenericParameterService.mergeParameter(parametersList, parameter);
+ }
+ operation.setParameters(parametersList);
+ }
+ }
+
+ /**
+ * Fill router operation.
+ *
+ * @param routerFunctionData the router function data
+ * @param routerOperation the router operation
+ */
+ private void fillRouterOperation(RouterFunctionData routerFunctionData, RouterOperation routerOperation) {
+ if (ArrayUtils.isEmpty(routerOperation.getConsumes()))
+ routerOperation.setConsumes(routerFunctionData.getConsumes());
+ if (ArrayUtils.isEmpty(routerOperation.getProduces()))
+ routerOperation.setProduces(routerFunctionData.getProduces());
+ if (ArrayUtils.isEmpty(routerOperation.getHeaders()))
+ routerOperation.setHeaders(routerFunctionData.getHeaders());
+ if (ArrayUtils.isEmpty(routerOperation.getMethods()))
+ routerOperation.setMethods(routerFunctionData.getMethods());
+ if (CollectionUtils.isEmpty(routerOperation.getQueryParams()))
+ routerOperation.setQueryParams(routerFunctionData.getQueryParams());
+ }
+
+ /**
+ * Build path item.
+ *
+ * @param requestMethod the request method
+ * @param operation the operation
+ * @param operationPath the operation path
+ * @param paths the paths
+ * @return the path item
+ */
+ private PathItem buildPathItem(RequestMethod requestMethod, Operation operation, String operationPath,
+ Paths paths) {
+ PathItem pathItemObject;
+ if (operation != null && !CollectionUtils.isEmpty(operation.getParameters())) {
+ Iterator paramIt = operation.getParameters().iterator();
+ while (paramIt.hasNext()) {
+ Parameter parameter = paramIt.next();
+ if (ParameterIn.PATH.toString().equals(parameter.getIn())) {
+ // check it's present in the path
+ String name = parameter.getName();
+ if (!StringUtils.containsAny(operationPath, "{" + name + "}", "{*" + name + "}"))
+ paramIt.remove();
+ }
+ }
+ }
+ if (paths.containsKey(operationPath))
+ pathItemObject = paths.get(operationPath);
+ else
+ pathItemObject = new PathItem();
+
+ switch (requestMethod) {
+ case POST:
+ pathItemObject.post(operation);
+ break;
+ case GET:
+ pathItemObject.get(operation);
+ break;
+ case DELETE:
+ pathItemObject.delete(operation);
+ break;
+ case PUT:
+ pathItemObject.put(operation);
+ break;
+ case PATCH:
+ pathItemObject.patch(operation);
+ break;
+ case TRACE:
+ pathItemObject.trace(operation);
+ break;
+ case HEAD:
+ pathItemObject.head(operation);
+ break;
+ case OPTIONS:
+ pathItemObject.options(operation);
+ break;
+ default:
+ // Do nothing here
+ break;
+ }
+ return pathItemObject;
+ }
+
+ /**
+ * Gets existing operation.
+ *
+ * @param operationMap the operation map
+ * @param requestMethod the request method
+ * @return the existing operation
+ */
+ private Operation getExistingOperation(Map operationMap, RequestMethod requestMethod) {
+ Operation existingOperation = null;
+ if (!CollectionUtils.isEmpty(operationMap)) {
+ // Get existing operation definition
+ switch (requestMethod) {
+ case GET:
+ existingOperation = operationMap.get(HttpMethod.GET);
+ break;
+ case POST:
+ existingOperation = operationMap.get(HttpMethod.POST);
+ break;
+ case PUT:
+ existingOperation = operationMap.get(HttpMethod.PUT);
+ break;
+ case DELETE:
+ existingOperation = operationMap.get(HttpMethod.DELETE);
+ break;
+ case PATCH:
+ existingOperation = operationMap.get(HttpMethod.PATCH);
+ break;
+ case HEAD:
+ existingOperation = operationMap.get(HttpMethod.HEAD);
+ break;
+ case OPTIONS:
+ existingOperation = operationMap.get(HttpMethod.OPTIONS);
+ break;
+ default:
+ // Do nothing here
+ break;
+ }
+ }
+ return existingOperation;
+ }
+
+ /**
+ * Gets operation.
+ *
+ * @param routerOperation the router operation
+ * @param existingOperation the existing operation
+ * @return the operation
+ */
+ private Operation getOperation(RouterOperation routerOperation, Operation existingOperation) {
+ Operation operationModel = routerOperation.getOperationModel();
+ Operation operation;
+ if (existingOperation != null && operationModel == null) {
+ operation = existingOperation;
+ }
+ else if (existingOperation == null && operationModel != null) {
+ operation = operationModel;
+ }
+ else if (existingOperation != null) {
+ operation = operationParser.mergeOperation(existingOperation, operationModel);
+ }
+ else {
+ operation = new Operation();
+ }
+ return operation;
+ }
+
+ /**
+ * Init open api builder.
+ *
+ * @param locale the locale
+ */
+ protected void initOpenAPIBuilder(Locale locale) {
+ locale = selectLocale(locale);
+ if (openAPIService.getCachedOpenAPI(locale) != null && springDocConfigProperties.isCacheDisabled()) {
+ openAPIService = openAPIBuilderObjectFactory.getObject();
+ }
+ }
+
+ /**
+ * Write yaml value string.
+ *
+ * @param openAPI the open api
+ * @return the string
+ * @throws JsonProcessingException the json processing exception
+ */
+ protected byte[] writeYamlValue(OpenAPI openAPI) throws JsonProcessingException {
+ String result;
+ ObjectMapper objectMapper = springDocProviders.yamlMapper();
+ if (springDocConfigProperties.isWriterWithOrderByKeys())
+ ObjectMapperProvider.sortOutput(objectMapper, springDocConfigProperties);
+ YAMLFactory factory = (YAMLFactory) objectMapper.getFactory();
+ factory.configure(Feature.USE_NATIVE_TYPE_ID, false);
+ if (!springDocConfigProperties.isWriterWithDefaultPrettyPrinter())
+ result = objectMapper.writerFor(OpenAPI.class).writeValueAsString(openAPI);
+ else
+ result = objectMapper.writerWithDefaultPrettyPrinter().forType(OpenAPI.class).writeValueAsString(openAPI);
+ return result.getBytes(StandardCharsets.UTF_8);
+ }
+
+ /**
+ * Gets actuator uri.
+ *
+ * @param scheme the scheme
+ * @param host the host
+ * @return the actuator uri
+ */
+ protected URI getActuatorURI(String scheme, String host) {
+ final Optional actuatorProviderOptional = springDocProviders.getActuatorProvider();
+ URI uri = null;
+ if (actuatorProviderOptional.isPresent()) {
+ ActuatorProvider actuatorProvider = actuatorProviderOptional.get();
+ int port;
+ String path;
+ if (ACTUATOR_DEFAULT_GROUP.equals(this.groupName)) {
+ port = actuatorProvider.getActuatorPort();
+ path = actuatorProvider.getActuatorPath();
+ }
+ else {
+ port = actuatorProvider.getApplicationPort();
+ path = actuatorProvider.getContextPath();
+ String mvcServletPath = this.openAPIService.getContext().getBean(Environment.class).getProperty(SPRING_MVC_SERVLET_PATH);
+ if (SpringDocUtils.isValidPath(mvcServletPath))
+ path = path + mvcServletPath;
+ }
+ try {
+ uri = new URI(StringUtils.defaultIfEmpty(scheme, "http"), null, StringUtils.defaultIfEmpty(host, "localhost"), port, path, null, null);
+ }
+ catch (URISyntaxException e) {
+ LOGGER.error("Unable to parse the URL: scheme {}, host {}, port {}, path {}", scheme, host, port, path);
+ }
+ }
+ return uri;
+ }
+
+ /**
+ * Is actuator rest controller boolean.
+ *
+ * @param operationPath the operation path
+ * @param handlerMethod the handler method
+ * @return the boolean
+ */
+ protected boolean isActuatorRestController(String operationPath, HandlerMethod handlerMethod) {
+ Optional actuatorProviderOptional = springDocProviders.getActuatorProvider();
+ boolean isActuatorRestController = false;
+ if (actuatorProviderOptional.isPresent())
+ isActuatorRestController = actuatorProviderOptional.get().isRestController(operationPath, handlerMethod);
+ return springDocConfigProperties.isShowActuator() && isActuatorRestController && (modelAndViewClass == null || !modelAndViewClass.isAssignableFrom(handlerMethod.getMethod().getReturnType()));
+ }
+
+ /**
+ * Write json value string.
+ *
+ * @param openAPI the open api
+ * @return the string
+ * @throws JsonProcessingException the json processing exception
+ */
+ protected byte[] writeJsonValue(OpenAPI openAPI) throws JsonProcessingException {
+ String result;
+ ObjectMapper objectMapper = springDocProviders.jsonMapper();
+ if (springDocConfigProperties.isWriterWithOrderByKeys())
+ ObjectMapperProvider.sortOutput(objectMapper, springDocConfigProperties);
+ if (!springDocConfigProperties.isWriterWithDefaultPrettyPrinter())
+ result = objectMapper.writerFor(OpenAPI.class).writeValueAsString(openAPI);
+ else
+ result = objectMapper.writerWithDefaultPrettyPrinter().forType(OpenAPI.class).writeValueAsString(openAPI);
+ return result.getBytes(StandardCharsets.UTF_8);
+ }
+
+ /**
+ * Gets conditions to match.
+ *
+ * @param conditionType the condition type
+ * @param groupConfigs the group configs
+ * @return the conditions to match
+ */
+ private List getConditionsToMatch(ConditionType conditionType, GroupConfig... groupConfigs) {
+ List conditionsToMatch = null;
+ GroupConfig groupConfig = null;
+ if (ArrayUtils.isNotEmpty(groupConfigs))
+ groupConfig = groupConfigs[0];
+ switch (conditionType) {
+ case HEADERS:
+ conditionsToMatch = (groupConfig != null) ? groupConfig.getHeadersToMatch() : springDocConfigProperties.getHeadersToMatch();
+ break;
+ case PRODUCES:
+ conditionsToMatch = (groupConfig != null) ? groupConfig.getProducesToMatch() : springDocConfigProperties.getProducesToMatch();
+ break;
+ case CONSUMES:
+ conditionsToMatch = (groupConfig != null) ? groupConfig.getConsumesToMatch() : springDocConfigProperties.getConsumesToMatch();
+ break;
+ default:
+ break;
+ }
+ return conditionsToMatch;
+ }
+
+ /**
+ * Is filter condition boolean.
+ *
+ * @param operationPath the operation path
+ * @param produces the produces
+ * @param consumes the consumes
+ * @param headers the headers
+ * @return the boolean
+ */
+ private boolean isFilterCondition(String operationPath, String[] produces, String[] consumes, String[] headers) {
+ return isPathToMatch(operationPath)
+ && isConditionToMatch(produces, ConditionType.PRODUCES)
+ && isConditionToMatch(consumes, ConditionType.CONSUMES)
+ && isConditionToMatch(headers, ConditionType.HEADERS);
+ }
+
+ /**
+ * The enum Condition type.
+ */
+ enum ConditionType {
+ /**
+ * Produces condition type.
+ */
+ PRODUCES,
+ /**
+ * Consumes condition type.
+ */
+ CONSUMES,
+ /**
+ * Headers condition type.
+ */
+ HEADERS
+ }
+}
diff --git a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/SaTokenTestController.java b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/SaTokenTestController.java
new file mode 100644
index 000000000..c9dada111
--- /dev/null
+++ b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/SaTokenTestController.java
@@ -0,0 +1,198 @@
+package org.dromara.demo.controller;
+
+import cn.dev33.satoken.annotation.*;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.domain.R;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * SaToken 权限测试
+ *
+ * @author AprilWind
+ */
+@Slf4j
+@RestController
+@RequestMapping("/demo/SaToken")
+public class SaTokenTestController {
+
+ // ====================== 基础场景:单一校验规则 ======================
+
+ /**
+ * 场景1:仅登录校验(无角色/权限限制,只需登录态)
+ */
+ @SaCheckLogin
+ @GetMapping("/basic/loginOnly")
+ public R loginOnly() {
+ log.info("【场景1】仅登录校验通过");
+ return R.ok("仅登录校验通过,无需角色/权限");
+ }
+
+ /**
+ * 场景2:单一角色校验(AND模式,默认)
+ */
+ @SaCheckRole("admin")
+ @GetMapping("/basic/singleRole")
+ public R singleRole() {
+ log.info("【场景2】单一角色(admin)校验通过");
+ return R.ok("拥有admin角色,校验通过");
+ }
+
+ /**
+ * 场景3:单一权限校验(AND模式,默认)
+ */
+ @SaCheckPermission("system:user:view")
+ @GetMapping("/basic/singlePermission")
+ public R singlePermission() {
+ log.info("【场景3】单一权限(system:user:view)校验通过");
+ return R.ok("拥有system:user:view权限,校验通过");
+ }
+
+ /**
+ * 场景4:忽略所有权限校验(SaIgnore优先级最高)
+ */
+ @SaIgnore
+ @SaCheckRole("none_exist") // 该注解会被忽略
+ @GetMapping("/basic/ignoreAll")
+ public R ignoreAll() {
+ log.info("【场景4】SaIgnore忽略所有权限校验");
+ return R.ok("SaIgnore生效,所有权限校验被忽略");
+ }
+
+ // ====================== 进阶场景:多条件组合(AND/OR) ======================
+
+ /**
+ * 场景5:多角色AND模式(必须同时拥有所有角色)
+ */
+ @SaCheckRole(value = {"admin", "operator"}, mode = SaMode.AND)
+ @GetMapping("/advance/multiRoleAnd")
+ public R multiRoleAnd() {
+ log.info("【场景5】多角色AND模式(admin+operator)校验通过");
+ return R.ok("同时拥有admin和operator角色,校验通过");
+ }
+
+ /**
+ * 场景6:多角色OR模式(拥有任一角色即可)
+ */
+ @SaCheckRole(value = {"admin", "test"}, mode = SaMode.OR)
+ @GetMapping("/advance/multiRoleOr")
+ public R multiRoleOr() {
+ log.info("【场景6】多角色OR模式(admin|test)校验通过");
+ return R.ok("拥有admin或test角色,校验通过");
+ }
+
+ /**
+ * 场景7:多权限AND模式(必须同时拥有所有权限)
+ */
+ @SaCheckPermission(value = {"system:user:edit", "system:log:view"}, mode = SaMode.AND)
+ @GetMapping("/advance/multiPermAnd")
+ public R multiPermAnd() {
+ log.info("【场景7】多权限AND模式(system:user:edit+system:log:view)校验通过");
+ return R.ok("同时拥有system:user:edit和system:log:view权限,校验通过");
+ }
+
+ /**
+ * 场景8:多权限OR模式(拥有任一权限即可)
+ */
+ @SaCheckPermission(value = {"system:user:add", "system:user:delete"}, mode = SaMode.OR)
+ @GetMapping("/advance/multiPermOr")
+ public R multiPermOr() {
+ log.info("【场景8】多权限OR模式(system:user:add|system:user:delete)校验通过");
+ return R.ok("拥有system:user:add或system:user:delete权限,校验通过");
+ }
+
+ // ====================== 高级场景:通配符/混合组合 ======================
+
+ /**
+ * 场景9:权限通配符匹配(前缀匹配)
+ * 拥有system:user:* 即可匹配所有用户模块权限
+ */
+ @SaCheckPermission("system:user:*")
+ @GetMapping("/advanced/permWildcardPrefix")
+ public R permWildcardPrefix() {
+ log.info("【场景9】权限通配符(system:user:*)校验通过");
+ return R.ok("拥有system:user:*前缀权限,校验通过");
+ }
+
+ /**
+ * 场景10:角色通配符匹配(前缀匹配)
+ * 拥有admin_* 即可匹配所有admin开头的角色
+ */
+ @SaCheckRole("admin_*")
+ @GetMapping("/advanced/roleWildcardPrefix")
+ public R roleWildcardPrefix() {
+ log.info("【场景10】角色通配符(admin_*)校验通过");
+ return R.ok("拥有admin_*前缀角色,校验通过");
+ }
+
+ /**
+ * 场景11:权限+角色混合AND模式(所有条件必须满足)
+ * 需同时满足:拥有admin角色 + 拥有system:user:all权限
+ */
+ @SaCheckRole("admin")
+ @SaCheckPermission("system:user:all")
+ @GetMapping("/advanced/mixRolePermAnd")
+ public R mixRolePermAnd() {
+ log.info("【场景11】角色+权限混合AND(admin+system:user:all)校验通过");
+ return R.ok("拥有admin角色且拥有system:user:all权限,校验通过");
+ }
+
+ /**
+ * 场景12:权限+角色混合OR模式(任一条件满足即可)
+ * 满足任一:拥有super_admin角色 | 拥有system:manage权限
+ */
+ @SaCheckRole(value = {"super_admin"}, mode = SaMode.OR)
+ @SaCheckPermission(value = {"system:manage"}, mode = SaMode.OR)
+ @GetMapping("/advanced/mixRolePermOr")
+ public R mixRolePermOr() {
+ log.info("【场景12】角色+权限混合OR(super_admin|system:manage)校验通过");
+ return R.ok("拥有super_admin角色或system:manage权限,校验通过");
+ }
+
+ /**
+ * 场景13:orRole参数(权限校验失败时,兜底角色校验)
+ * 核心逻辑:无system:user:export权限时,检查是否有admin/operator角色
+ */
+ @SaCheckPermission(value = "system:user:export", orRole = {"admin", "operator"})
+ @GetMapping("/advanced/permWithOrRole")
+ public R permWithOrRole() {
+ log.info("【场景13】权限+orRole兜底校验通过");
+ return R.ok("拥有system:user:export权限,或拥有admin/operator角色,校验通过");
+ }
+
+ // ====================== 特殊场景:临时权限/注解覆盖 ======================
+
+ /**
+ * 场景14:SaIgnore局部覆盖(方法注解覆盖类注解,若有)
+ * 假设类上有@SaCheckLogin,方法上@SaIgnore会覆盖
+ */
+ @SaIgnore
+ @GetMapping("/special/ignoreOverride")
+ public R ignoreOverride() {
+ log.info("【场景14】SaIgnore覆盖类级别权限注解");
+ return R.ok("方法级SaIgnore覆盖类级别权限校验");
+ }
+
+ /**
+ * 场景15:临时权限校验(SaCheckPermission逻辑:临时权限>永久权限)
+ * 注:临时权限需通过SaToken API手动设置,如 SaHolder.getStpLogic().setTempPermission("system:temp:test")
+ */
+ @SaCheckPermission("system:temp:test")
+ @GetMapping("/special/tempPermission")
+ public R tempPermission() {
+ log.info("【场景15】临时权限(system:temp:test)校验通过");
+ return R.ok("临时权限校验通过(需先通过API设置临时权限)");
+ }
+
+ /**
+ * 场景16:登录类型指定(多端登录场景,如PC/APP/小程序)
+ * 注:需配合SaToken多账号体系配置
+ */
+ @SaCheckLogin(type = "PC") // 仅校验PC端的登录态
+ @GetMapping("/special/loginTypeSpecify")
+ public R loginTypeSpecify() {
+ log.info("【场景16】指定登录类型(PC)校验通过");
+ return R.ok("仅PC端登录态校验通过");
+ }
+}
From 7a603e6b93082bad1b9b04e2838aa186cf113cb3 Mon Sep 17 00:00:00 2001
From: AprilWind <2100166581@qq.com>
Date: Thu, 5 Mar 2026 12:11:20 +0800
Subject: [PATCH 2/4] =?UTF-8?q?update=20=E4=BC=98=E5=8C=96=E6=8E=A5?=
=?UTF-8?q?=E5=8F=A3=E6=8F=8F=E8=BF=B0=E6=96=87=E6=9C=AC=E5=B1=95=E7=A4=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../java/org/springdoc/api/AbstractOpenApiResource.java | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/springdoc/api/AbstractOpenApiResource.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/springdoc/api/AbstractOpenApiResource.java
index 781824efa..192588796 100644
--- a/ruoyi-common/ruoyi-common-doc/src/main/java/org/springdoc/api/AbstractOpenApiResource.java
+++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/springdoc/api/AbstractOpenApiResource.java
@@ -646,7 +646,11 @@ public abstract class AbstractOpenApiResource extends SpecFilter {
// boolean emptyOverrideDescription = StringUtils.isEmpty(operation.getDescription());
boolean emptyOverrideSummary = StringUtils.isEmpty(operation.getSummary());
if (!StringUtils.isEmpty(description)) {
- operation.setDescription(description + operation.getDescription());
+ if(StringUtils.isEmpty(operation.getDescription())){
+ operation.setDescription(description);
+ }else {
+ operation.setDescription(description + operation.getDescription());
+ }
}
// if there is a previously set description
// but no summary then it is intentional
From 3e08d94dbc25f75c9fb50e54cac8d7d4dab38f5b Mon Sep 17 00:00:00 2001
From: AprilWind <2100166581@qq.com>
Date: Thu, 5 Mar 2026 14:00:08 +0800
Subject: [PATCH 3/4] =?UTF-8?q?update=20=E4=BC=98=E5=8C=96satoken=E4=BE=9D?=
=?UTF-8?q?=E8=B5=96=E5=BC=95=E7=94=A8=EF=BC=8C=E5=87=8F=E5=B0=91=E8=80=A6?=
=?UTF-8?q?=E5=90=88=E6=80=A7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ruoyi-common/ruoyi-common-doc/pom.xml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/ruoyi-common/ruoyi-common-doc/pom.xml b/ruoyi-common/ruoyi-common-doc/pom.xml
index 77f8d703f..390d34f01 100644
--- a/ruoyi-common/ruoyi-common-doc/pom.xml
+++ b/ruoyi-common/ruoyi-common-doc/pom.xml
@@ -22,8 +22,8 @@
- org.dromara
- ruoyi-common-satoken
+ cn.dev33
+ sa-token-core
From 3b510c057d98e427268e3954371dfa53c03773b0 Mon Sep 17 00:00:00 2001
From: AprilWind <2100166581@qq.com>
Date: Thu, 5 Mar 2026 14:40:30 +0800
Subject: [PATCH 4/4] =?UTF-8?q?update=20=E5=A2=9E=E5=BC=BA=E6=8E=A5?=
=?UTF-8?q?=E5=8F=A3=E6=8F=8F=E8=BF=B0=EF=BC=8C=E5=90=88=E5=B9=B6JavaDoc?=
=?UTF-8?q?=E6=9D=83=E9=99=90=E4=BF=A1=E6=81=AF=E5=88=B0=E6=93=8D=E4=BD=9C?=
=?UTF-8?q?=E6=8F=8F=E8=BF=B0=E4=B8=AD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../common/doc/handler/OpenApiHandler.java | 14 +-
.../api/AbstractOpenApiResource.java | 1556 -----------------
2 files changed, 10 insertions(+), 1560 deletions(-)
delete mode 100644 ruoyi-common/ruoyi-common-doc/src/main/java/org/springdoc/api/AbstractOpenApiResource.java
diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/handler/OpenApiHandler.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/handler/OpenApiHandler.java
index 2add6d290..830665942 100644
--- a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/handler/OpenApiHandler.java
+++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/handler/OpenApiHandler.java
@@ -11,7 +11,6 @@ import io.swagger.v3.oas.models.Paths;
import io.swagger.v3.oas.models.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
-import org.dromara.common.core.utils.ObjectUtils;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.doc.core.enhancer.SaTokenMetadataResolver;
import org.dromara.common.doc.core.model.SaTokenSecurityMetadata;
@@ -229,14 +228,21 @@ public class OpenApiHandler extends OpenAPIService {
else
securityParser.buildSecurityRequirement(securityRequirements, operation);
}
-
+ String description = javadocProvider.get().getMethodJavadocDescription(handlerMethod.getMethod());
+ String summary = javadocProvider.get().getFirstSentence(description);
+ if (StringUtils.isNotBlank(description)){
+ operation.setSummary(summary);
+ }
// 调用SaToken解析器提取JavaDoc中的权限信息
if (saTokenJavadocResolver.supports(handlerMethod)) {
SaTokenSecurityMetadata metadata = new SaTokenSecurityMetadata();
saTokenJavadocResolver.resolve(handlerMethod, operation, metadata);
- operation.setDescription(metadata.toMarkdownString());
+ String markdownString = metadata.toMarkdownString();
+ if (StringUtils.isNotBlank(markdownString)) {
+ description = description + markdownString;
+ }
}
-
+ operation.setDescription(description);
return operation;
}
diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/springdoc/api/AbstractOpenApiResource.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/springdoc/api/AbstractOpenApiResource.java
deleted file mode 100644
index 192588796..000000000
--- a/ruoyi-common/ruoyi-common-doc/src/main/java/org/springdoc/api/AbstractOpenApiResource.java
+++ /dev/null
@@ -1,1556 +0,0 @@
-/*
- *
- * *
- * * *
- * * * *
- * * * * *
- * * * * * * Copyright 2019-2025 the original author or authors.
- * * * * * *
- * * * * * * Licensed under the Apache License, Version 2.0 (the "License");
- * * * * * * you may not use this file except in compliance with the License.
- * * * * * * You may obtain a copy of the License at
- * * * * * *
- * * * * * * https://www.apache.org/licenses/LICENSE-2.0
- * * * * * *
- * * * * * * Unless required by applicable law or agreed to in writing, software
- * * * * * * distributed under the License is distributed on an "AS IS" BASIS,
- * * * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * * * * * * See the License for the specific language governing permissions and
- * * * * * * limitations under the License.
- * * * * *
- * * * *
- * * *
- * *
- *
- */
-
-package org.springdoc.api;
-
-import java.io.UnsupportedEncodingException;
-import java.lang.reflect.Method;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URLDecoder;
-import java.nio.charset.StandardCharsets;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.TreeSet;
-import java.util.concurrent.Executors;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import java.util.stream.Collectors;
-
-import com.fasterxml.jackson.annotation.JsonView;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
-import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature;
-import io.swagger.v3.core.filter.SpecFilter;
-import io.swagger.v3.core.util.ReflectionUtils;
-import io.swagger.v3.oas.annotations.Hidden;
-import io.swagger.v3.oas.annotations.Webhook;
-import io.swagger.v3.oas.annotations.Webhooks;
-import io.swagger.v3.oas.annotations.callbacks.Callback;
-import io.swagger.v3.oas.annotations.enums.ParameterIn;
-import io.swagger.v3.oas.models.Components;
-import io.swagger.v3.oas.models.OpenAPI;
-import io.swagger.v3.oas.models.Operation;
-import io.swagger.v3.oas.models.PathItem;
-import io.swagger.v3.oas.models.PathItem.HttpMethod;
-import io.swagger.v3.oas.models.Paths;
-import io.swagger.v3.oas.models.SpecVersion;
-import io.swagger.v3.oas.models.media.Schema;
-import io.swagger.v3.oas.models.media.StringSchema;
-import io.swagger.v3.oas.models.parameters.Parameter;
-import io.swagger.v3.oas.models.responses.ApiResponses;
-import io.swagger.v3.oas.models.servers.Server;
-import org.apache.commons.lang3.ArrayUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springdoc.core.annotations.RouterOperations;
-import org.springdoc.core.customizers.DataRestRouterOperationCustomizer;
-import org.springdoc.core.customizers.GlobalOperationComponentsCustomizer;
-import org.springdoc.core.customizers.OpenApiLocaleCustomizer;
-import org.springdoc.core.customizers.OperationCustomizer;
-import org.springdoc.core.customizers.RouterOperationCustomizer;
-import org.springdoc.core.customizers.SpringDocCustomizers;
-import org.springdoc.core.fn.AbstractRouterFunctionVisitor;
-import org.springdoc.core.fn.RouterFunctionData;
-import org.springdoc.core.fn.RouterOperation;
-import org.springdoc.core.models.MethodAttributes;
-import org.springdoc.core.properties.SpringDocConfigProperties;
-import org.springdoc.core.properties.SpringDocConfigProperties.ApiDocs.OpenApiVersion;
-import org.springdoc.core.properties.SpringDocConfigProperties.GroupConfig;
-import org.springdoc.core.providers.ActuatorProvider;
-import org.springdoc.core.providers.CloudFunctionProvider;
-import org.springdoc.core.providers.JavadocProvider;
-import org.springdoc.core.providers.ObjectMapperProvider;
-import org.springdoc.core.providers.SpringDocProviders;
-import org.springdoc.core.service.AbstractRequestService;
-import org.springdoc.core.service.GenericParameterService;
-import org.springdoc.core.service.GenericResponseService;
-import org.springdoc.core.service.OpenAPIService;
-import org.springdoc.core.service.OperationService;
-import org.springdoc.core.utils.PropertyResolverUtils;
-import org.springdoc.core.utils.SpringDocAnnotationsUtils;
-import org.springdoc.core.utils.SpringDocUtils;
-
-import org.springframework.aop.support.AopUtils;
-import org.springframework.beans.factory.ObjectFactory;
-import org.springframework.context.ApplicationContext;
-import org.springframework.core.annotation.AnnotatedElementUtils;
-import org.springframework.core.annotation.AnnotationUtils;
-import org.springframework.core.env.Environment;
-import org.springframework.util.AntPathMatcher;
-import org.springframework.util.ClassUtils;
-import org.springframework.util.CollectionUtils;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestMethod;
-import org.springframework.web.bind.annotation.ResponseBody;
-import org.springframework.web.method.HandlerMethod;
-
-import static org.springdoc.core.converters.SchemaPropertyDeprecatingConverter.isDeprecated;
-import static org.springdoc.core.utils.Constants.ACTUATOR_DEFAULT_GROUP;
-import static org.springdoc.core.utils.Constants.DOT;
-import static org.springdoc.core.utils.Constants.OPERATION_ATTRIBUTE;
-import static org.springdoc.core.utils.Constants.SPRING_MVC_SERVLET_PATH;
-import static org.springdoc.core.utils.SpringDocUtils.cloneViaJson;
-import static org.springframework.util.AntPathMatcher.DEFAULT_PATH_SEPARATOR;
-
-/**
- * The type Abstract open api resource.
- *
- * @author bnasslahsen
- * @author kevinraddatz
- * @author hyeonisism
- * @author doljae
- * @author zdary
- * @author Haotian Zhang
- */
-public abstract class AbstractOpenApiResource extends SpecFilter {
-
- /**
- * The constant LOGGER.
- */
- private static final Logger LOGGER = LoggerFactory.getLogger(AbstractOpenApiResource.class);
-
- /**
- * The constant ADDITIONAL_REST_CONTROLLERS.
- */
- private static final List> ADDITIONAL_REST_CONTROLLERS = Collections.synchronizedList(new ArrayList<>());
-
- /**
- * The constant HIDDEN_REST_CONTROLLERS.
- */
- private static final List> HIDDEN_REST_CONTROLLERS = Collections.synchronizedList(new ArrayList<>());
-
- /**
- * The constant MODEL_AND_VIEW_CLASS.
- */
- private static Class> modelAndViewClass;
-
- /**
- * The Spring doc config properties.
- */
- protected final SpringDocConfigProperties springDocConfigProperties;
-
- /**
- * The Group name.
- */
- protected final String groupName;
-
- /**
- * The Spring doc providers.
- */
- protected final SpringDocProviders springDocProviders;
-
- /**
- * The Spring doc customizers.
- */
- protected final SpringDocCustomizers springDocCustomizers;
-
- /**
- * The open api builder object factory.
- */
- private final ObjectFactory openAPIBuilderObjectFactory;
-
- /**
- * The Request builder.
- */
- private final AbstractRequestService requestBuilder;
-
- /**
- * The Response builder.
- */
- private final GenericResponseService responseBuilder;
-
- /**
- * The Operation parser.
- */
- private final OperationService operationParser;
-
- /**
- * The Ant path matcher.
- */
- private final AntPathMatcher antPathMatcher = new AntPathMatcher();
-
- /**
- * The Reentrant lock.
- */
- private final Lock reentrantLock = new ReentrantLock();
-
- /**
- * The Path pattern.
- */
- private final Pattern pathPattern = Pattern.compile("\\{(.*?)}");
-
- /**
- * The Open api builder.
- */
- protected OpenAPIService openAPIService;
-
-
- /**
- * Instantiates a new Abstract open api resource.
- *
- * @param groupName the group name
- * @param openAPIBuilderObjectFactory the open api builder object factory
- * @param requestBuilder the request builder
- * @param responseBuilder the response builder
- * @param operationParser the operation parser
- * @param springDocConfigProperties the spring doc config properties
- * @param springDocProviders the spring doc providers
- * @param springDocCustomizers the spring doc customizers
- */
- protected AbstractOpenApiResource(String groupName, ObjectFactory openAPIBuilderObjectFactory,
- AbstractRequestService requestBuilder,
- GenericResponseService responseBuilder, OperationService operationParser,
- SpringDocConfigProperties springDocConfigProperties, SpringDocProviders springDocProviders, SpringDocCustomizers springDocCustomizers) {
- super();
- this.groupName = Objects.requireNonNull(groupName, "groupName");
- this.openAPIBuilderObjectFactory = openAPIBuilderObjectFactory;
- this.openAPIService = openAPIBuilderObjectFactory.getObject();
- this.requestBuilder = requestBuilder;
- this.responseBuilder = responseBuilder;
- this.operationParser = operationParser;
- this.springDocProviders = springDocProviders;
- this.springDocCustomizers = springDocCustomizers;
- this.springDocConfigProperties = springDocConfigProperties;
- if (springDocConfigProperties.isPreLoadingEnabled()) {
- if (CollectionUtils.isEmpty(springDocConfigProperties.getPreLoadingLocales())) {
- Executors.newSingleThreadExecutor().execute(this::getOpenApi);
- }
- else {
- for (String locale : springDocConfigProperties.getPreLoadingLocales()) {
- Executors.newSingleThreadExecutor().execute(() -> this.getOpenApi(null, Locale.forLanguageTag(locale)));
- }
- }
- }
- }
-
- /**
- * Add rest controllers.
- *
- * @param classes the classes
- */
- public static void addRestControllers(Class>... classes) {
- ADDITIONAL_REST_CONTROLLERS.addAll(Arrays.asList(classes));
- }
-
- /**
- * Add hidden rest controllers.
- *
- * @param classes the classes
- */
- public static void addHiddenRestControllers(Class>... classes) {
- HIDDEN_REST_CONTROLLERS.addAll(Arrays.asList(classes));
- }
-
- /**
- * Add hidden rest controllers.
- *
- * @param classes the classes
- */
- public static void addHiddenRestControllers(String... classes) {
- Set> hiddenClasses = new HashSet<>();
- for (String aClass : classes) {
- try {
- hiddenClasses.add(Class.forName(aClass));
- }
- catch (ClassNotFoundException e) {
- LOGGER.warn("The following class doesn't exist and cannot be hidden: {}", aClass);
- }
- }
- HIDDEN_REST_CONTROLLERS.addAll(hiddenClasses);
- }
-
- /**
- * Contains response body boolean.
- *
- * @param handlerMethod the handler method
- * @return the boolean
- */
- public static boolean containsResponseBody(HandlerMethod handlerMethod) {
- ResponseBody responseBodyAnnotation = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), ResponseBody.class);
- if (responseBodyAnnotation == null)
- responseBodyAnnotation = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), ResponseBody.class);
- return responseBodyAnnotation != null;
- }
-
- /**
- * Is hidden rest controllers boolean.
- *
- * @param rawClass the raw class
- * @return the boolean
- */
- public static boolean isHiddenRestControllers(Class> rawClass) {
- return HIDDEN_REST_CONTROLLERS.stream().anyMatch(clazz -> ClassUtils.getUserClass(clazz).isAssignableFrom(rawClass));
- }
-
- /**
- * Sets model and view class.
- *
- * @param modelAndViewClass the model and view class
- */
- public static void setModelAndViewClass(Class> modelAndViewClass) {
- AbstractOpenApiResource.modelAndViewClass = modelAndViewClass;
- }
-
- /**
- * Gets open api.
- */
- private void getOpenApi() {
- this.getOpenApi(null, Locale.getDefault());
- }
-
- /**
- * Gets open api.
- *
- * @param locale the locale
- * @return the open api
- */
- protected OpenAPI getOpenApi(Locale locale) {
- return this.getOpenApi(null, locale);
- }
-
- /**
- * Gets open api.
- *
- * @param serverBaseUrl the server base url
- * @param locale the locale
- * @return the open api
- */
- protected OpenAPI getOpenApi(String serverBaseUrl, Locale locale) {
- this.reentrantLock.lock();
- try {
- final OpenAPI openAPI;
- final Locale finalLocale = selectLocale(locale);
- if (openAPIService.getCachedOpenAPI(finalLocale) == null || springDocConfigProperties.isCacheDisabled()) {
- Instant start = Instant.now();
- openAPI = openAPIService.build(finalLocale);
- Map mappingsMap = openAPIService.getMappingsMap().entrySet().stream()
- .filter(controller -> (AnnotationUtils.findAnnotation(controller.getValue().getClass(),
- Hidden.class) == null))
- .filter(controller -> !isHiddenRestControllers(controller.getValue().getClass()))
- .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a1, a2) -> a1));
-
- Map findControllerAdvice = openAPIService.getControllerAdviceMap();
- if (OpenApiVersion.OPENAPI_3_1 == springDocConfigProperties.getApiDocs().getVersion()) {
- openAPI.openapi(OpenApiVersion.OPENAPI_3_1.getVersion());
- openAPI.specVersion(SpecVersion.V31);
- calculateWebhooks(openAPI, locale);
- }
- if (springDocConfigProperties.isDefaultOverrideWithGenericResponse()) {
- if (!CollectionUtils.isEmpty(mappingsMap))
- findControllerAdvice.putAll(mappingsMap);
- responseBuilder.buildGenericResponse(openAPI.getComponents(), findControllerAdvice, finalLocale);
- }
- getPaths(mappingsMap, finalLocale, openAPI);
-
- if (springDocConfigProperties.isTrimKotlinIndent())
- this.trimIndent(openAPI);
-
- Optional cloudFunctionProviderOptional = springDocProviders.getSpringCloudFunctionProvider();
- cloudFunctionProviderOptional.ifPresent(cloudFunctionProvider -> {
- List routerOperationList = cloudFunctionProvider.getRouterOperations(openAPI);
- if (!CollectionUtils.isEmpty(routerOperationList))
- this.calculatePath(routerOperationList, locale, openAPI);
- }
- );
- if (!CollectionUtils.isEmpty(openAPI.getServers()))
- openAPIService.setServersPresent(true);
- else
- openAPIService.setServersPresent(false);
- openAPIService.updateServers(serverBaseUrl, openAPI);
-
- if (springDocConfigProperties.isRemoveBrokenReferenceDefinitions())
- this.removeBrokenReferenceDefinitions(openAPI);
-
- // run the optional customizers
- List servers = openAPI.getServers();
- List serversCopy = cloneViaJson(servers, new TypeReference>() {}, springDocProviders.jsonMapper());
-
- openAPIService.getContext().getBeansOfType(OpenApiLocaleCustomizer.class).values().forEach(openApiLocaleCustomizer -> openApiLocaleCustomizer.customise(openAPI, finalLocale));
- springDocCustomizers.getOpenApiCustomizers().ifPresent(apiCustomizers -> apiCustomizers.forEach(openApiCustomizer -> openApiCustomizer.customise(openAPI)));
- if (!CollectionUtils.isEmpty(openAPI.getServers()) && !openAPI.getServers().equals(serversCopy))
- openAPIService.setServersPresent(true);
-
- openAPIService.setCachedOpenAPI(openAPI, finalLocale);
-
- LOGGER.info("Init duration for springdoc-openapi is: {} ms",
- Duration.between(start, Instant.now()).toMillis());
- }
- else {
- LOGGER.debug("Fetching openApi document from cache");
- openAPI = openAPIService.getCachedOpenAPI(finalLocale);
- openAPIService.updateServers(serverBaseUrl, openAPI);
- }
- return openAPI;
- }
- finally {
- SpringDocAnnotationsUtils.clearCache(operationParser.getJavadocProvider());
- this.reentrantLock.unlock();
- }
- }
-
- private Locale selectLocale(Locale inputLocale) {
- List allowedLocales = springDocConfigProperties.getAllowedLocales();
- if (!CollectionUtils.isEmpty(allowedLocales)) {
- Locale bestMatchingAllowedLocale = Locale.lookup(
- Locale.LanguageRange.parse(inputLocale.toLanguageTag()),
- allowedLocales.stream().map(Locale::forLanguageTag).toList()
- );
-
- return bestMatchingAllowedLocale == null ? Locale.forLanguageTag(allowedLocales.get(0)) : bestMatchingAllowedLocale;
- }
-
- return inputLocale == null ? Locale.getDefault() : inputLocale;
- }
-
- /**
- * Indents are removed for properties that are mainly used as “explanations” using Open API.
- *
- * @param openAPI the open api
- */
- private void trimIndent(OpenAPI openAPI) {
- trimComponents(openAPI);
- trimPaths(openAPI);
- }
-
- /**
- * Trim the indent for descriptions in the 'components' of open api.
- *
- * @param openAPI the open api
- */
- private void trimComponents(OpenAPI openAPI) {
- final PropertyResolverUtils propertyResolverUtils = operationParser.getPropertyResolverUtils();
- if (openAPI.getComponents() == null || openAPI.getComponents().getSchemas() == null) {
- return;
- }
- for (Schema> schema : openAPI.getComponents().getSchemas().values()) {
- schema.description(propertyResolverUtils.trimIndent(schema.getDescription()));
- if (schema.getProperties() == null) {
- continue;
- }
- for (Object prop : schema.getProperties().values()) {
- if (prop instanceof Schema> schemaProp) {
- schemaProp.setDescription(propertyResolverUtils.trimIndent(schemaProp.getDescription()));
- }
- }
- }
- }
-
- /**
- * Trim the indent for descriptions in the 'paths' of open api.
- *
- * @param openAPI the open api
- */
- private void trimPaths(OpenAPI openAPI) {
- final PropertyResolverUtils propertyResolverUtils = operationParser.getPropertyResolverUtils();
- if (openAPI.getPaths() == null) {
- return;
- }
- for (PathItem value : openAPI.getPaths().values()) {
- value.setDescription(propertyResolverUtils.trimIndent(value.getDescription()));
- trimIndentOperation(value.getGet());
- trimIndentOperation(value.getPut());
- trimIndentOperation(value.getPost());
- trimIndentOperation(value.getDelete());
- trimIndentOperation(value.getOptions());
- trimIndentOperation(value.getHead());
- trimIndentOperation(value.getPatch());
- trimIndentOperation(value.getTrace());
- }
- }
-
- /**
- * Trim the indent for 'operation'
- *
- * @param operation the operation
- */
- private void trimIndentOperation(Operation operation) {
- final PropertyResolverUtils propertyResolverUtils = operationParser.getPropertyResolverUtils();
- if (operation == null) {
- return;
- }
- operation.setSummary(propertyResolverUtils.trimIndent(operation.getSummary()));
- operation.setDescription(propertyResolverUtils.trimIndent(operation.getDescription()));
- }
-
- /**
- * Gets paths.
- *
- * @param findRestControllers the find rest controllers
- * @param locale the locale
- * @param openAPI the open api
- */
- protected abstract void getPaths(Map findRestControllers, Locale locale, OpenAPI openAPI);
-
-
- /**
- * Calculate webhooks.
- *
- * @param calculatedOpenAPI the calculated open api
- * @param locale the locale
- */
- protected void calculateWebhooks(OpenAPI calculatedOpenAPI, Locale locale) {
- Class>[] classes = openAPIService.getWebhooksClasses();
- Class>[] refinedClasses = Arrays.stream(classes)
- .filter(clazz -> isPackageToScan(clazz.getPackage()))
- .toArray(Class>[]::new);
- Webhooks[] webhooksAttr = openAPIService.getWebhooks(refinedClasses);
- if (ArrayUtils.isEmpty(webhooksAttr))
- return;
- var webhooks = Arrays.stream(webhooksAttr).map(Webhooks::value).flatMap(Arrays::stream).toArray(Webhook[]::new);
- Arrays.stream(webhooks).forEach(webhook -> {
- io.swagger.v3.oas.annotations.Operation apiOperation = webhook.operation();
- Operation operation = new Operation();
- MethodAttributes methodAttributes = new MethodAttributes(springDocConfigProperties.getDefaultConsumesMediaType(),
- springDocConfigProperties.getDefaultProducesMediaType(), locale);
- operationParser.parse(apiOperation, operation, calculatedOpenAPI, methodAttributes);
- PathItem pathItem = new PathItem().post(operation);
- calculatedOpenAPI.addWebhooks(webhook.name(), pathItem);
- });
- }
-
- /**
- * Calculate path.
- *
- * @param handlerMethod the handler method
- * @param routerOperation the router operation
- * @param locale the locale
- * @param openAPI the open api
- */
- protected void calculatePath(HandlerMethod handlerMethod, RouterOperation routerOperation, Locale locale, OpenAPI openAPI) {
- routerOperation = customizeRouterOperation(routerOperation, handlerMethod);
-
- String operationPath = routerOperation.getPath();
- Set requestMethods = new TreeSet<>(Arrays.asList(routerOperation.getMethods()));
- io.swagger.v3.oas.annotations.Operation apiOperation = routerOperation.getOperation();
- String[] methodConsumes = routerOperation.getConsumes();
- String[] methodProduces = routerOperation.getProduces();
- String[] headers = routerOperation.getHeaders();
- Map queryParams = routerOperation.getQueryParams();
-
- Components components = openAPI.getComponents();
- Paths paths = openAPI.getPaths();
-
- Map operationMap = null;
- if (paths.containsKey(operationPath)) {
- PathItem pathItem = paths.get(operationPath);
- operationMap = pathItem.readOperationsMap();
- }
-
- JavadocProvider javadocProvider = operationParser.getJavadocProvider();
-
- for (RequestMethod requestMethod : requestMethods) {
- Operation existingOperation = getExistingOperation(operationMap, requestMethod);
- Method method = handlerMethod.getMethod();
- // skip hidden operations
- if (operationParser.isHidden(method))
- continue;
-
- RequestMapping reqMappingClass = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(),
- RequestMapping.class);
-
- MethodAttributes methodAttributes = new MethodAttributes(springDocConfigProperties.getDefaultConsumesMediaType(), springDocConfigProperties.getDefaultProducesMediaType(), methodConsumes, methodProduces, headers, locale);
- methodAttributes.setMethodOverloaded(existingOperation != null);
- //Use the javadoc return if present
- if (javadocProvider != null) {
- methodAttributes.setJavadocReturn(javadocProvider.getMethodJavadocReturn(handlerMethod.getMethod()));
- }
-
- if (reqMappingClass != null) {
- methodAttributes.setClassConsumes(reqMappingClass.consumes());
- methodAttributes.setClassProduces(reqMappingClass.produces());
- }
-
- methodAttributes.calculateHeadersForClass(method.getDeclaringClass());
- methodAttributes.calculateConsumesProduces(method);
-
- Operation operation = (existingOperation != null) ? existingOperation : new Operation();
-
- if (isDeprecated(method))
- operation.setDeprecated(true);
-
- // Add documentation from operation annotation
- if (apiOperation == null || StringUtils.isBlank(apiOperation.operationId()))
- apiOperation = AnnotatedElementUtils.findMergedAnnotation(method,
- io.swagger.v3.oas.annotations.Operation.class);
-
- calculateJsonView(apiOperation, methodAttributes, method);
- if (apiOperation != null)
- openAPI = operationParser.parse(apiOperation, operation, openAPI, methodAttributes);
- fillParametersList(operation, queryParams, methodAttributes);
-
- // compute tags
- operation = openAPIService.buildTags(handlerMethod, operation, openAPI, locale);
-
- io.swagger.v3.oas.annotations.parameters.RequestBody requestBodyDoc = AnnotatedElementUtils.findMergedAnnotation(method,
- io.swagger.v3.oas.annotations.parameters.RequestBody.class);
-
- // RequestBody in Operation
- requestBuilder.getRequestBodyBuilder()
- .buildRequestBodyFromDoc(requestBodyDoc, methodAttributes, components,
- methodAttributes.getJsonViewAnnotationForRequestBody(), locale)
- .ifPresent(operation::setRequestBody);
- // requests
- operation = requestBuilder.build(handlerMethod, requestMethod, operation, methodAttributes, openAPI);
-
- // responses
- ApiResponses apiResponses = responseBuilder.build(components, handlerMethod, operation, methodAttributes);
- operation.setResponses(apiResponses);
-
- // get javadoc method description
- if (javadocProvider != null) {
- String description = javadocProvider.getMethodJavadocDescription(handlerMethod.getMethod());
- String summary = javadocProvider.getFirstSentence(description);
-// boolean emptyOverrideDescription = StringUtils.isEmpty(operation.getDescription());
- boolean emptyOverrideSummary = StringUtils.isEmpty(operation.getSummary());
- if (!StringUtils.isEmpty(description)) {
- if(StringUtils.isEmpty(operation.getDescription())){
- operation.setDescription(description);
- }else {
- operation.setDescription(description + operation.getDescription());
- }
- }
- // if there is a previously set description
- // but no summary then it is intentional
- // we keep it as is
- if (!StringUtils.isEmpty(summary) && emptyOverrideSummary) {
- operation.setSummary(javadocProvider.getFirstSentence(description));
- }
- }
-
- Set apiCallbacks = AnnotatedElementUtils.findMergedRepeatableAnnotations(method, io.swagger.v3.oas.annotations.callbacks.Callback.class);
-
- // callbacks
- buildCallbacks(openAPI, methodAttributes, operation, apiCallbacks);
-
- // allow for customisation
- operation = customizeOperation(operation, components, handlerMethod);
-
- if (StringUtils.contains(operationPath, "*")) {
- Matcher matcher = pathPattern.matcher(operationPath);
- while (matcher.find()) {
- String pathParam = matcher.group(1);
- String newPathParam = pathParam.replace("*", "");
- operationPath = operationPath.replace("{" + pathParam + "}", "{" + newPathParam + "}");
- }
- }
-
- PathItem pathItemObject = buildPathItem(requestMethod, operation, operationPath, paths);
- paths.addPathItem(operationPath, pathItemObject);
- }
- }
-
- /**
- * Build callbacks.
- *
- * @param openAPI the open api
- * @param methodAttributes the method attributes
- * @param operation the operation
- * @param apiCallbacks the api callbacks
- */
- private void buildCallbacks(OpenAPI openAPI, MethodAttributes methodAttributes, Operation operation, Set apiCallbacks) {
- if (!CollectionUtils.isEmpty(apiCallbacks))
- operationParser.buildCallbacks(apiCallbacks, openAPI, methodAttributes)
- .ifPresent(operation::setCallbacks);
- }
-
- /**
- * Calculate path.
- *
- * @param routerOperationList the router operation list
- * @param locale the locale
- * @param openAPI the open api
- */
- protected void calculatePath(List routerOperationList, Locale locale, OpenAPI openAPI) {
- ApplicationContext applicationContext = openAPIService.getContext();
- if (!CollectionUtils.isEmpty(routerOperationList)) {
- Collections.sort(routerOperationList);
- for (RouterOperation routerOperation : routerOperationList) {
- if (routerOperation.getBeanClass() != null && !Void.class.equals(routerOperation.getBeanClass())) {
- Object handlerBean = applicationContext.getBean(routerOperation.getBeanClass());
- HandlerMethod handlerMethod = null;
-
- if (StringUtils.isNotBlank(routerOperation.getBeanMethod())) {
- try {
- if (ArrayUtils.isEmpty(routerOperation.getParameterTypes())) {
- Method[] declaredMethods = org.springframework.util.ReflectionUtils.getAllDeclaredMethods(AopUtils.getTargetClass(handlerBean));
- Optional methodOptional = Arrays.stream(declaredMethods)
- .filter(method -> routerOperation.getBeanMethod().equals(method.getName()) && method.getParameters().length == 0)
- .findAny();
- if (!methodOptional.isPresent())
- methodOptional = Arrays.stream(declaredMethods)
- .filter(method1 -> routerOperation.getBeanMethod().equals(method1.getName()))
- .findAny();
- if (methodOptional.isPresent())
- handlerMethod = new HandlerMethod(handlerBean, methodOptional.get());
- }
- else
- handlerMethod = new HandlerMethod(handlerBean, routerOperation.getBeanMethod(), routerOperation.getParameterTypes());
- }
- catch (NoSuchMethodException e) {
- LOGGER.error(e.getMessage());
- }
- if (handlerMethod != null && isFilterCondition(handlerMethod, routerOperation.getPath(), routerOperation.getProduces(), routerOperation.getConsumes(), routerOperation.getHeaders()))
- calculatePath(handlerMethod, routerOperation, locale, openAPI);
- }
- }
- else if (routerOperation.getOperation() != null && StringUtils.isNotBlank(routerOperation.getOperation().operationId()) && isFilterCondition(routerOperation.getPath(), routerOperation.getProduces(), routerOperation.getConsumes(), routerOperation.getHeaders())) {
- calculatePath(routerOperation, locale, openAPI);
- }
- else if (routerOperation.getOperationModel() != null && StringUtils.isNotBlank(routerOperation.getOperationModel().getOperationId()) && isFilterCondition(routerOperation.getPath(), routerOperation.getProduces(), routerOperation.getConsumes(), routerOperation.getHeaders())) {
- calculatePath(routerOperation, locale, openAPI);
- }
- }
- }
- }
-
- /**
- * Calculate path.
- *
- * @param routerOperation the router operation
- * @param locale the locale
- * @param openAPI the open api
- */
- protected void calculatePath(RouterOperation routerOperation, Locale locale, OpenAPI openAPI) {
- routerOperation = customizeDataRestRouterOperation(routerOperation);
-
- String operationPath = routerOperation.getPath();
- io.swagger.v3.oas.annotations.Operation apiOperation = routerOperation.getOperation();
- String[] methodConsumes = routerOperation.getConsumes();
- String[] methodProduces = routerOperation.getProduces();
- String[] headers = routerOperation.getHeaders();
- Map queryParams = routerOperation.getQueryParams();
-
- Paths paths = openAPI.getPaths();
- Map operationMap = null;
- if (paths.containsKey(operationPath)) {
- PathItem pathItem = paths.get(operationPath);
- operationMap = pathItem.readOperationsMap();
- }
- for (RequestMethod requestMethod : routerOperation.getMethods()) {
- Operation existingOperation = getExistingOperation(operationMap, requestMethod);
- MethodAttributes methodAttributes = new MethodAttributes(springDocConfigProperties.getDefaultConsumesMediaType(), springDocConfigProperties.getDefaultProducesMediaType(), methodConsumes, methodProduces, headers, locale);
- methodAttributes.setMethodOverloaded(existingOperation != null);
- Operation operation = getOperation(routerOperation, existingOperation);
- if (apiOperation != null)
- openAPI = operationParser.parse(apiOperation, operation, openAPI, methodAttributes);
-
- String operationId = operation.getOperationId();
- operation.setOperationId(operationId);
-
- fillParametersList(operation, queryParams, methodAttributes);
- if (!CollectionUtils.isEmpty(operation.getParameters()))
- operation.getParameters().stream()
- .filter(parameter -> StringUtils.isEmpty(parameter.get$ref()))
- .forEach(parameter -> {
- if (parameter.getSchema() == null)
- parameter.setSchema(new StringSchema());
- if (parameter.getIn() == null)
- parameter.setIn(ParameterIn.QUERY.toString());
- }
- );
-
- PathItem pathItemObject = buildPathItem(requestMethod, operation, operationPath, paths);
- paths.addPathItem(operationPath, pathItemObject);
- }
- }
-
- /**
- * Customize data rest router operation.
- *
- * @param routerOperation the router operation
- * @return the router operation
- */
- private RouterOperation customizeDataRestRouterOperation(RouterOperation routerOperation) {
- Optional> optionalDataRestRouterOperationCustomizers = springDocCustomizers.getDataRestRouterOperationCustomizers();
- if (optionalDataRestRouterOperationCustomizers.isPresent()) {
- Set dataRestRouterOperationCustomizerList = optionalDataRestRouterOperationCustomizers.get();
- for (DataRestRouterOperationCustomizer dataRestRouterOperationCustomizer : dataRestRouterOperationCustomizerList) {
- routerOperation = dataRestRouterOperationCustomizer.customize(routerOperation);
- }
- }
- return routerOperation;
- }
-
- /**
- * Calculate path.
- *
- * @param handlerMethod the handler method
- * @param operationPath the operation path
- * @param requestMethods the request methods
- * @param consumes the consumes
- * @param produces the produces
- * @param headers the headers
- * @param params the params
- * @param locale the locale
- * @param openAPI the open api
- */
- protected void calculatePath(HandlerMethod handlerMethod, String operationPath,
- Set requestMethods, String[] consumes, String[] produces, String[] headers, String[] params, Locale locale, OpenAPI openAPI) {
- this.calculatePath(handlerMethod, new RouterOperation(operationPath, requestMethods.toArray(new RequestMethod[requestMethods.size()]), consumes, produces, headers, params), locale, openAPI);
- }
-
- /**
- * Gets router function paths.
- *
- * @param beanName the bean name
- * @param routerFunctionVisitor the router function visitor
- * @param locale the locale
- * @param openAPI the open api
- */
- protected void getRouterFunctionPaths(String beanName, AbstractRouterFunctionVisitor routerFunctionVisitor,
- Locale locale, OpenAPI openAPI) {
- boolean withRouterOperation = routerFunctionVisitor.getRouterFunctionDatas().stream()
- .anyMatch(routerFunctionData -> routerFunctionData.getAttributes().containsKey(OPERATION_ATTRIBUTE));
- if (withRouterOperation) {
- List operationList = routerFunctionVisitor.getRouterFunctionDatas().stream().map(RouterOperation::new).collect(Collectors.toList());
- calculatePath(operationList, locale, openAPI);
- }
- else {
- List routerOperationList = new ArrayList<>();
- ApplicationContext applicationContext = openAPIService.getContext();
- RouterOperations routerOperations = applicationContext.findAnnotationOnBean(beanName, RouterOperations.class);
- if (routerOperations == null) {
- org.springdoc.core.annotations.RouterOperation routerOperation = applicationContext.findAnnotationOnBean(beanName, org.springdoc.core.annotations.RouterOperation.class);
- if (routerOperation != null)
- routerOperationList.add(routerOperation);
- }
- else
- routerOperationList.addAll(Arrays.asList(routerOperations.value()));
- if (routerOperationList.size() == 1)
- calculatePath(routerOperationList.stream().map(routerOperation -> new RouterOperation(routerOperation, routerFunctionVisitor.getRouterFunctionDatas().get(0))).collect(Collectors.toList()), locale, openAPI);
- else {
- List operationList = routerOperationList.stream().map(RouterOperation::new).collect(Collectors.toList());
- mergeRouters(routerFunctionVisitor.getRouterFunctionDatas(), operationList);
- calculatePath(operationList, locale, openAPI);
- }
- }
- }
-
- /**
- * Is filter condition boolean.
- *
- * @param handlerMethod the handler method
- * @param operationPath the operation path
- * @param produces the produces
- * @param consumes the consumes
- * @param headers the headers
- * @return the boolean
- */
- protected boolean isFilterCondition(HandlerMethod handlerMethod, String operationPath, String[] produces, String[] consumes, String[] headers) {
- return isMethodToFilter(handlerMethod)
- && isPackageToScan(handlerMethod.getBeanType().getPackage())
- && isFilterCondition(operationPath, produces, consumes, headers);
- }
-
- /**
- * Is target method suitable for inclusion in current documentation/
- *
- * @param handlerMethod the method to check
- * @return whether the method should be included in the current OpenAPI definition
- */
- protected boolean isMethodToFilter(HandlerMethod handlerMethod) {
- return this.springDocCustomizers.getMethodFilters()
- .map(Collection::stream)
- .map(stream -> stream.allMatch(m -> m.isMethodToInclude(handlerMethod.getMethod())))
- .orElse(true);
- }
-
- /**
- * Is condition to match boolean.
- *
- * @param existingConditions the existing conditions
- * @param conditionType the condition type
- * @return the boolean
- */
- protected boolean isConditionToMatch(String[] existingConditions, ConditionType conditionType) {
- List conditionsToMatch = getConditionsToMatch(conditionType);
- if (CollectionUtils.isEmpty(conditionsToMatch)) {
- Optional optionalGroupConfig = springDocConfigProperties.getGroupConfigs().stream().filter(groupConfig -> this.groupName.equals(groupConfig.getGroup())).findAny();
- if (optionalGroupConfig.isPresent())
- conditionsToMatch = getConditionsToMatch(conditionType, optionalGroupConfig.get());
- }
- return CollectionUtils.isEmpty(conditionsToMatch)
- || (!ArrayUtils.isEmpty(existingConditions) && conditionsToMatch.size() == existingConditions.length && conditionsToMatch.containsAll(Arrays.asList(existingConditions)));
- }
-
- /**
- * Is package to scan boolean.
- *
- * @param aPackage the a package
- * @return the boolean
- */
- protected boolean isPackageToScan(Package aPackage) {
- if (aPackage == null)
- return true;
- final String packageName = aPackage.getName();
- List packagesToScan = springDocConfigProperties.getPackagesToScan();
- List packagesToExclude = springDocConfigProperties.getPackagesToExclude();
- if (CollectionUtils.isEmpty(packagesToScan)) {
- Optional optionalGroupConfig = springDocConfigProperties.getGroupConfigs().stream().filter(groupConfig -> this.groupName.equals(groupConfig.getGroup())).findAny();
- if (optionalGroupConfig.isPresent())
- packagesToScan = optionalGroupConfig.get().getPackagesToScan();
- }
- if (CollectionUtils.isEmpty(packagesToExclude)) {
- Optional optionalGroupConfig = springDocConfigProperties.getGroupConfigs().stream().filter(groupConfig -> this.groupName.equals(groupConfig.getGroup())).findAny();
- if (optionalGroupConfig.isPresent())
- packagesToExclude = optionalGroupConfig.get().getPackagesToExclude();
- }
- boolean include = CollectionUtils.isEmpty(packagesToScan)
- || packagesToScan.stream().anyMatch(pack -> packageName.equals(pack)
- || packageName.startsWith(pack + DOT));
- boolean exclude = !CollectionUtils.isEmpty(packagesToExclude)
- && (packagesToExclude.stream().anyMatch(pack -> packageName.equals(pack)
- || packageName.startsWith(pack + DOT)));
-
- return include && !exclude;
- }
-
- /**
- * Is path to match boolean.
- *
- * @param operationPath the operation path
- * @return the boolean
- */
- protected boolean isPathToMatch(String operationPath) {
- List pathsToMatch = springDocConfigProperties.getPathsToMatch();
- List pathsToExclude = springDocConfigProperties.getPathsToExclude();
- if (CollectionUtils.isEmpty(pathsToMatch)) {
- Optional optionalGroupConfig = springDocConfigProperties.getGroupConfigs().stream().filter(groupConfig -> this.groupName.equals(groupConfig.getGroup())).findAny();
- if (optionalGroupConfig.isPresent())
- pathsToMatch = optionalGroupConfig.get().getPathsToMatch();
- }
- if (CollectionUtils.isEmpty(pathsToExclude)) {
- Optional optionalGroupConfig = springDocConfigProperties.getGroupConfigs().stream().filter(groupConfig -> this.groupName.equals(groupConfig.getGroup())).findAny();
- if (optionalGroupConfig.isPresent())
- pathsToExclude = optionalGroupConfig.get().getPathsToExclude();
- }
- boolean include = CollectionUtils.isEmpty(pathsToMatch) || pathsToMatch.stream().anyMatch(pattern -> antPathMatcher.match(pattern, operationPath));
- boolean exclude = !CollectionUtils.isEmpty(pathsToExclude) && pathsToExclude.stream().anyMatch(pattern -> antPathMatcher.match(pattern, operationPath));
- return include && !exclude;
- }
-
- /**
- * Decode string.
- *
- * @param requestURI the request uri
- * @return the string
- */
- protected String decode(String requestURI) {
- try {
- return URLDecoder.decode(requestURI, StandardCharsets.UTF_8.toString());
- }
- catch (UnsupportedEncodingException e) {
- return requestURI;
- }
- }
-
- /**
- * Is additional rest controller boolean.
- *
- * @param rawClass the raw class
- * @return the boolean
- */
- protected boolean isAdditionalRestController(Class> rawClass) {
- return ADDITIONAL_REST_CONTROLLERS.stream().anyMatch(clazz -> ClassUtils.getUserClass(clazz).isAssignableFrom(rawClass));
- }
-
- /**
- * Is rest controller boolean.
- *
- * @param restControllers the rest controllers
- * @param handlerMethod the handler method
- * @param operationPath the operation path
- * @return the boolean
- */
- protected boolean isRestController(Map restControllers, HandlerMethod handlerMethod,
- String operationPath) {
- boolean hasOperationAnnotation = AnnotatedElementUtils.hasAnnotation(handlerMethod.getMethod(), io.swagger.v3.oas.annotations.Operation.class);
-
- return ((containsResponseBody(handlerMethod) || hasOperationAnnotation) && restControllers.containsKey(handlerMethod.getBean().toString()) || isAdditionalRestController(handlerMethod.getBeanType()))
- && operationPath.startsWith(DEFAULT_PATH_SEPARATOR)
- && (springDocConfigProperties.isModelAndViewAllowed() || modelAndViewClass == null || !modelAndViewClass.isAssignableFrom(handlerMethod.getMethod().getReturnType()));
- }
-
- /**
- * Gets default allowed http methods.
- *
- * @return the default allowed http methods
- */
- protected Set getDefaultAllowedHttpMethods() {
- RequestMethod[] allowedRequestMethods = { RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.PATCH, RequestMethod.DELETE, RequestMethod.OPTIONS, RequestMethod.HEAD };
- return new HashSet<>(Arrays.asList(allowedRequestMethods));
- }
-
- /**
- * Customise operation.
- *
- * @param operation the operation
- * @param components
- * @param handlerMethod the handler method
- * @return the operation
- */
- protected Operation customizeOperation(Operation operation, Components components, HandlerMethod handlerMethod) {
- Optional> optionalOperationCustomizers = springDocCustomizers.getOperationCustomizers();
- if (optionalOperationCustomizers.isPresent()) {
- Set operationCustomizerList = optionalOperationCustomizers.get();
- for (OperationCustomizer operationCustomizer : operationCustomizerList) {
- if (operationCustomizer instanceof GlobalOperationComponentsCustomizer globalOperationComponentsCustomizer)
- operation = globalOperationComponentsCustomizer.customize(operation, components, handlerMethod);
- else
- operation = operationCustomizer.customize(operation, handlerMethod);
- }
- }
- return operation;
- }
-
- /**
- * Customise router operation
- *
- * @param routerOperation the router operation
- * @param handlerMethod the handler method
- * @return the router operation
- */
- protected RouterOperation customizeRouterOperation(RouterOperation routerOperation, HandlerMethod handlerMethod) {
- Optional> optionalRouterOperationCustomizers = springDocCustomizers.getRouterOperationCustomizers();
- if (optionalRouterOperationCustomizers.isPresent()) {
- Set routerOperationCustomizerList = optionalRouterOperationCustomizers.get();
- for (RouterOperationCustomizer routerOperationCustomizer : routerOperationCustomizerList) {
- routerOperation = routerOperationCustomizer.customize(routerOperation, handlerMethod);
- }
- }
- return routerOperation;
- }
-
- /**
- * Merge routers.
- *
- * @param routerFunctionDatas the router function datas
- * @param routerOperationList the router operation list
- */
- protected void mergeRouters(List routerFunctionDatas, List routerOperationList) {
- for (RouterOperation routerOperation : routerOperationList) {
- if (StringUtils.isNotBlank(routerOperation.getPath())) {
- // PATH
- List routerFunctionDataList = routerFunctionDatas.stream()
- .filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath()))
- .toList();
- if (routerFunctionDataList.size() == 1)
- fillRouterOperation(routerFunctionDataList.get(0), routerOperation);
- else if (routerFunctionDataList.size() > 1 && ArrayUtils.isNotEmpty(routerOperation.getMethods())) {
- // PATH + METHOD
- routerFunctionDataList = routerFunctionDatas.stream()
- .filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath())
- && isEqualMethods(routerOperation.getMethods(), routerFunctionData1.getMethods()))
- .toList();
- if (routerFunctionDataList.size() == 1)
- fillRouterOperation(routerFunctionDataList.get(0), routerOperation);
- else if (routerFunctionDataList.size() > 1 && ArrayUtils.isNotEmpty(routerOperation.getProduces())) {
- // PATH + METHOD + PRODUCES
- routerFunctionDataList = routerFunctionDatas.stream()
- .filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath())
- && isEqualMethods(routerOperation.getMethods(), routerFunctionData1.getMethods())
- && isEqualArrays(routerFunctionData1.getProduces(), routerOperation.getProduces()))
- .toList();
- if (routerFunctionDataList.size() == 1)
- fillRouterOperation(routerFunctionDataList.get(0), routerOperation);
- else if (routerFunctionDataList.size() > 1 && ArrayUtils.isNotEmpty(routerOperation.getConsumes())) {
- // PATH + METHOD + PRODUCES + CONSUMES
- routerFunctionDataList = routerFunctionDatas.stream()
- .filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath())
- && isEqualMethods(routerOperation.getMethods(), routerFunctionData1.getMethods())
- && isEqualArrays(routerFunctionData1.getProduces(), routerOperation.getProduces())
- && isEqualArrays(routerFunctionData1.getConsumes(), routerOperation.getConsumes()))
- .toList();
- if (routerFunctionDataList.size() == 1)
- fillRouterOperation(routerFunctionDataList.get(0), routerOperation);
- }
- }
- else if (routerFunctionDataList.size() > 1 && ArrayUtils.isNotEmpty(routerOperation.getConsumes())) {
- // PATH + METHOD + CONSUMES
- routerFunctionDataList = routerFunctionDatas.stream()
- .filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath())
- && isEqualMethods(routerOperation.getMethods(), routerFunctionData1.getMethods())
- && isEqualArrays(routerFunctionData1.getConsumes(), routerOperation.getConsumes()))
- .toList();
- if (routerFunctionDataList.size() == 1)
- fillRouterOperation(routerFunctionDataList.get(0), routerOperation);
- }
- }
- else if (routerFunctionDataList.size() > 1 && ArrayUtils.isNotEmpty(routerOperation.getProduces())) {
- // PATH + PRODUCES
- routerFunctionDataList = routerFunctionDatas.stream()
- .filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath())
- && isEqualArrays(routerFunctionData1.getProduces(), routerOperation.getProduces()))
- .toList();
- if (routerFunctionDataList.size() == 1)
- fillRouterOperation(routerFunctionDataList.get(0), routerOperation);
- else if (routerFunctionDataList.size() > 1 && ArrayUtils.isNotEmpty(routerOperation.getConsumes())) {
- // PATH + PRODUCES + CONSUMES
- routerFunctionDataList = routerFunctionDatas.stream()
- .filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath())
- && isEqualMethods(routerOperation.getMethods(), routerFunctionData1.getMethods())
- && isEqualArrays(routerFunctionData1.getConsumes(), routerOperation.getConsumes())
- && isEqualArrays(routerFunctionData1.getProduces(), routerOperation.getProduces()))
- .toList();
- if (routerFunctionDataList.size() == 1)
- fillRouterOperation(routerFunctionDataList.get(0), routerOperation);
- }
- }
- else if (routerFunctionDataList.size() > 1 && ArrayUtils.isNotEmpty(routerOperation.getConsumes())) {
- // PATH + CONSUMES
- routerFunctionDataList = routerFunctionDatas.stream()
- .filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath())
- && isEqualArrays(routerFunctionData1.getConsumes(), routerOperation.getConsumes()))
- .toList();
- if (routerFunctionDataList.size() == 1)
- fillRouterOperation(routerFunctionDataList.get(0), routerOperation);
- }
- }
- }
- }
-
- /**
- * Calculate json view.
- *
- * @param apiOperation the api operation
- * @param methodAttributes the method attributes
- * @param method the method
- */
- private void calculateJsonView(io.swagger.v3.oas.annotations.Operation apiOperation,
- MethodAttributes methodAttributes, Method method) {
- JsonView jsonViewAnnotation;
- JsonView jsonViewAnnotationForRequestBody;
- if (apiOperation != null && apiOperation.ignoreJsonView()) {
- jsonViewAnnotation = null;
- jsonViewAnnotationForRequestBody = null;
- }
- else {
- jsonViewAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, JsonView.class);
- /*
- * If one and only one exists, use the @JsonView annotation from the method
- * parameter annotated with @RequestBody. Otherwise fall back to the @JsonView
- * annotation for the method itself.
- */
- jsonViewAnnotationForRequestBody = (JsonView) Arrays.stream(ReflectionUtils.getParameterAnnotations(method))
- .filter(arr -> Arrays.stream(arr)
- .anyMatch(annotation -> (annotation.annotationType()
- .equals(io.swagger.v3.oas.annotations.parameters.RequestBody.class) || annotation.annotationType().equals(RequestBody.class))))
- .flatMap(Arrays::stream).filter(annotation -> annotation.annotationType().equals(JsonView.class))
- .reduce((a, b) -> null).orElse(jsonViewAnnotation);
- }
- methodAttributes.setJsonViewAnnotation(jsonViewAnnotation);
- methodAttributes.setJsonViewAnnotationForRequestBody(jsonViewAnnotationForRequestBody);
- }
-
- /**
- * Is equal arrays boolean.
- *
- * @param array1 the array 1
- * @param array2 the array 2
- * @return the boolean
- */
- private boolean isEqualArrays(String[] array1, String[] array2) {
- Arrays.sort(array1);
- Arrays.sort(array2);
- return Arrays.equals(array1, array2);
- }
-
- /**
- * Is equal methods boolean.
- *
- * @param requestMethods1 the request methods 1
- * @param requestMethods2 the request methods 2
- * @return the boolean
- */
- private boolean isEqualMethods(RequestMethod[] requestMethods1, RequestMethod[] requestMethods2) {
- Arrays.sort(requestMethods1);
- Arrays.sort(requestMethods2);
- return Arrays.equals(requestMethods1, requestMethods2);
- }
-
- /**
- * Fill parameters list.
- *
- * @param operation the operation
- * @param queryParams the query params
- * @param methodAttributes the method attributes
- */
- private void fillParametersList(Operation operation, Map queryParams, MethodAttributes methodAttributes) {
- List parametersList = operation.getParameters();
- if (parametersList == null)
- parametersList = new ArrayList<>();
- Collection headersMap = AbstractRequestService.getHeaders(methodAttributes, new LinkedHashMap<>());
- headersMap.forEach(parameter -> {
- Optional existingParam;
- if (!CollectionUtils.isEmpty(operation.getParameters())) {
- existingParam = operation.getParameters().stream().filter(p -> parameter.getName().equals(p.getName())).findAny();
- if (existingParam.isEmpty())
- operation.addParametersItem(parameter);
- }
- });
- if (!CollectionUtils.isEmpty(queryParams)) {
- for (Map.Entry entry : queryParams.entrySet()) {
- io.swagger.v3.oas.models.parameters.Parameter parameter = new io.swagger.v3.oas.models.parameters.Parameter();
- parameter.setName(entry.getKey());
- parameter.setSchema(new StringSchema()._default(entry.getValue()));
- parameter.setRequired(true);
- parameter.setIn(ParameterIn.QUERY.toString());
- GenericParameterService.mergeParameter(parametersList, parameter);
- }
- operation.setParameters(parametersList);
- }
- }
-
- /**
- * Fill router operation.
- *
- * @param routerFunctionData the router function data
- * @param routerOperation the router operation
- */
- private void fillRouterOperation(RouterFunctionData routerFunctionData, RouterOperation routerOperation) {
- if (ArrayUtils.isEmpty(routerOperation.getConsumes()))
- routerOperation.setConsumes(routerFunctionData.getConsumes());
- if (ArrayUtils.isEmpty(routerOperation.getProduces()))
- routerOperation.setProduces(routerFunctionData.getProduces());
- if (ArrayUtils.isEmpty(routerOperation.getHeaders()))
- routerOperation.setHeaders(routerFunctionData.getHeaders());
- if (ArrayUtils.isEmpty(routerOperation.getMethods()))
- routerOperation.setMethods(routerFunctionData.getMethods());
- if (CollectionUtils.isEmpty(routerOperation.getQueryParams()))
- routerOperation.setQueryParams(routerFunctionData.getQueryParams());
- }
-
- /**
- * Build path item.
- *
- * @param requestMethod the request method
- * @param operation the operation
- * @param operationPath the operation path
- * @param paths the paths
- * @return the path item
- */
- private PathItem buildPathItem(RequestMethod requestMethod, Operation operation, String operationPath,
- Paths paths) {
- PathItem pathItemObject;
- if (operation != null && !CollectionUtils.isEmpty(operation.getParameters())) {
- Iterator paramIt = operation.getParameters().iterator();
- while (paramIt.hasNext()) {
- Parameter parameter = paramIt.next();
- if (ParameterIn.PATH.toString().equals(parameter.getIn())) {
- // check it's present in the path
- String name = parameter.getName();
- if (!StringUtils.containsAny(operationPath, "{" + name + "}", "{*" + name + "}"))
- paramIt.remove();
- }
- }
- }
- if (paths.containsKey(operationPath))
- pathItemObject = paths.get(operationPath);
- else
- pathItemObject = new PathItem();
-
- switch (requestMethod) {
- case POST:
- pathItemObject.post(operation);
- break;
- case GET:
- pathItemObject.get(operation);
- break;
- case DELETE:
- pathItemObject.delete(operation);
- break;
- case PUT:
- pathItemObject.put(operation);
- break;
- case PATCH:
- pathItemObject.patch(operation);
- break;
- case TRACE:
- pathItemObject.trace(operation);
- break;
- case HEAD:
- pathItemObject.head(operation);
- break;
- case OPTIONS:
- pathItemObject.options(operation);
- break;
- default:
- // Do nothing here
- break;
- }
- return pathItemObject;
- }
-
- /**
- * Gets existing operation.
- *
- * @param operationMap the operation map
- * @param requestMethod the request method
- * @return the existing operation
- */
- private Operation getExistingOperation(Map operationMap, RequestMethod requestMethod) {
- Operation existingOperation = null;
- if (!CollectionUtils.isEmpty(operationMap)) {
- // Get existing operation definition
- switch (requestMethod) {
- case GET:
- existingOperation = operationMap.get(HttpMethod.GET);
- break;
- case POST:
- existingOperation = operationMap.get(HttpMethod.POST);
- break;
- case PUT:
- existingOperation = operationMap.get(HttpMethod.PUT);
- break;
- case DELETE:
- existingOperation = operationMap.get(HttpMethod.DELETE);
- break;
- case PATCH:
- existingOperation = operationMap.get(HttpMethod.PATCH);
- break;
- case HEAD:
- existingOperation = operationMap.get(HttpMethod.HEAD);
- break;
- case OPTIONS:
- existingOperation = operationMap.get(HttpMethod.OPTIONS);
- break;
- default:
- // Do nothing here
- break;
- }
- }
- return existingOperation;
- }
-
- /**
- * Gets operation.
- *
- * @param routerOperation the router operation
- * @param existingOperation the existing operation
- * @return the operation
- */
- private Operation getOperation(RouterOperation routerOperation, Operation existingOperation) {
- Operation operationModel = routerOperation.getOperationModel();
- Operation operation;
- if (existingOperation != null && operationModel == null) {
- operation = existingOperation;
- }
- else if (existingOperation == null && operationModel != null) {
- operation = operationModel;
- }
- else if (existingOperation != null) {
- operation = operationParser.mergeOperation(existingOperation, operationModel);
- }
- else {
- operation = new Operation();
- }
- return operation;
- }
-
- /**
- * Init open api builder.
- *
- * @param locale the locale
- */
- protected void initOpenAPIBuilder(Locale locale) {
- locale = selectLocale(locale);
- if (openAPIService.getCachedOpenAPI(locale) != null && springDocConfigProperties.isCacheDisabled()) {
- openAPIService = openAPIBuilderObjectFactory.getObject();
- }
- }
-
- /**
- * Write yaml value string.
- *
- * @param openAPI the open api
- * @return the string
- * @throws JsonProcessingException the json processing exception
- */
- protected byte[] writeYamlValue(OpenAPI openAPI) throws JsonProcessingException {
- String result;
- ObjectMapper objectMapper = springDocProviders.yamlMapper();
- if (springDocConfigProperties.isWriterWithOrderByKeys())
- ObjectMapperProvider.sortOutput(objectMapper, springDocConfigProperties);
- YAMLFactory factory = (YAMLFactory) objectMapper.getFactory();
- factory.configure(Feature.USE_NATIVE_TYPE_ID, false);
- if (!springDocConfigProperties.isWriterWithDefaultPrettyPrinter())
- result = objectMapper.writerFor(OpenAPI.class).writeValueAsString(openAPI);
- else
- result = objectMapper.writerWithDefaultPrettyPrinter().forType(OpenAPI.class).writeValueAsString(openAPI);
- return result.getBytes(StandardCharsets.UTF_8);
- }
-
- /**
- * Gets actuator uri.
- *
- * @param scheme the scheme
- * @param host the host
- * @return the actuator uri
- */
- protected URI getActuatorURI(String scheme, String host) {
- final Optional actuatorProviderOptional = springDocProviders.getActuatorProvider();
- URI uri = null;
- if (actuatorProviderOptional.isPresent()) {
- ActuatorProvider actuatorProvider = actuatorProviderOptional.get();
- int port;
- String path;
- if (ACTUATOR_DEFAULT_GROUP.equals(this.groupName)) {
- port = actuatorProvider.getActuatorPort();
- path = actuatorProvider.getActuatorPath();
- }
- else {
- port = actuatorProvider.getApplicationPort();
- path = actuatorProvider.getContextPath();
- String mvcServletPath = this.openAPIService.getContext().getBean(Environment.class).getProperty(SPRING_MVC_SERVLET_PATH);
- if (SpringDocUtils.isValidPath(mvcServletPath))
- path = path + mvcServletPath;
- }
- try {
- uri = new URI(StringUtils.defaultIfEmpty(scheme, "http"), null, StringUtils.defaultIfEmpty(host, "localhost"), port, path, null, null);
- }
- catch (URISyntaxException e) {
- LOGGER.error("Unable to parse the URL: scheme {}, host {}, port {}, path {}", scheme, host, port, path);
- }
- }
- return uri;
- }
-
- /**
- * Is actuator rest controller boolean.
- *
- * @param operationPath the operation path
- * @param handlerMethod the handler method
- * @return the boolean
- */
- protected boolean isActuatorRestController(String operationPath, HandlerMethod handlerMethod) {
- Optional actuatorProviderOptional = springDocProviders.getActuatorProvider();
- boolean isActuatorRestController = false;
- if (actuatorProviderOptional.isPresent())
- isActuatorRestController = actuatorProviderOptional.get().isRestController(operationPath, handlerMethod);
- return springDocConfigProperties.isShowActuator() && isActuatorRestController && (modelAndViewClass == null || !modelAndViewClass.isAssignableFrom(handlerMethod.getMethod().getReturnType()));
- }
-
- /**
- * Write json value string.
- *
- * @param openAPI the open api
- * @return the string
- * @throws JsonProcessingException the json processing exception
- */
- protected byte[] writeJsonValue(OpenAPI openAPI) throws JsonProcessingException {
- String result;
- ObjectMapper objectMapper = springDocProviders.jsonMapper();
- if (springDocConfigProperties.isWriterWithOrderByKeys())
- ObjectMapperProvider.sortOutput(objectMapper, springDocConfigProperties);
- if (!springDocConfigProperties.isWriterWithDefaultPrettyPrinter())
- result = objectMapper.writerFor(OpenAPI.class).writeValueAsString(openAPI);
- else
- result = objectMapper.writerWithDefaultPrettyPrinter().forType(OpenAPI.class).writeValueAsString(openAPI);
- return result.getBytes(StandardCharsets.UTF_8);
- }
-
- /**
- * Gets conditions to match.
- *
- * @param conditionType the condition type
- * @param groupConfigs the group configs
- * @return the conditions to match
- */
- private List getConditionsToMatch(ConditionType conditionType, GroupConfig... groupConfigs) {
- List conditionsToMatch = null;
- GroupConfig groupConfig = null;
- if (ArrayUtils.isNotEmpty(groupConfigs))
- groupConfig = groupConfigs[0];
- switch (conditionType) {
- case HEADERS:
- conditionsToMatch = (groupConfig != null) ? groupConfig.getHeadersToMatch() : springDocConfigProperties.getHeadersToMatch();
- break;
- case PRODUCES:
- conditionsToMatch = (groupConfig != null) ? groupConfig.getProducesToMatch() : springDocConfigProperties.getProducesToMatch();
- break;
- case CONSUMES:
- conditionsToMatch = (groupConfig != null) ? groupConfig.getConsumesToMatch() : springDocConfigProperties.getConsumesToMatch();
- break;
- default:
- break;
- }
- return conditionsToMatch;
- }
-
- /**
- * Is filter condition boolean.
- *
- * @param operationPath the operation path
- * @param produces the produces
- * @param consumes the consumes
- * @param headers the headers
- * @return the boolean
- */
- private boolean isFilterCondition(String operationPath, String[] produces, String[] consumes, String[] headers) {
- return isPathToMatch(operationPath)
- && isConditionToMatch(produces, ConditionType.PRODUCES)
- && isConditionToMatch(consumes, ConditionType.CONSUMES)
- && isConditionToMatch(headers, ConditionType.HEADERS);
- }
-
- /**
- * The enum Condition type.
- */
- enum ConditionType {
- /**
- * Produces condition type.
- */
- PRODUCES,
- /**
- * Consumes condition type.
- */
- CONSUMES,
- /**
- * Headers condition type.
- */
- HEADERS
- }
-}