mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-01-24 14:24:47 +08:00
improve pagination configuration
This commit is contained in:
parent
1ec34b350b
commit
b6c34e616d
@ -2,6 +2,7 @@ package io.papermc.hangar.controller.api.v1;
|
||||
|
||||
import io.papermc.hangar.controller.api.v1.interfaces.IVersionsController;
|
||||
import io.papermc.hangar.controller.extras.pagination.annotations.ApplicableFilters;
|
||||
import io.papermc.hangar.controller.extras.pagination.annotations.ConfigurePagination;
|
||||
import io.papermc.hangar.controller.extras.pagination.filters.versions.VersionChannelFilter;
|
||||
import io.papermc.hangar.controller.extras.pagination.filters.versions.VersionPlatformFilter;
|
||||
import io.papermc.hangar.controller.extras.pagination.filters.versions.VersionTagFilter;
|
||||
@ -51,7 +52,7 @@ public class VersionsController implements IVersionsController {
|
||||
@Override
|
||||
@VisibilityRequired(type = Type.PROJECT, args = "{#author, #slug}")
|
||||
@ApplicableFilters({VersionChannelFilter.class, VersionPlatformFilter.class, VersionTagFilter.class})
|
||||
public PaginatedResult<Version> getVersions(String author, String slug, @NotNull RequestPagination pagination) {
|
||||
public PaginatedResult<Version> getVersions(String author, String slug, @NotNull @ConfigurePagination(defaultLimitString = "@hangarConfig.projects.initVersionLoad", maxLimit = 25) RequestPagination pagination) {
|
||||
return versionsApiService.getVersions(author, slug, pagination);
|
||||
}
|
||||
|
||||
|
@ -1,28 +1,35 @@
|
||||
package io.papermc.hangar.controller.extras;
|
||||
|
||||
import io.papermc.hangar.config.hangar.HangarConfig;
|
||||
import io.papermc.hangar.exceptions.HangarApiException;
|
||||
import io.papermc.hangar.util.StaticContextAccessor;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import java.util.function.Function;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.checkerframework.framework.qual.DefaultQualifier;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
|
||||
public class ApiUtils {
|
||||
@DefaultQualifier(NonNull.class)
|
||||
public final class ApiUtils {
|
||||
|
||||
private ApiUtils() { }
|
||||
public static final int DEFAULT_MAX_LIMIT = 50;
|
||||
public static final int DEFAULT_LIMIT = 25;
|
||||
|
||||
private static final HangarConfig hangarConfig = StaticContextAccessor.getBean(HangarConfig.class);
|
||||
private ApiUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the pagination limit or the max configured
|
||||
*
|
||||
* @param limit requested limit
|
||||
* @return actual limit
|
||||
*/
|
||||
public static long limitOrDefault(@Nullable Long limit) {
|
||||
return limitOrDefault(limit, hangarConfig.projects.initLoad());
|
||||
public static long limitOrDefault(@Nullable final Long limit) {
|
||||
return limitOrDefault(limit, DEFAULT_LIMIT);
|
||||
}
|
||||
|
||||
public static long limitOrDefault(@Nullable Long limit, long maxLimit) {
|
||||
if (limit != null && limit < 1) throw new HangarApiException(HttpStatus.BAD_REQUEST, "Limit should be greater than 0");
|
||||
public static long limitOrDefault(@Nullable final Long limit, final long maxLimit) {
|
||||
if (limit != null && limit < 1)
|
||||
throw new HangarApiException(HttpStatus.BAD_REQUEST, "Limit should be greater than 0");
|
||||
return Math.min(limit == null ? maxLimit : limit, maxLimit);
|
||||
}
|
||||
|
||||
@ -32,8 +39,16 @@ public class ApiUtils {
|
||||
* @param offset the requested offset
|
||||
* @return actual offset
|
||||
*/
|
||||
public static long offsetOrZero(Long offset) {
|
||||
public static long offsetOrZero(final @Nullable Long offset) {
|
||||
return Math.max(offset == null ? 0 : offset, 0);
|
||||
}
|
||||
|
||||
public static <T> @Nullable T mapParameter(final NativeWebRequest webRequest, final String param, Function<String, T> map) {
|
||||
final @Nullable String value = webRequest.getParameter(param);
|
||||
if (value != null) {
|
||||
return map.apply(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,13 +1,11 @@
|
||||
package io.papermc.hangar.controller.extras.pagination;
|
||||
|
||||
import io.papermc.hangar.controller.extras.pagination.Filter.FilterInstance;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@SuppressWarnings("unchecked")
|
||||
@ -16,9 +14,9 @@ public class FilterRegistry {
|
||||
private final Map<Class<? extends Filter<?>>, Filter<?>> filters = new HashMap<>();
|
||||
|
||||
@Autowired
|
||||
public FilterRegistry(List<? extends Filter<? extends FilterInstance>> filters) {
|
||||
public FilterRegistry(final List<? extends Filter<? extends Filter.FilterInstance>> filters) {
|
||||
filters.forEach(f -> {
|
||||
var filterClass = (Class<? extends Filter<? extends FilterInstance>>) f.getClass();
|
||||
final Class<? extends Filter<?>> filterClass = (Class<? extends Filter<? extends Filter.FilterInstance>>) f.getClass();
|
||||
if (this.filters.containsKey((filterClass))) {
|
||||
throw new IllegalArgumentException(filterClass + " is already registered as filter");
|
||||
}
|
||||
@ -27,9 +25,9 @@ public class FilterRegistry {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public <T extends Filter<? extends FilterInstance>> T get(Class<T> filterClass) {
|
||||
if (filters.containsKey(filterClass)) {
|
||||
return (T) filters.get(filterClass);
|
||||
public <T extends Filter<? extends Filter.FilterInstance>> T get(final Class<T> filterClass) {
|
||||
if (this.filters.containsKey(filterClass)) {
|
||||
return (T) this.filters.get(filterClass);
|
||||
}
|
||||
throw new IllegalArgumentException(filterClass + " is not a registered filter");
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import org.intellij.lang.annotations.Language;
|
||||
|
||||
/**
|
||||
* Configure default page length
|
||||
@ -14,10 +15,13 @@ import java.lang.annotation.Target;
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ConfigurePagination {
|
||||
|
||||
/**
|
||||
* -1 means fallback to default configured value
|
||||
*/
|
||||
long maxLimit();
|
||||
long maxLimit() default -1;
|
||||
|
||||
// TODO add String SpEL param to use configurable values for action log amounts and version amounts
|
||||
@Language("SpEL")
|
||||
String maxLimitString() default ""; // TODO implement
|
||||
|
||||
long defaultLimit() default -1;
|
||||
|
||||
@Language("SpEL")
|
||||
String defaultLimitString() default "";
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package io.papermc.hangar.controller.extras.resolvers;
|
||||
|
||||
import io.papermc.hangar.controller.extras.ApiUtils;
|
||||
import io.papermc.hangar.controller.extras.pagination.Filter;
|
||||
import io.papermc.hangar.controller.extras.pagination.Filter.FilterInstance;
|
||||
import io.papermc.hangar.controller.extras.pagination.FilterRegistry;
|
||||
import io.papermc.hangar.controller.extras.pagination.SorterRegistry;
|
||||
import io.papermc.hangar.controller.extras.pagination.annotations.ApplicableFilters;
|
||||
@ -10,62 +9,98 @@ import io.papermc.hangar.controller.extras.pagination.annotations.ApplicableSort
|
||||
import io.papermc.hangar.controller.extras.pagination.annotations.ConfigurePagination;
|
||||
import io.papermc.hangar.exceptions.HangarApiException;
|
||||
import io.papermc.hangar.model.api.requests.RequestPagination;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
|
||||
@Component
|
||||
public class RequestPaginationResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
|
||||
|
||||
private final FilterRegistry filterRegistry;
|
||||
private final StandardEvaluationContext evaluationContext;
|
||||
|
||||
// need lazy here to avoid circular dep issue
|
||||
@Autowired
|
||||
public RequestPaginationResolver(@Lazy FilterRegistry filterRegistry) {
|
||||
public RequestPaginationResolver(@Lazy final FilterRegistry filterRegistry, final StandardEvaluationContext evaluationContext) {
|
||||
this.filterRegistry = filterRegistry;
|
||||
this.evaluationContext = evaluationContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(@NotNull MethodParameter parameter) {
|
||||
public boolean supportsParameter(@NotNull final MethodParameter parameter) {
|
||||
return RequestPagination.class.isAssignableFrom(parameter.getParameterType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestPagination resolveArgument(@NotNull MethodParameter parameter, ModelAndViewContainer mavContainer, @NotNull NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
|
||||
long offset = ApiUtils.offsetOrZero(Optional.ofNullable(webRequest.getParameter("offset")).map(Long::parseLong).orElse(null));
|
||||
long limit;
|
||||
Optional<Long> limitParam = Optional.ofNullable(webRequest.getParameter("limit")).map(Long::parseLong);
|
||||
ConfigurePagination settings = parameter.getParameterAnnotation(ConfigurePagination.class);
|
||||
if (settings != null) {
|
||||
limit = ApiUtils.limitOrDefault(limitParam.orElse(null), settings.maxLimit());
|
||||
} else {
|
||||
limit = ApiUtils.limitOrDefault(limitParam.orElse(null));
|
||||
private RequestPagination create(final @Nullable Long requestOffset, final @Nullable Long requestLimit, final @Nullable ConfigurePagination settings) {
|
||||
final long offset = ApiUtils.offsetOrZero(requestOffset);
|
||||
if (settings == null) {
|
||||
return new RequestPagination(ApiUtils.limitOrDefault(requestLimit), offset);
|
||||
}
|
||||
RequestPagination pagination = new RequestPagination(limit, offset);
|
||||
final long maxLimit;
|
||||
if (settings.maxLimit() != -1) {
|
||||
maxLimit = settings.maxLimit();
|
||||
} else if (!settings.maxLimitString().isBlank()) {
|
||||
final Expression expression = EXPRESSION_PARSER.parseExpression(settings.maxLimitString());
|
||||
maxLimit = Objects.requireNonNull(expression.getValue(this.evaluationContext, requestLimit, Long.class), "SpEL must evaluate to a long");
|
||||
} else {
|
||||
maxLimit = ApiUtils.DEFAULT_MAX_LIMIT;
|
||||
}
|
||||
final long limit;
|
||||
if (requestLimit == null) {
|
||||
final long defaultLimit;
|
||||
if (settings.defaultLimit() != -1) {
|
||||
defaultLimit = settings.defaultLimit();
|
||||
} else if (!settings.defaultLimitString().isBlank()) {
|
||||
final Expression expression = EXPRESSION_PARSER.parseExpression(settings.defaultLimitString());
|
||||
defaultLimit = Objects.requireNonNull(expression.getValue(this.evaluationContext, requestLimit, Long.class), "SpEL must evaluate to a long");
|
||||
} else {
|
||||
defaultLimit = ApiUtils.DEFAULT_LIMIT;
|
||||
}
|
||||
limit = defaultLimit;
|
||||
} else {
|
||||
limit = requestLimit;
|
||||
}
|
||||
return new RequestPagination(Math.min(maxLimit, limit), offset);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestPagination resolveArgument(@NotNull final MethodParameter parameter, final ModelAndViewContainer mavContainer, @NotNull final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory) {
|
||||
final RequestPagination pagination = this.create(
|
||||
ApiUtils.mapParameter(webRequest, "offset", Long::parseLong),
|
||||
ApiUtils.mapParameter(webRequest, "limit", Long::parseLong),
|
||||
parameter.getParameterAnnotation(ConfigurePagination.class)
|
||||
);
|
||||
|
||||
// find filters
|
||||
Set<String> paramNames = new HashSet<>(webRequest.getParameterMap().keySet());
|
||||
Class<? extends Filter<? extends FilterInstance>>[] applicableFilters = Optional.ofNullable(parameter.getMethodAnnotation(ApplicableFilters.class)).map(ApplicableFilters::value).orElse(null);
|
||||
final Set<String> paramNames = new HashSet<>(webRequest.getParameterMap().keySet());
|
||||
final Class<? extends Filter<? extends Filter.FilterInstance>>[] applicableFilters = Optional.ofNullable(parameter.getMethodAnnotation(ApplicableFilters.class)).map(ApplicableFilters::value).orElse(null);
|
||||
if (applicableFilters != null) {
|
||||
for (Class<? extends Filter<? extends FilterInstance>> filter : applicableFilters) {
|
||||
Filter<? extends FilterInstance> f = filterRegistry.get(filter);
|
||||
for (final Class<? extends Filter<? extends Filter.FilterInstance>> filterClass : applicableFilters) {
|
||||
final Filter<? extends Filter.FilterInstance> f = this.filterRegistry.get(filterClass);
|
||||
if (f.supports(webRequest)) {
|
||||
pagination.getFilters().add(f.create(webRequest));
|
||||
paramNames.removeAll(f.getQueryParamNames());
|
||||
@ -81,7 +116,7 @@ public class RequestPaginationResolver implements HandlerMethodArgumentResolver
|
||||
paramNames.remove("relevance");
|
||||
|
||||
// remove request params
|
||||
for (Parameter param : parameter.getExecutable().getParameters()) {
|
||||
for (final Parameter param : parameter.getExecutable().getParameters()) {
|
||||
paramNames.remove(param.getName());
|
||||
}
|
||||
|
||||
@ -91,10 +126,10 @@ public class RequestPaginationResolver implements HandlerMethodArgumentResolver
|
||||
}
|
||||
|
||||
// find sorters
|
||||
Set<String> applicableSorters = Optional.ofNullable(parameter.getMethodAnnotation(ApplicableSorters.class)).map(ApplicableSorters::value).map(sorters -> Stream.of(sorters).map(SorterRegistry::getName).collect(Collectors.toUnmodifiableSet())).orElse(Collections.emptySet());
|
||||
List<String> presentSorters = Optional.ofNullable(webRequest.getParameterValues("sort")).map(Arrays::asList).orElse(new ArrayList<>());
|
||||
for (String sorter : presentSorters) {
|
||||
String sortKey = sorter.startsWith("-") ? sorter.substring(1) : sorter;
|
||||
final Set<String> applicableSorters = Optional.ofNullable(parameter.getMethodAnnotation(ApplicableSorters.class)).map(ApplicableSorters::value).map(sorters -> Stream.of(sorters).map(SorterRegistry::getName).collect(Collectors.toUnmodifiableSet())).orElse(Collections.emptySet());
|
||||
final List<String> presentSorters = Optional.ofNullable(webRequest.getParameterValues("sort")).map(Arrays::asList).orElse(new ArrayList<>());
|
||||
for (final String sorter : presentSorters) {
|
||||
final String sortKey = sorter.startsWith("-") ? sorter.substring(1) : sorter;
|
||||
if (!applicableSorters.contains(sortKey)) {
|
||||
throw new HangarApiException(sortKey + " is an invalid sort type for this request");
|
||||
}
|
||||
|
@ -124,7 +124,7 @@ public class AdminController extends HangarComponent {
|
||||
@PermissionRequired(NamedPermission.REVIEWER)
|
||||
@ApplicableFilters({LogActionFilter.class, LogPageFilter.class, LogProjectFilter.class, LogSubjectFilter.class, LogUserFilter.class, LogVersionFilter.class})
|
||||
// TODO add sorters
|
||||
public PaginatedResult<HangarLoggedAction> getActionLog(@NotNull @ConfigurePagination(maxLimit = 50) RequestPagination pagination) {
|
||||
public PaginatedResult<HangarLoggedAction> getActionLog(@NotNull @ConfigurePagination(defaultLimit = 50, maxLimit = 100) RequestPagination pagination) {
|
||||
return actionLogger.getLogs(pagination);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user