Skip to content

Commit ffd013c

Browse files
authored
Merge pull request #40 from aboutbits/finc-788-be-move-core-types-for-sorting-to-toolbox
add sort&page parameters
2 parents 8e16bb4 + cb8d6a5 commit ffd013c

File tree

7 files changed

+920
-5
lines changed

7 files changed

+920
-5
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package it.aboutbits.springboot.toolbox.parameter;
2+
3+
import lombok.Getter;
4+
import lombok.Setter;
5+
import lombok.ToString;
6+
import lombok.experimental.Accessors;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.springframework.data.domain.PageRequest;
9+
import org.springframework.data.domain.Sort;
10+
11+
@Slf4j
12+
@ToString
13+
@Accessors(fluent = true)
14+
public final class PageParameter {
15+
@Getter
16+
@Setter
17+
@SuppressWarnings({"checkstyle:StaticVariableName", "java:S3008"})
18+
private static int MAX_PAGE_SIZE = 9999;
19+
20+
@Getter
21+
@Setter
22+
@SuppressWarnings({"checkstyle:StaticVariableName", "java:S3008"})
23+
private static int DEFAULT_PAGE_SIZE = 50;
24+
25+
private record PageInfo(int page, int size, boolean paged) {
26+
}
27+
28+
private final PageInfo pageInfo;
29+
30+
private PageParameter(Integer page, Integer size) {
31+
var actualPage = (page == null) ? 0 : page;
32+
var actualSize = (size == null) ? DEFAULT_PAGE_SIZE : size;
33+
34+
if (actualSize > MAX_PAGE_SIZE) {
35+
log.warn("Page size exceeded maximum [actualSize={}, maxSize={}]", actualSize, MAX_PAGE_SIZE);
36+
}
37+
38+
pageInfo = new PageInfo(
39+
Math.max(0, actualPage),
40+
Math.min(actualSize, MAX_PAGE_SIZE),
41+
true
42+
);
43+
}
44+
45+
private PageParameter() {
46+
pageInfo = new PageInfo(0, Integer.MAX_VALUE, false);
47+
}
48+
49+
public static PageParameter of(Integer page) {
50+
return new PageParameter(page, null);
51+
}
52+
53+
public static PageParameter of(Integer page, Integer size) {
54+
return new PageParameter(page, size);
55+
}
56+
57+
public static PageParameter defaultPage() {
58+
return new PageParameter(null, null);
59+
}
60+
61+
public static PageParameter unpaged() {
62+
return new PageParameter();
63+
}
64+
65+
public int page() {
66+
return pageInfo.page();
67+
}
68+
69+
public int size() {
70+
return pageInfo.size();
71+
}
72+
73+
public boolean isPaged() {
74+
return pageInfo.paged();
75+
}
76+
77+
public boolean isUnpaged() {
78+
return !pageInfo.paged();
79+
}
80+
81+
public PageRequest toPageRequest() {
82+
return PageRequest.of(page(), size());
83+
}
84+
85+
public PageRequest toPageRequest(Sort sort) {
86+
return PageRequest.of(page(), size(), sort);
87+
}
88+
89+
@Override
90+
public boolean equals(Object obj) {
91+
if (obj == null) {
92+
return false;
93+
}
94+
if (obj == this) {
95+
return true;
96+
}
97+
if (obj instanceof PageParameter pageParameter) {
98+
return this.pageInfo.equals(pageParameter.pageInfo);
99+
}
100+
return false;
101+
}
102+
103+
@Override
104+
public int hashCode() {
105+
return super.hashCode();
106+
}
107+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package it.aboutbits.springboot.toolbox.parameter;
2+
3+
import lombok.NonNull;
4+
import org.springframework.data.domain.Sort;
5+
6+
import java.util.Collections;
7+
import java.util.List;
8+
import java.util.Map;
9+
import java.util.stream.Collectors;
10+
import java.util.stream.Stream;
11+
12+
public record SortParameter<T extends Enum<?> & SortParameter.Definition>(List<SortField> sortFields) {
13+
private static final String DEFAULT_SORT_PROPERTY = "id";
14+
private static final Sort DEFAULT_SORT = Sort.by(
15+
Sort.Direction.ASC,
16+
DEFAULT_SORT_PROPERTY
17+
);
18+
19+
public static <T extends Enum<?> & Definition> SortParameter<T> unsorted() {
20+
return new SortParameter<>(Collections.emptyList());
21+
}
22+
23+
@SafeVarargs
24+
public static <T extends Enum<?> & Definition> SortParameter<T> by(
25+
@NonNull T... sortDefinitions
26+
) {
27+
return new SortParameter<>(
28+
Stream.of(sortDefinitions)
29+
.map(sortDefinition -> new SortField(
30+
sortDefinition.name(),
31+
Sort.Direction.ASC,
32+
Sort.NullHandling.NATIVE
33+
)
34+
)
35+
.toList()
36+
);
37+
}
38+
39+
public static <T extends Enum<?> & Definition> SortParameter<T> by(
40+
@NonNull T sortDefinition,
41+
@NonNull Sort.Direction direction,
42+
@NonNull Sort.NullHandling nullHandling
43+
) {
44+
return new SortParameter<>(
45+
List.of(new SortField(
46+
sortDefinition.name(),
47+
direction,
48+
nullHandling
49+
)
50+
)
51+
);
52+
}
53+
54+
public SortParameter<T> or(@NonNull SortParameter<T> fallback) {
55+
return sortFields == null || sortFields.isEmpty() ? fallback : this;
56+
}
57+
58+
public Sort buildSortWithoutDefault(@NonNull Map<T, String> mapping) {
59+
var stringMapping = mapping.entrySet().stream()
60+
.collect(Collectors.toMap(
61+
entry -> entry.getKey().name(),
62+
Map.Entry::getValue
63+
));
64+
65+
return buildSort(stringMapping, false);
66+
}
67+
68+
public Sort buildSort(@NonNull Map<T, String> mapping) {
69+
var stringMapping = mapping.entrySet().stream()
70+
.collect(Collectors.toMap(
71+
entry -> entry.getKey().name(),
72+
Map.Entry::getValue
73+
));
74+
75+
return buildSort(stringMapping, true);
76+
}
77+
78+
// SonarLint: Replace this usage of 'Stream.collect(Collectors.toList())' with 'Stream.toList()' and ensure that the list is unmodified.
79+
@SuppressWarnings("java:S6204")
80+
private Sort buildSort(@NonNull Map<String, String> mapping, boolean withDefault) {
81+
if (sortFields == null || sortFields.isEmpty()) {
82+
return withDefault ? DEFAULT_SORT : Sort.unsorted();
83+
}
84+
85+
var additionalSort = Sort.by(
86+
sortFields.stream()
87+
.filter(sortField -> mapping.containsKey(sortField.property()))
88+
.map(sortField -> new Sort.Order(
89+
sortField.direction(),
90+
mapping.get(sortField.property()),
91+
sortField.nullHandling()
92+
))
93+
// We do not use .toList() here as we potentially want to modify the sort list later in the StoreImpl
94+
.collect(Collectors.toList())
95+
);
96+
97+
if (withDefault) {
98+
var includesDefault = additionalSort.getOrderFor(DEFAULT_SORT_PROPERTY) != null;
99+
if (!includesDefault) {
100+
return additionalSort.and(DEFAULT_SORT);
101+
}
102+
}
103+
return additionalSort;
104+
}
105+
106+
public record SortField(
107+
@NonNull String property,
108+
@NonNull Sort.Direction direction,
109+
@NonNull Sort.NullHandling nullHandling
110+
) {
111+
}
112+
113+
public interface Definition {
114+
}
115+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package it.aboutbits.springboot.toolbox.persistence;
2+
3+
import it.aboutbits.springboot.toolbox.parameter.SortParameter;
4+
import lombok.NonNull;
5+
6+
import java.util.HashMap;
7+
8+
public final class SortMappings<T extends Enum<?> & SortParameter.Definition> extends HashMap<T, String> {
9+
private SortMappings() {
10+
super();
11+
}
12+
13+
public static <T extends Enum<?> & SortParameter.Definition> Mapping<T> map(
14+
@NonNull T property,
15+
@NonNull String column
16+
) {
17+
return new Mapping<>(property, column);
18+
}
19+
20+
@SafeVarargs
21+
public static <T extends Enum<?> & SortParameter.Definition> SortMappings<T> of(
22+
@NonNull Mapping<T>... mappings
23+
) {
24+
var sortMappings = new SortMappings<T>();
25+
26+
for (var mapping : mappings) {
27+
sortMappings.put(mapping.property(), mapping.column());
28+
}
29+
30+
return sortMappings;
31+
}
32+
33+
public record Mapping<T extends Enum<?> & SortParameter.Definition>(@NonNull T property, @NonNull String column) {
34+
}
35+
}

src/main/java/it/aboutbits/springboot/toolbox/swagger/sort_parameter/SortParameterCustomizer.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@
33
import io.swagger.v3.oas.models.OpenAPI;
44
import io.swagger.v3.oas.models.media.ArraySchema;
55
import io.swagger.v3.oas.models.media.StringSchema;
6-
import lombok.RequiredArgsConstructor;
6+
import it.aboutbits.springboot.toolbox.parameter.SortParameter;
77
import org.springdoc.core.customizers.OpenApiCustomizer;
88

9-
@RequiredArgsConstructor
109
public class SortParameterCustomizer implements OpenApiCustomizer {
11-
private final Class<?> sortParameterSortFieldClass;
10+
private final String nameToMatch = "." + SortParameter.class.getSimpleName();
1211

1312
@Override
1413
public void customise(OpenAPI openApi) {
@@ -29,7 +28,7 @@ public void customise(OpenAPI openApi) {
2928
continue;
3029
}
3130

32-
if (parameter.getSchema().get$ref().endsWith(".SortParameter")) {
31+
if (parameter.getSchema().get$ref().endsWith(nameToMatch)) {
3332
parameter.required(false);
3433
parameter.description(
3534
"""
@@ -85,7 +84,7 @@ public void customise(OpenAPI openApi) {
8584
);
8685
var itemSchema = new StringSchema()._default("property:asc:last");
8786
itemSchema.setDescription(
88-
"{\"originalTypeFqn\": \"%s\"}".formatted(sortParameterSortFieldClass.getName())
87+
"{\"originalTypeFqn\": \"%s\"}".formatted(SortParameter.class.getName())
8988
);
9089

9190
parameter.setSchema(new ArraySchema().items(
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package it.aboutbits.springboot.toolbox.web;
2+
3+
import it.aboutbits.springboot.toolbox.parameter.SortParameter;
4+
import lombok.NonNull;
5+
import org.springframework.core.MethodParameter;
6+
import org.springframework.web.bind.support.WebDataBinderFactory;
7+
import org.springframework.web.context.request.NativeWebRequest;
8+
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
9+
import org.springframework.web.method.support.ModelAndViewContainer;
10+
11+
import java.util.ArrayList;
12+
import java.util.Objects;
13+
14+
public class SortParameterResolver implements HandlerMethodArgumentResolver {
15+
@Override
16+
public boolean supportsParameter(MethodParameter parameter) {
17+
return SortParameter.class.isAssignableFrom(parameter.getParameterType());
18+
}
19+
20+
@Override
21+
public Object resolveArgument(
22+
@NonNull MethodParameter parameter,
23+
ModelAndViewContainer mavContainer,
24+
NativeWebRequest webRequest,
25+
WebDataBinderFactory binderFactory
26+
) throws IllegalArgumentException {
27+
var sorts = webRequest.getParameterValues(
28+
Objects.requireNonNull(parameter.getParameterName())
29+
);
30+
31+
// Allow empty order list
32+
if (sorts == null) {
33+
return SortParameter.unsorted();
34+
}
35+
36+
var sortFields = new ArrayList<SortParameter.SortField>(sorts.length);
37+
for (var order : sorts) {
38+
var parts = order.split(":");
39+
if (parts.length == 1) {
40+
sortFields.add(new SortParameter.SortField(
41+
parts[0],
42+
org.springframework.data.domain.Sort.DEFAULT_DIRECTION,
43+
org.springframework.data.domain.Sort.NullHandling.NATIVE
44+
));
45+
} else if (parts.length == 2) {
46+
sortFields.add(new SortParameter.SortField(
47+
parts[0],
48+
org.springframework.data.domain.Sort.Direction.fromString(parts[1]),
49+
org.springframework.data.domain.Sort.NullHandling.NATIVE
50+
));
51+
} else if (parts.length == 3) {
52+
sortFields.add(new SortParameter.SortField(
53+
parts[0],
54+
org.springframework.data.domain.Sort.Direction.fromString(parts[1]),
55+
nullHandlingFromString(parts[2])
56+
));
57+
} else {
58+
throw new IllegalArgumentException(
59+
"Invalid sort order, only a maximum of two semicolons are allowed in format <property>[:asc|desc][:native|first|last] -> Your input " + order
60+
);
61+
}
62+
}
63+
64+
return new SortParameter<>(sortFields);
65+
}
66+
67+
private org.springframework.data.domain.Sort.NullHandling nullHandlingFromString(@NonNull String value) {
68+
return switch (value.toLowerCase()) {
69+
case "first":
70+
yield org.springframework.data.domain.Sort.NullHandling.NULLS_FIRST;
71+
case "last":
72+
yield org.springframework.data.domain.Sort.NullHandling.NULLS_LAST;
73+
case "native":
74+
yield org.springframework.data.domain.Sort.NullHandling.NATIVE;
75+
default:
76+
throw new IllegalArgumentException(
77+
"Only native, first or last are allowed as null handling strategy -> Your input " + value
78+
);
79+
};
80+
}
81+
}

0 commit comments

Comments
 (0)