Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Page response model inconsistent in swagger ui #1277

Merged
merged 2 commits into from
Feb 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 54 additions & 61 deletions src/main/java/run/halo/app/config/SwaggerConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
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.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 com.fasterxml.classmate.TypeResolver;
Expand All @@ -19,30 +21,27 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.lang.NonNull;
import org.springframework.util.AntPathMatcher;
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.model.entity.User;
import run.halo.app.security.support.UserDetail;
import springfox.documentation.builders.AlternateTypeBuilder;
import springfox.documentation.builders.AlternateTypePropertyBuilder;
import run.halo.app.utils.SwaggerUtils;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.builders.ResponseMessageBuilder;
import springfox.documentation.schema.AlternateTypeRule;
import springfox.documentation.schema.AlternateTypeRuleConvention;
import springfox.documentation.schema.WildcardType;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiKey;
import springfox.documentation.service.AuthorizationScope;
import springfox.documentation.service.Contact;
import springfox.documentation.service.ResponseMessage;
import springfox.documentation.service.SecurityReference;
import springfox.documentation.service.SecurityScheme;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger.web.DocExpansion;
Expand All @@ -69,14 +68,6 @@ public class SwaggerConfiguration {

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) {
this.haloProperties = haloProperties;
}
Expand Down Expand Up @@ -143,18 +134,13 @@ private Docket buildApiDocket(@NonNull String groupName, @NonNull String basePac
Assert.hasText(basePackage, "Base package must not be blank");
Assert.hasText(antPattern, "Ant pattern must not be blank");

return new Docket(DocumentationType.SWAGGER_2)
return SwaggerUtils.defaultDocket()
.groupName(groupName)
.select()
.apis(RequestHandlerSelectors.basePackage(basePackage))
.paths(PathSelectors.ant(antPattern))
.build()
.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);
}

Expand All @@ -166,10 +152,14 @@ private List<SecurityScheme> adminApiKeys() {
}

private List<SecurityContext> adminSecurityContext() {
final PathMatcher pathMatcher = new AntPathMatcher();
return Collections.singletonList(
SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.regex("/api/admin/.*"))
.operationSelector(operationContext -> {
var requestMappingPattern = operationContext.requestMappingPattern();
return pathMatcher.match("/api/admin/**/*", requestMappingPattern);
})
.build()
);
}
Expand All @@ -182,10 +172,14 @@ private List<SecurityScheme> contentApiKeys() {
}

private List<SecurityContext> contentSecurityContext() {
final PathMatcher pathMatcher = new AntPathMatcher();
return Collections.singletonList(
SecurityContext.builder()
.securityReferences(contentApiAuth())
.forPaths(PathSelectors.regex("/api/content/.*"))
.operationSelector(operationContext -> {
var requestMappingPattern = operationContext.requestMappingPattern();
return pathMatcher.match("/api/content/**/*", requestMappingPattern);
})
.build()
);
}
Expand Down Expand Up @@ -228,56 +222,55 @@ public int getOrder() {
@Override
public List<AlternateTypeRule> rules() {
return Arrays.asList(
newRule(User.class, emptyMixin(User.class)),
newRule(UserDetail.class, emptyMixin(UserDetail.class)),
newRule(resolver.resolve(Page.class, WildcardType.class),
resolver.resolve(CustomizedPage.class, WildcardType.class)),
newRule(resolver.resolve(Pageable.class), resolver.resolve(pageableMixin())),
newRule(resolver.resolve(Sort.class), resolver.resolve(sortMixin())));
}
};
}

private Type sortMixin() {
return customMixin(Sort.class,
Collections.singletonList(propertyBuilder(String[].class, "sort")));
}

private Type pageableMixin() {
return customMixin(Pageable.class, Arrays.asList(
propertyBuilder(Integer.class, "page"),
propertyBuilder(Integer.class, "size"),
propertyBuilder(String[].class, "sort")
));
}

/**
* For controller parameter(like eg: HttpServletRequest, ModelView ...).
* Alternative page type.
*
* @param clazz controller parameter class type must not be null
* @return empty type
* @param <T> content type
* @author johnniang
*/
private Type emptyMixin(Class<?> clazz) {
Assert.notNull(clazz, "class type must not be null");
interface CustomizedPage<T> {

return new AlternateTypeBuilder()
.fullyQualifiedClassName(String
.format("%s.generated.%s", clazz.getPackage().getName(), clazz.getSimpleName()))
.withProperties(Collections.emptyList())
.build();
}
List<T> getContent();

private Type sortMixin() {
return new AlternateTypeBuilder()
.fullyQualifiedClassName(String
.format("%s.generated.%s", Sort.class.getPackage().getName(),
Sort.class.getSimpleName()))
.withProperties(Collections.singletonList(property(String[].class, "sort")))
.build();
}
int getPage();

private Type pageableMixin() {
return new AlternateTypeBuilder()
.fullyQualifiedClassName(String
.format("%s.generated.%s", Pageable.class.getPackage().getName(),
Pageable.class.getSimpleName()))
.withProperties(Arrays
.asList(property(Integer.class, "page"), property(Integer.class, "size"),
property(String[].class, "sort")))
.build();
}
int getPages();

long getTotal();

int getRpp();

boolean getHasNext();

boolean getHasPrevious();

boolean getIsFirst();

boolean getIsEmpty();

boolean getHasContent();

private AlternateTypePropertyBuilder property(Class<?> type, String name) {
return new AlternateTypePropertyBuilder()
.withName(name)
.withType(type)
.withCanRead(true)
.withCanWrite(true);
}

}
117 changes: 117 additions & 0 deletions src/main/java/run/halo/app/utils/SwaggerUtils.java
Original file line number Diff line number Diff line change
@@ -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);
}

}