mirror of https://github.com/halo-dev/halo
Fix Page response model inconsistent in swagger ui (#1277)
parent
cdc15942cf
commit
49a461f245
|
@ -5,6 +5,8 @@ import static run.halo.app.model.support.HaloConst.ADMIN_TOKEN_QUERY_NAME;
|
||||||
import static run.halo.app.model.support.HaloConst.API_ACCESS_KEY_HEADER_NAME;
|
import static run.halo.app.model.support.HaloConst.API_ACCESS_KEY_HEADER_NAME;
|
||||||
import static run.halo.app.model.support.HaloConst.API_ACCESS_KEY_QUERY_NAME;
|
import static run.halo.app.model.support.HaloConst.API_ACCESS_KEY_QUERY_NAME;
|
||||||
import static run.halo.app.model.support.HaloConst.HALO_VERSION;
|
import static run.halo.app.model.support.HaloConst.HALO_VERSION;
|
||||||
|
import static run.halo.app.utils.SwaggerUtils.customMixin;
|
||||||
|
import static run.halo.app.utils.SwaggerUtils.propertyBuilder;
|
||||||
import static springfox.documentation.schema.AlternateTypeRules.newRule;
|
import static springfox.documentation.schema.AlternateTypeRules.newRule;
|
||||||
|
|
||||||
import com.fasterxml.classmate.TypeResolver;
|
import com.fasterxml.classmate.TypeResolver;
|
||||||
|
@ -19,30 +21,27 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.core.Ordered;
|
import org.springframework.core.Ordered;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.domain.Sort;
|
import org.springframework.data.domain.Sort;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.util.AntPathMatcher;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.web.bind.annotation.RequestMethod;
|
import org.springframework.util.PathMatcher;
|
||||||
import run.halo.app.config.properties.HaloProperties;
|
import run.halo.app.config.properties.HaloProperties;
|
||||||
import run.halo.app.model.entity.User;
|
import run.halo.app.utils.SwaggerUtils;
|
||||||
import run.halo.app.security.support.UserDetail;
|
|
||||||
import springfox.documentation.builders.AlternateTypeBuilder;
|
|
||||||
import springfox.documentation.builders.AlternateTypePropertyBuilder;
|
|
||||||
import springfox.documentation.builders.ApiInfoBuilder;
|
import springfox.documentation.builders.ApiInfoBuilder;
|
||||||
import springfox.documentation.builders.PathSelectors;
|
import springfox.documentation.builders.PathSelectors;
|
||||||
import springfox.documentation.builders.RequestHandlerSelectors;
|
import springfox.documentation.builders.RequestHandlerSelectors;
|
||||||
import springfox.documentation.builders.ResponseMessageBuilder;
|
|
||||||
import springfox.documentation.schema.AlternateTypeRule;
|
import springfox.documentation.schema.AlternateTypeRule;
|
||||||
import springfox.documentation.schema.AlternateTypeRuleConvention;
|
import springfox.documentation.schema.AlternateTypeRuleConvention;
|
||||||
|
import springfox.documentation.schema.WildcardType;
|
||||||
import springfox.documentation.service.ApiInfo;
|
import springfox.documentation.service.ApiInfo;
|
||||||
import springfox.documentation.service.ApiKey;
|
import springfox.documentation.service.ApiKey;
|
||||||
import springfox.documentation.service.AuthorizationScope;
|
import springfox.documentation.service.AuthorizationScope;
|
||||||
import springfox.documentation.service.Contact;
|
import springfox.documentation.service.Contact;
|
||||||
import springfox.documentation.service.ResponseMessage;
|
|
||||||
import springfox.documentation.service.SecurityReference;
|
import springfox.documentation.service.SecurityReference;
|
||||||
import springfox.documentation.service.SecurityScheme;
|
import springfox.documentation.service.SecurityScheme;
|
||||||
import springfox.documentation.spi.DocumentationType;
|
|
||||||
import springfox.documentation.spi.service.contexts.SecurityContext;
|
import springfox.documentation.spi.service.contexts.SecurityContext;
|
||||||
import springfox.documentation.spring.web.plugins.Docket;
|
import springfox.documentation.spring.web.plugins.Docket;
|
||||||
import springfox.documentation.swagger.web.DocExpansion;
|
import springfox.documentation.swagger.web.DocExpansion;
|
||||||
|
@ -69,14 +68,6 @@ public class SwaggerConfiguration {
|
||||||
|
|
||||||
private final HaloProperties haloProperties;
|
private final HaloProperties haloProperties;
|
||||||
|
|
||||||
private final List<ResponseMessage> globalResponses = Arrays.asList(
|
|
||||||
new ResponseMessageBuilder().code(200).message("Success").build(),
|
|
||||||
new ResponseMessageBuilder().code(400).message("Bad request").build(),
|
|
||||||
new ResponseMessageBuilder().code(401).message("Unauthorized").build(),
|
|
||||||
new ResponseMessageBuilder().code(403).message("Forbidden").build(),
|
|
||||||
new ResponseMessageBuilder().code(404).message("Not found").build(),
|
|
||||||
new ResponseMessageBuilder().code(500).message("Internal server error").build());
|
|
||||||
|
|
||||||
public SwaggerConfiguration(HaloProperties haloProperties) {
|
public SwaggerConfiguration(HaloProperties haloProperties) {
|
||||||
this.haloProperties = haloProperties;
|
this.haloProperties = haloProperties;
|
||||||
}
|
}
|
||||||
|
@ -143,18 +134,13 @@ public class SwaggerConfiguration {
|
||||||
Assert.hasText(basePackage, "Base package must not be blank");
|
Assert.hasText(basePackage, "Base package must not be blank");
|
||||||
Assert.hasText(antPattern, "Ant pattern must not be blank");
|
Assert.hasText(antPattern, "Ant pattern must not be blank");
|
||||||
|
|
||||||
return new Docket(DocumentationType.SWAGGER_2)
|
return SwaggerUtils.defaultDocket()
|
||||||
.groupName(groupName)
|
.groupName(groupName)
|
||||||
.select()
|
.select()
|
||||||
.apis(RequestHandlerSelectors.basePackage(basePackage))
|
.apis(RequestHandlerSelectors.basePackage(basePackage))
|
||||||
.paths(PathSelectors.ant(antPattern))
|
.paths(PathSelectors.ant(antPattern))
|
||||||
.build()
|
.build()
|
||||||
.apiInfo(apiInfo())
|
.apiInfo(apiInfo())
|
||||||
.useDefaultResponseMessages(false)
|
|
||||||
.globalResponseMessage(RequestMethod.GET, globalResponses)
|
|
||||||
.globalResponseMessage(RequestMethod.POST, globalResponses)
|
|
||||||
.globalResponseMessage(RequestMethod.DELETE, globalResponses)
|
|
||||||
.globalResponseMessage(RequestMethod.PUT, globalResponses)
|
|
||||||
.directModelSubstitute(Temporal.class, String.class);
|
.directModelSubstitute(Temporal.class, String.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,10 +152,14 @@ public class SwaggerConfiguration {
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<SecurityContext> adminSecurityContext() {
|
private List<SecurityContext> adminSecurityContext() {
|
||||||
|
final PathMatcher pathMatcher = new AntPathMatcher();
|
||||||
return Collections.singletonList(
|
return Collections.singletonList(
|
||||||
SecurityContext.builder()
|
SecurityContext.builder()
|
||||||
.securityReferences(defaultAuth())
|
.securityReferences(defaultAuth())
|
||||||
.forPaths(PathSelectors.regex("/api/admin/.*"))
|
.operationSelector(operationContext -> {
|
||||||
|
var requestMappingPattern = operationContext.requestMappingPattern();
|
||||||
|
return pathMatcher.match("/api/admin/**/*", requestMappingPattern);
|
||||||
|
})
|
||||||
.build()
|
.build()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -182,10 +172,14 @@ public class SwaggerConfiguration {
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<SecurityContext> contentSecurityContext() {
|
private List<SecurityContext> contentSecurityContext() {
|
||||||
|
final PathMatcher pathMatcher = new AntPathMatcher();
|
||||||
return Collections.singletonList(
|
return Collections.singletonList(
|
||||||
SecurityContext.builder()
|
SecurityContext.builder()
|
||||||
.securityReferences(contentApiAuth())
|
.securityReferences(contentApiAuth())
|
||||||
.forPaths(PathSelectors.regex("/api/content/.*"))
|
.operationSelector(operationContext -> {
|
||||||
|
var requestMappingPattern = operationContext.requestMappingPattern();
|
||||||
|
return pathMatcher.match("/api/content/**/*", requestMappingPattern);
|
||||||
|
})
|
||||||
.build()
|
.build()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -228,56 +222,55 @@ public class SwaggerConfiguration {
|
||||||
@Override
|
@Override
|
||||||
public List<AlternateTypeRule> rules() {
|
public List<AlternateTypeRule> rules() {
|
||||||
return Arrays.asList(
|
return Arrays.asList(
|
||||||
newRule(User.class, emptyMixin(User.class)),
|
newRule(resolver.resolve(Page.class, WildcardType.class),
|
||||||
newRule(UserDetail.class, emptyMixin(UserDetail.class)),
|
resolver.resolve(CustomizedPage.class, WildcardType.class)),
|
||||||
newRule(resolver.resolve(Pageable.class), resolver.resolve(pageableMixin())),
|
newRule(resolver.resolve(Pageable.class), resolver.resolve(pageableMixin())),
|
||||||
newRule(resolver.resolve(Sort.class), resolver.resolve(sortMixin())));
|
newRule(resolver.resolve(Sort.class), resolver.resolve(sortMixin())));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* For controller parameter(like eg: HttpServletRequest, ModelView ...).
|
|
||||||
*
|
|
||||||
* @param clazz controller parameter class type must not be null
|
|
||||||
* @return empty type
|
|
||||||
*/
|
|
||||||
private Type emptyMixin(Class<?> clazz) {
|
|
||||||
Assert.notNull(clazz, "class type must not be null");
|
|
||||||
|
|
||||||
return new AlternateTypeBuilder()
|
|
||||||
.fullyQualifiedClassName(String
|
|
||||||
.format("%s.generated.%s", clazz.getPackage().getName(), clazz.getSimpleName()))
|
|
||||||
.withProperties(Collections.emptyList())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Type sortMixin() {
|
private Type sortMixin() {
|
||||||
return new AlternateTypeBuilder()
|
return customMixin(Sort.class,
|
||||||
.fullyQualifiedClassName(String
|
Collections.singletonList(propertyBuilder(String[].class, "sort")));
|
||||||
.format("%s.generated.%s", Sort.class.getPackage().getName(),
|
|
||||||
Sort.class.getSimpleName()))
|
|
||||||
.withProperties(Collections.singletonList(property(String[].class, "sort")))
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Type pageableMixin() {
|
private Type pageableMixin() {
|
||||||
return new AlternateTypeBuilder()
|
return customMixin(Pageable.class, Arrays.asList(
|
||||||
.fullyQualifiedClassName(String
|
propertyBuilder(Integer.class, "page"),
|
||||||
.format("%s.generated.%s", Pageable.class.getPackage().getName(),
|
propertyBuilder(Integer.class, "size"),
|
||||||
Pageable.class.getSimpleName()))
|
propertyBuilder(String[].class, "sort")
|
||||||
.withProperties(Arrays
|
));
|
||||||
.asList(property(Integer.class, "page"), property(Integer.class, "size"),
|
|
||||||
property(String[].class, "sort")))
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private AlternateTypePropertyBuilder property(Class<?> type, String name) {
|
/**
|
||||||
return new AlternateTypePropertyBuilder()
|
* Alternative page type.
|
||||||
.withName(name)
|
*
|
||||||
.withType(type)
|
* @param <T> content type
|
||||||
.withCanRead(true)
|
* @author johnniang
|
||||||
.withCanWrite(true);
|
*/
|
||||||
|
interface CustomizedPage<T> {
|
||||||
|
|
||||||
|
List<T> getContent();
|
||||||
|
|
||||||
|
int getPage();
|
||||||
|
|
||||||
|
int getPages();
|
||||||
|
|
||||||
|
long getTotal();
|
||||||
|
|
||||||
|
int getRpp();
|
||||||
|
|
||||||
|
boolean getHasNext();
|
||||||
|
|
||||||
|
boolean getHasPrevious();
|
||||||
|
|
||||||
|
boolean getIsFirst();
|
||||||
|
|
||||||
|
boolean getIsEmpty();
|
||||||
|
|
||||||
|
boolean getHasContent();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
package run.halo.app.utils;
|
||||||
|
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import run.halo.app.model.entity.User;
|
||||||
|
import run.halo.app.security.authentication.Authentication;
|
||||||
|
import run.halo.app.security.support.UserDetail;
|
||||||
|
import springfox.documentation.builders.AlternateTypeBuilder;
|
||||||
|
import springfox.documentation.builders.AlternateTypePropertyBuilder;
|
||||||
|
import springfox.documentation.builders.ResponseBuilder;
|
||||||
|
import springfox.documentation.service.Response;
|
||||||
|
import springfox.documentation.spi.DocumentationType;
|
||||||
|
import springfox.documentation.spring.web.plugins.Docket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swagger utils.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
public final class SwaggerUtils {
|
||||||
|
|
||||||
|
private SwaggerUtils() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Type customMixin(Class<?> clazz,
|
||||||
|
List<Consumer<AlternateTypePropertyBuilder>> properties) {
|
||||||
|
Assert.notNull(clazz, "Class must not be null");
|
||||||
|
final var typeBuilder = new AlternateTypeBuilder()
|
||||||
|
.fullyQualifiedClassName(
|
||||||
|
String.format("%s.generated.%s", clazz.getPackage().getName(),
|
||||||
|
clazz.getSimpleName()));
|
||||||
|
|
||||||
|
properties.forEach(typeBuilder::property);
|
||||||
|
|
||||||
|
return typeBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Type emptyMixin(Class<?> clazz) {
|
||||||
|
return customMixin(clazz, Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Consumer<AlternateTypePropertyBuilder> propertyBuilder(Class<?> clazz,
|
||||||
|
String name) {
|
||||||
|
return propertyBuilder -> propertyBuilder.type(clazz)
|
||||||
|
.name(name)
|
||||||
|
.canRead(true)
|
||||||
|
.canWrite(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final List<Response> GLOBAL_RESPONSES = Arrays.asList(
|
||||||
|
new ResponseBuilder().code("200").description("The request has succeeded.").isDefault(true)
|
||||||
|
.build(),
|
||||||
|
new ResponseBuilder().code("201").description(
|
||||||
|
"The request has succeeded and a new resource has been created as a result.").build(),
|
||||||
|
new ResponseBuilder().code("204").description(
|
||||||
|
"There is no content to send for this request, but the headers may be useful.").build(),
|
||||||
|
new ResponseBuilder().code("400")
|
||||||
|
.description("The server could not understand the request due to invalid syntax.")
|
||||||
|
.build(),
|
||||||
|
new ResponseBuilder().code("401").description("Although the HTTP standard specifies "
|
||||||
|
+ "\"unauthorized\", semantically this response means \"unauthenticated\"").build(),
|
||||||
|
new ResponseBuilder().code("403")
|
||||||
|
.description("The client does not have access rights to the content.").build(),
|
||||||
|
new ResponseBuilder().code("404")
|
||||||
|
.description("The server can not find the requested resource.").build(),
|
||||||
|
new ResponseBuilder().code("405").description(
|
||||||
|
"The request method is known by the server but has been disabled and cannot be used. ")
|
||||||
|
.build(),
|
||||||
|
new ResponseBuilder().code("500")
|
||||||
|
.description("The server has encountered a situation it doesn't know how to handle.")
|
||||||
|
.build(),
|
||||||
|
new ResponseBuilder().code("501")
|
||||||
|
.description("The request method is not supported by the server and cannot be handled.")
|
||||||
|
.build(),
|
||||||
|
new ResponseBuilder().code("503")
|
||||||
|
.description("The server is not ready to handle the request.").build());
|
||||||
|
|
||||||
|
public static Docket defaultDocket() {
|
||||||
|
return new Docket(DocumentationType.OAS_30)
|
||||||
|
.forCodeGeneration(true)
|
||||||
|
.ignoredParameterTypes(initIgnore())
|
||||||
|
.useDefaultResponseMessages(false)
|
||||||
|
.globalResponses(HttpMethod.GET, GLOBAL_RESPONSES)
|
||||||
|
.globalResponses(HttpMethod.POST, GLOBAL_RESPONSES)
|
||||||
|
.globalResponses(HttpMethod.DELETE, GLOBAL_RESPONSES)
|
||||||
|
.globalResponses(HttpMethod.PATCH, GLOBAL_RESPONSES)
|
||||||
|
.globalResponses(HttpMethod.PUT, GLOBAL_RESPONSES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<Class<?>> classFor(String className) {
|
||||||
|
try {
|
||||||
|
return Optional
|
||||||
|
.of(Class.forName(className, false, SwaggerUtils.class.getClassLoader()));
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Class<?>[] initIgnore() {
|
||||||
|
final Set<Class<?>> ignoredClasses = new HashSet<>();
|
||||||
|
ignoredClasses.add(User.class);
|
||||||
|
ignoredClasses.add(UserDetail.class);
|
||||||
|
ignoredClasses.add(Authentication.class);
|
||||||
|
|
||||||
|
classFor(User.class.getName()).ifPresent(ignoredClasses::add);
|
||||||
|
return ignoredClasses.toArray(Class[]::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue