Skip to content

Commit

Permalink
Fix Page response model inconsistent in swagger ui (#1277)
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnNiang authored Feb 19, 2021
1 parent cdc1594 commit 49a461f
Show file tree
Hide file tree
Showing 2 changed files with 171 additions and 61 deletions.
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);
}

}

0 comments on commit 49a461f

Please sign in to comment.