sorting on author page

This commit is contained in:
Jake Potrebic 2021-03-25 20:58:47 -07:00
parent 1e97e576ca
commit 4a6c450644
No known key found for this signature in database
GPG Key ID: 7C58557EC9C421F8
15 changed files with 273 additions and 151 deletions

View File

@ -5,6 +5,7 @@
:items="authors.result"
:options.sync="options"
:server-items-length="authors.pagination.count"
multi-sort
:loading="loading"
class="elevation-1"
>
@ -33,9 +34,9 @@ import { HangarComponent } from '~/components/mixins';
})
export default class AuthorsPage extends HangarComponent {
headers: DataTableHeader[] = [
{ text: '', value: 'pic' },
{ text: '', value: 'pic', sortable: false },
{ text: 'Username', value: 'name' },
{ text: 'Roles', value: 'roles' },
{ text: 'Roles', value: 'roles', sortable: false },
{ text: 'Joined', value: 'joinDate' },
{ text: 'Projects', value: 'projectCount' },
];
@ -59,10 +60,15 @@ export default class AuthorsPage extends HangarComponent {
}
this.loading = true;
this.$api.request<PaginatedResult<User>>('authors', false, 'get', this.requestOptions).then((authors) => {
this.authors = authors;
this.loading = false;
});
this.$api
.request<PaginatedResult<User>>('authors', false, 'get', this.requestOptions)
.then((authors) => {
this.authors = authors;
})
.catch(this.$util.handleRequestError)
.finally(() => {
this.loading = false;
});
}
get requestOptions() {
@ -70,12 +76,16 @@ export default class AuthorsPage extends HangarComponent {
return {};
}
let sort = '';
if (this.options.sortBy.length === 1) {
sort = this.options.sortBy[0];
if (this.options.sortDesc[0]) {
sort = '-' + sort;
const sort: string[] = [];
for (let i = 0; i < this.options.sortBy.length; i++) {
let sortStr = this.options.sortBy[i];
if (sortStr === 'name') {
sortStr = 'username'; // TODO how to get around this... should we change the field on User to be username?
}
if (this.options.sortDesc[i]) {
sortStr = '-' + sortStr;
}
sort.push(sortStr);
}
return {
limit: this.options.itemsPerPage,

View File

@ -2,6 +2,8 @@ package io.papermc.hangar.controller.api.v1;
import io.papermc.hangar.controller.HangarController;
import io.papermc.hangar.controller.api.v1.interfaces.IUsersController;
import io.papermc.hangar.controller.extras.pagination.sorters.ApplicableSorters;
import io.papermc.hangar.controller.extras.pagination.sorters.Sorters;
import io.papermc.hangar.model.api.PaginatedResult;
import io.papermc.hangar.model.api.User;
import io.papermc.hangar.model.api.project.ProjectCompact;
@ -44,6 +46,7 @@ public class UsersController extends HangarController implements IUsersControlle
}
@Override
@ApplicableSorters({ Sorters.USERNAME_VALUE, Sorters.JOINED_VALUE, Sorters.PROJECT_COUNT_VALUE })
public ResponseEntity<PaginatedResult<User>> getAuthors(@NotNull RequestPagination pagination) {
return ResponseEntity.ok(usersApiService.getAuthors(pagination));
}

View File

@ -1,6 +0,0 @@
package io.papermc.hangar.controller.extras.pagination;
public interface QueryIdentified {
String getQueryParamName();
}

View File

@ -1,6 +0,0 @@
package io.papermc.hangar.controller.extras.pagination;
public interface SortBy extends QueryIdentified {
void createSql(StringBuilder sb);
}

View File

@ -0,0 +1,15 @@
package io.papermc.hangar.controller.extras.pagination.filters;
import io.papermc.hangar.controller.extras.pagination.filters.Filter.FilterInstance;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApplicableFilters {
Class<? extends Filter<? extends FilterInstance>>[] value();
}

View File

@ -1,10 +1,12 @@
package io.papermc.hangar.controller.extras.pagination;
package io.papermc.hangar.controller.extras.pagination.filters;
import io.papermc.hangar.controller.extras.pagination.Filter.FilterInstance;
import io.papermc.hangar.controller.extras.pagination.filters.Filter.FilterInstance;
import org.jdbi.v3.core.statement.SqlStatement;
import org.springframework.web.context.request.NativeWebRequest;
public interface Filter<F extends FilterInstance> extends QueryIdentified {
public interface Filter<F extends FilterInstance> {
String getQueryParamName();
default boolean supports(NativeWebRequest webRequest) {
return webRequest.getParameterMap().containsKey(getQueryParamName());
@ -13,6 +15,7 @@ public interface Filter<F extends FilterInstance> extends QueryIdentified {
F create(NativeWebRequest webRequest);
interface FilterInstance {
void createSql(StringBuilder sb, SqlStatement<?> q);
}

View File

@ -0,0 +1,30 @@
package io.papermc.hangar.controller.extras.pagination.filters;
import io.papermc.hangar.controller.extras.pagination.filters.Filter.FilterInstance;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
@SuppressWarnings("unchecked")
public class Filters {
private static final Map<Class<? extends Filter<?>>, Filter<?>> ALL_FILTERS = new HashMap<>();
public static final ProjectCategoryFilter PROJECT_CATEGORY_FILTER = register(new ProjectCategoryFilter());
private static <F extends Filter<?>> F register(F filter) {
ALL_FILTERS.put((Class<? extends Filter<?>>) filter.getClass(), filter);
return filter;
}
public static final Class<? extends Filter<? extends FilterInstance>>[] TEST = new Class[] { ProjectCategoryFilter.class };
@NotNull
public static <T extends Filter<? extends FilterInstance>> T getFilter(Class<T> filterClass) {
if (ALL_FILTERS.containsKey(filterClass)) {
return (T) ALL_FILTERS.get(filterClass);
}
throw new IllegalArgumentException(filterClass + " is not a registered filter");
}
}

View File

@ -1,6 +1,6 @@
package io.papermc.hangar.controller.extras.pagination;
package io.papermc.hangar.controller.extras.pagination.filters;
import io.papermc.hangar.controller.extras.pagination.ProjectCategoryFilter.ProjectCategoryFilterInstance;
import io.papermc.hangar.controller.extras.pagination.filters.ProjectCategoryFilter.ProjectCategoryFilterInstance;
import io.papermc.hangar.model.common.projects.Category;
import org.jdbi.v3.core.statement.SqlStatement;
import org.springframework.web.context.request.NativeWebRequest;
@ -21,6 +21,7 @@ public class ProjectCategoryFilter implements Filter<ProjectCategoryFilterInstan
static class ProjectCategoryFilterInstance implements FilterInstance {
// TODO multiple categories
private final Category category;
public ProjectCategoryFilterInstance(NativeWebRequest webRequest) {

View File

@ -0,0 +1,16 @@
package io.papermc.hangar.controller.extras.pagination.sorters;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApplicableSorters {
/**
* use {@link Sorters} static final strings
*/
String[] value();
}

View File

@ -0,0 +1,17 @@
package io.papermc.hangar.controller.extras.pagination.sorters;
public enum SortDirection {
ASCENDING(" ASC"),
DESCENDING(" DESC");
private final String sql;
SortDirection(String sql) {
this.sql = sql;
}
@Override
public String toString() {
return sql;
}
}

View File

@ -0,0 +1,52 @@
package io.papermc.hangar.controller.extras.pagination.sorters;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
public class Sorters {
private static final Map<String, ApplySorter> SORTERS = new HashMap<>();
public static final String JOINED_VALUE = "joinDate";
public static final ApplySorter JOINED = register(JOINED_VALUE, (sb, dir) -> sb.append("u.join_date").append(dir));
public static final String USERNAME_VALUE = "username";
public static final ApplySorter USERNAME = register(USERNAME_VALUE, (sb, dir) -> sb.append("u.name").append(dir));
public static final String PROJECT_COUNT_VALUE = "projectCount";
public static final ApplySorter PROJECT_COUNT = register(PROJECT_COUNT_VALUE, (sb, dir) -> sb.append("project_count").append(dir));
private static ApplySorter register(String name, ApplySorter applySorter) {
if (SORTERS.containsKey(name)) {
throw new IllegalArgumentException(name + " is already registered");
}
SORTERS.put(name, applySorter);
return applySorter;
}
@NotNull
public static ApplySorter getSorter(String key) {
if (SORTERS.containsKey(key)) {
return SORTERS.get(key);
}
throw new IllegalArgumentException(key + " is not a registered sorter");
}
@FunctionalInterface
public interface ApplySorter {
void applySorting(StringBuilder sb, SortDirection dir);
default Consumer<StringBuilder> ascending() {
return sb -> applySorting(sb, SortDirection.ASCENDING);
}
default Consumer<StringBuilder> descending() {
return sb -> applySorting(sb, SortDirection.DESCENDING);
}
}
}

View File

@ -1,91 +1,108 @@
package io.papermc.hangar.controller.extras.resolver;
import io.papermc.hangar.controller.extras.pagination.Filter;
import io.papermc.hangar.controller.extras.pagination.Filter.FilterInstance;
import io.papermc.hangar.controller.extras.pagination.ProjectCategoryFilter;
import io.papermc.hangar.controller.extras.pagination.filters.ApplicableFilters;
import io.papermc.hangar.controller.extras.pagination.filters.Filter;
import io.papermc.hangar.controller.extras.pagination.filters.Filter.FilterInstance;
import io.papermc.hangar.controller.extras.pagination.filters.Filters;
import io.papermc.hangar.controller.extras.pagination.sorters.ApplicableSorters;
import io.papermc.hangar.controller.extras.pagination.sorters.Sorters;
import io.papermc.hangar.exceptions.HangarApiException;
import io.papermc.hangar.model.api.requests.RequestPagination;
import org.jetbrains.annotations.NotNull;
import org.springframework.core.MethodParameter;
import org.springframework.lang.NonNull;
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.HashMap;
import java.util.HashSet;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.Optional;
public class PaginationResolver implements HandlerMethodArgumentResolver {
private final HandlerMethodArgumentResolver delegate;
// Map<Predicate<NativeWebRequest>, FilterConstructor> map = Map.of(
// ProjectCategoryFilter::supports, ProjectCategoryFilter::new
// );
List<Filter<?>> filters = List.of(
new ProjectCategoryFilter()
);
@FunctionalInterface
interface FilterConstructor {
Filter create(NativeWebRequest webRequest);
}
public PaginationResolver(HandlerMethodArgumentResolver delegate) {
this.delegate = delegate;
}
@Override
public boolean supportsParameter(@NonNull MethodParameter parameter) {
public boolean supportsParameter(@NotNull MethodParameter parameter) {
return delegate.supportsParameter(parameter);
}
@Override
public Object resolveArgument(@NonNull MethodParameter parameter, ModelAndViewContainer mavContainer, @NonNull NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
public Object resolveArgument(@NotNull MethodParameter parameter, ModelAndViewContainer mavContainer, @NotNull NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
Object result = delegate.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
filters.forEach(f -> {
if (f.supports(webRequest)) {
FilterInstance filterInstance = f.create(webRequest);
// add filter to RequestPagination object
}
});
if (result instanceof RequestPagination) {
RequestPagination pagination = (RequestPagination) result;
Set<String> knownParams = new HashSet<>();
knownParams.add("limit");
knownParams.add("offset");
knownParams.add("sort");
for (Parameter param : parameter.getExecutable().getParameters()) {
knownParams.add(param.getName());
}
Map<String, String> filters = new HashMap<>();
for (String key : webRequest.getParameterMap().keySet()) {
if (!knownParams.contains(key)) {
filters.put(key, String.join(",", webRequest.getParameterValues(key)));
}
}
pagination.setFilters(filters);
Class<? extends Filter<? extends FilterInstance>>[] applicableFilters = Optional.ofNullable(parameter.getMethodAnnotation(ApplicableFilters.class)).map(ApplicableFilters::value).orElse(null);
String[] sorts = webRequest.getParameterMap().get("sort");
List<String> sort = new ArrayList<>();
if (sorts != null && sorts.length > 0) {
for (String s : sorts) {
if (s != null && s.length() > 0) {
sort.add(s);
if (applicableFilters != null) {
for (Class<? extends Filter<? extends FilterInstance>> filter : applicableFilters) {
if (Filters.getFilter(filter).supports(webRequest)) {
pagination.getFilters().add(Filters.getFilter(filter).create(webRequest));
}
}
pagination.setSorts(sort);
}
List<String> applicableSorters = Optional.ofNullable(parameter.getMethodAnnotation(ApplicableSorters.class)).map(ApplicableSorters::value).map(Arrays::asList).orElse(new ArrayList<>());
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;
if (!applicableSorters.contains(sortKey)) {
throw new HangarApiException(sortKey + " is an invalid sort type for this request");
}
pagination.getSorters().add(sorter.startsWith("-") ? Sorters.getSorter(sortKey).descending() : Sorters.getSorter(sortKey).ascending());
}
//
// if (applicableSorters != null) {
// for (String sorter : applicableSorters) {
// if (webRequest.getParameterMap().containsKey(sorter)) {
//
// }
// }
// }
}
// if (result instanceof RequestPagination) {
// RequestPagination pagination = (RequestPagination) result;
// Set<String> knownParams = new HashSet<>();
// knownParams.add("limit");
// knownParams.add("offset");
// knownParams.add("sort");
// for (Parameter param : parameter.getExecutable().getParameters()) {
// knownParams.add(param.getName());
// }
//
// Map<String, String> filters = new HashMap<>();
// for (String key : webRequest.getParameterMap().keySet()) {
// if (!knownParams.contains(key)) {
// filters.put(key, String.join(",", webRequest.getParameterValues(key)));
// }
// }
// pagination.setFilters(filters);
//
// String[] sorts = webRequest.getParameterMap().get("sort");
// List<String> sort = new ArrayList<>();
// if (sorts != null && sorts.length > 0) {
// for (String s : sorts) {
// if (s != null && s.length() > 0) {
// sort.add(s);
// }
// }
// pagination.setSorts(sort);
// }
// }
return result;
}
}

View File

@ -4,7 +4,6 @@ import io.papermc.hangar.db.extras.BindPagination;
import io.papermc.hangar.model.api.User;
import io.papermc.hangar.model.api.project.ProjectCompact;
import io.papermc.hangar.model.api.requests.RequestPagination;
import org.jdbi.v3.sqlobject.config.RegisterConstructorMapper;
import org.jdbi.v3.sqlobject.customizer.BindList;
import org.jdbi.v3.sqlobject.customizer.Define;
@ -97,7 +96,8 @@ public interface UsersApiDAO {
" FROM users u" +
" WHERE u.id IN " +
" (SELECT DISTINCT p.owner_id FROM projects p WHERE p.visibility != 1)" +
" <pagination>")
" <sorters>" +
" <offsetLimit>")
List<User> getAuthors(@BindPagination RequestPagination pagination);
@SqlQuery("SELECT count(DISTINCT p.owner_id) FROM projects p WHERE p.visibility != 1")

View File

@ -14,7 +14,6 @@ import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
import java.util.regex.Pattern;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
@ -23,71 +22,42 @@ public @interface BindPagination {
class BindPaginationFactory implements SqlStatementCustomizerFactory {
private static final Pattern valid = Pattern.compile("[a-zA-Z_]+");
private static final Pattern dot = Pattern.compile("\\.");
@Override
public SqlStatementParameterCustomizer createForParameter(final Annotation annotation, final Class<?> sqlObjectType, final Method method, final Parameter param, final int index, final Type paramType) {
return (q, arg) -> {
RequestPagination pagination = (RequestPagination) arg;
StringBuilder sb = new StringBuilder();
filter(pagination, sb, q);
sort(pagination, sb);
offsetLimit(pagination, sb, q);
// use filters/sort here
// set the sql
q.define("pagination", sb.toString());
filter(pagination, q);
sorters(pagination, q);
offsetLimit(pagination, q);
};
}
private void filter(RequestPagination pagination, StringBuilder sb, SqlStatement<?> q) {
pagination.getFilters().forEach((key, value) -> {
if (validate(key)) {
sb.append(" AND ").append(key).append(" = :").append(key);
q.bind(key, value);
}
});
private void filter(RequestPagination pagination, SqlStatement<?> q) {
StringBuilder sb = new StringBuilder();
pagination.getFilters().forEach(filter -> filter.createSql(sb, q));
q.define("filters", sb.toString());
}
private void sort(RequestPagination pagination, StringBuilder sb) {
if (!pagination.getSorts().isEmpty()) {
private void sorters(RequestPagination pagination, SqlStatement<?> q) {
StringBuilder sb = new StringBuilder();
if (!pagination.getSorters().isEmpty()) {
sb.append(" ORDER BY ");
boolean first = true;
for (String sort : pagination.getSorts()) {
if (first) {
first = false;
} else {
sb.append(", ");
}
boolean desc = sort.startsWith("-");
if (desc) {
sort = sort.substring(1);
}
if (validate(sort)) {
sb.append(sort).append(" ").append(desc ? "DESC" : "ASC");
}
}
var iter = pagination.getSorters().iterator();
while (iter.hasNext()) {
iter.next().accept(sb);
if (iter.hasNext()) {
sb.append(", ");
}
}
q.define("sorters", sb.toString());
}
private void offsetLimit(RequestPagination pagination, StringBuilder sb, SqlStatement<?> q) {
sb.append(" LIMIT :limit OFFSET :offset ");
private void offsetLimit(RequestPagination pagination, SqlStatement<?> q) {
q.bind("limit", pagination.getLimit());
q.bind("offset", pagination.getOffset());
}
private boolean validate(String key) {
if (key.contains(".")) {
String[] keys = dot.split(key);
if (keys.length == 2) {
return validate(keys[0]) && validate(keys[1]);
} else {
return false;
}
} else return valid.matcher(key).matches();
q.define("offsetLimit", " LIMIT :limit OFFSET :offset ");
}
}
}

View File

@ -1,32 +1,40 @@
package io.papermc.hangar.model.api.requests;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.papermc.hangar.controller.extras.ApiUtils;
import io.papermc.hangar.controller.extras.pagination.filters.Filter.FilterInstance;
import io.swagger.annotations.ApiModelProperty;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.papermc.hangar.controller.extras.ApiUtils;
import io.swagger.annotations.ApiModelProperty;
import java.util.function.Consumer;
public class RequestPagination {
@ApiModelProperty(value = "The maximum amount of items to return", example = "1", allowEmptyValue = true, allowableValues = "range[1, 25]")
private long limit = ApiUtils.limitOrDefault(null);
@ApiModelProperty(value = "Where to start searching", example = "0", allowEmptyValue = true, allowableValues = "range[0, infinity]")
private long offset = 0;
@JsonIgnore
private Map<String, String> filters = new HashMap<>();
@JsonIgnore
private List<String> sorts = new ArrayList<>();
@ApiModelProperty(hidden = true)
private final List<FilterInstance> filters;
public RequestPagination() { }
@JsonIgnore
@ApiModelProperty(hidden = true)
private final List<Consumer<StringBuilder>> sorters;
public RequestPagination() {
this.filters = new ArrayList<>();
this.sorters = new ArrayList<>();
}
protected RequestPagination(long limit, long offset) {
this.limit = limit;
this.offset = offset;
this.filters = new ArrayList<>();
this.sorters = new ArrayList<>();
}
public long getLimit() {
@ -45,20 +53,12 @@ public class RequestPagination {
this.offset = ApiUtils.offsetOrZero(offset);
}
public Map<String, String> getFilters() {
public List<FilterInstance> getFilters() {
return filters;
}
public void setFilters(Map<String, String> filters) {
this.filters = filters;
}
public List<String> getSorts() {
return sorts;
}
public void setSorts(List<String> sorts) {
this.sorts = sorts;
public List<Consumer<StringBuilder>> getSorters() {
return sorters;
}
@Override