Skip to content

Commit 869303f

Browse files
Introduce PropertyPath based SortAccessor.
Allow easy configuration of PropertyPath based sorting. See: #565
1 parent 99c1e2e commit 869303f

10 files changed

+503
-2
lines changed

src/main/java/org/springframework/data/keyvalue/core/AbstractKeyValueAdapter.java

+12-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.data.keyvalue.core;
1717

1818
import java.util.Collection;
19+
import java.util.Comparator;
1920

2021
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
2122
import org.springframework.lang.Nullable;
@@ -35,7 +36,17 @@ public abstract class AbstractKeyValueAdapter implements KeyValueAdapter {
3536
* Creates new {@link AbstractKeyValueAdapter} with using the default query engine.
3637
*/
3738
protected AbstractKeyValueAdapter() {
38-
this(null);
39+
this((QueryEngine<? extends KeyValueAdapter, ?, ?>) null);
40+
}
41+
42+
/**
43+
* Creates new {@link AbstractKeyValueAdapter} with using the default query engine and provided comparator for sorting.
44+
*
45+
* @param sortAccessor must not be {@literal null}.
46+
* @since 3.1.10
47+
*/
48+
protected AbstractKeyValueAdapter(SortAccessor<Comparator<?>> sortAccessor) {
49+
this(new SpelQueryEngine(sortAccessor));
3950
}
4051

4152
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.keyvalue.core;
17+
18+
import java.util.Comparator;
19+
import java.util.Optional;
20+
21+
import org.springframework.data.domain.Sort.Direction;
22+
import org.springframework.data.domain.Sort.NullHandling;
23+
import org.springframework.data.domain.Sort.Order;
24+
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
25+
import org.springframework.lang.Nullable;
26+
27+
/**
28+
* @author Christoph Strobl
29+
* @since 3.1.10
30+
*/
31+
public class PathSortAccessor implements SortAccessor<Comparator<?>> {
32+
33+
@Nullable
34+
@Override
35+
public Comparator<?> resolve(KeyValueQuery<?> query) {
36+
37+
if (query.getSort().isUnsorted()) {
38+
return null;
39+
}
40+
41+
Optional<Comparator<?>> comparator = Optional.empty();
42+
for (Order order : query.getSort()) {
43+
44+
PropertyPathComparator<Object> pathSort = new PropertyPathComparator<>(order.getProperty());
45+
46+
if (Direction.DESC.equals(order.getDirection())) {
47+
48+
pathSort.desc();
49+
50+
if (!NullHandling.NATIVE.equals(order.getNullHandling())) {
51+
pathSort = NullHandling.NULLS_FIRST.equals(order.getNullHandling()) ? pathSort.nullsFirst()
52+
: pathSort.nullsLast();
53+
}
54+
}
55+
56+
if (!comparator.isPresent()) {
57+
comparator = Optional.of(pathSort);
58+
} else {
59+
60+
PropertyPathComparator<Object> pathSortToUse = pathSort;
61+
comparator = comparator.map(it -> it.thenComparing(pathSortToUse));
62+
}
63+
}
64+
65+
return comparator.orElseThrow(
66+
() -> new IllegalStateException("No sort definitions have been added to this CompoundComparator to compare"));
67+
68+
}
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.keyvalue.core;
17+
18+
import java.util.Comparator;
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
22+
import org.springframework.data.mapping.PropertyPath;
23+
import org.springframework.data.util.Lazy;
24+
import org.springframework.util.comparator.NullSafeComparator;
25+
26+
/**
27+
* @author Christoph Strobl
28+
* @since 3.1.10
29+
*/
30+
public class PropertyPathComparator<T> implements Comparator<T> {
31+
32+
private final String path;
33+
34+
private boolean asc = true;
35+
private boolean nullsFirst = true;
36+
37+
private final Map<Class<?>, PropertyPath> pathCache = new HashMap<>(2);
38+
private Lazy<Comparator<Object>> comparator = Lazy
39+
.of(() -> new NullSafeComparator(Comparator.naturalOrder(), this.nullsFirst));
40+
41+
public PropertyPathComparator(String path) {
42+
this.path = path;
43+
}
44+
45+
@Override
46+
public int compare(T o1, T o2) {
47+
48+
if (o1 == null && o2 == null) {
49+
return 0;
50+
}
51+
if (o1 == null) {
52+
return nullsFirst ? 1 : -1;
53+
}
54+
if (o2 == null) {
55+
return nullsFirst ? 1 : -1;
56+
}
57+
58+
PropertyPath propertyPath = pathCache.computeIfAbsent(o1.getClass(), it -> PropertyPath.from(path, it));
59+
Object value1 = new SimplePropertyPathAccessor<>(o1).getValue(propertyPath);
60+
Object value2 = new SimplePropertyPathAccessor<>(o2).getValue(propertyPath);
61+
62+
return comparator.get().compare(value1, value2) * (asc ? 1 : -1);
63+
}
64+
65+
/**
66+
* Sort {@literal ascending}.
67+
*
68+
* @return
69+
*/
70+
public PropertyPathComparator<T> asc() {
71+
this.asc = true;
72+
return this;
73+
}
74+
75+
/**
76+
* Sort {@literal descending}.
77+
*
78+
* @return
79+
*/
80+
public PropertyPathComparator<T> desc() {
81+
this.asc = false;
82+
return this;
83+
}
84+
85+
/**
86+
* Sort {@literal null} values first.
87+
*
88+
* @return
89+
*/
90+
public PropertyPathComparator<T> nullsFirst() {
91+
this.nullsFirst = true;
92+
return this;
93+
}
94+
95+
/**
96+
* Sort {@literal null} values last.
97+
*
98+
* @return
99+
*/
100+
public PropertyPathComparator<T> nullsLast() {
101+
this.nullsFirst = false;
102+
return this;
103+
}
104+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.keyvalue.core;
17+
18+
import org.springframework.beans.BeanWrapper;
19+
import org.springframework.data.mapping.PropertyPath;
20+
import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper;
21+
22+
/**
23+
* @author Christoph Strobl
24+
* @since 3.1.10
25+
*/
26+
class SimplePropertyPathAccessor<T> {
27+
28+
private final Object root;
29+
30+
public SimplePropertyPathAccessor(Object source) {
31+
this.root = source;
32+
}
33+
34+
Object getValue(PropertyPath path) {
35+
36+
Object currentValue = root;
37+
for (PropertyPath current : path) {
38+
currentValue = wrap(currentValue).getPropertyValue(current.getSegment());
39+
if (currentValue == null) {
40+
break;
41+
}
42+
}
43+
return currentValue;
44+
}
45+
46+
BeanWrapper wrap(Object o) {
47+
return new DirectFieldAccessFallbackBeanWrapper(o);
48+
}
49+
}

src/main/java/org/springframework/data/keyvalue/core/SpelQueryEngine.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,16 @@ class SpelQueryEngine extends QueryEngine<KeyValueAdapter, SpelCriteria, Compara
4444
* Creates a new {@link SpelQueryEngine}.
4545
*/
4646
public SpelQueryEngine() {
47-
super(new SpelCriteriaAccessor(PARSER), new SpelSortAccessor(PARSER));
47+
this(new SpelSortAccessor(PARSER));
48+
}
49+
50+
/**
51+
* Creates a new query engine using provided {@link SortAccessor accessor} for sorting results.
52+
*
53+
* @since 3.1.10
54+
*/
55+
public SpelQueryEngine(SortAccessor<Comparator<?>> sortAccessor) {
56+
super(new SpelCriteriaAccessor(PARSER), sortAccessor);
4857
}
4958

5059
@Override

src/main/java/org/springframework/data/map/MapKeyValueAdapter.java

+19
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.data.map;
1717

1818
import java.util.Collection;
19+
import java.util.Comparator;
1920
import java.util.Map;
2021
import java.util.Map.Entry;
2122
import java.util.concurrent.ConcurrentHashMap;
@@ -25,6 +26,7 @@
2526
import org.springframework.data.keyvalue.core.ForwardingCloseableIterator;
2627
import org.springframework.data.keyvalue.core.KeyValueAdapter;
2728
import org.springframework.data.keyvalue.core.QueryEngine;
29+
import org.springframework.data.keyvalue.core.SortAccessor;
2830
import org.springframework.data.util.CloseableIterator;
2931
import org.springframework.util.Assert;
3032
import org.springframework.util.ClassUtils;
@@ -69,6 +71,23 @@ public MapKeyValueAdapter(Class<? extends Map> mapType) {
6971
this(CollectionFactory.createMap(mapType, 100), mapType, null);
7072
}
7173

74+
/**
75+
* Creates a new {@link MapKeyValueAdapter} using the given {@link Map} as backing store.
76+
*
77+
* @param mapType must not be {@literal null}.
78+
* @param sortAccessor accessor granting access to sorting implementation
79+
* @since 3.1.10
80+
*/
81+
public MapKeyValueAdapter(Class<? extends Map> mapType, SortAccessor<Comparator<?>> sortAccessor) {
82+
83+
super(sortAccessor);
84+
85+
Assert.notNull(mapType, "Store must not be null");
86+
87+
this.store = CollectionFactory.createMap(mapType, 100);
88+
this.keySpaceMapType = (Class<? extends Map>) ClassUtils.getUserClass(store);
89+
}
90+
7291
/**
7392
* Creates a new {@link MapKeyValueAdapter} using the given {@link Map} as backing store and query engine.
7493
*

src/main/java/org/springframework/data/map/repository/config/EnableMapRepositories.java

+9
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.springframework.context.annotation.Import;
3131
import org.springframework.data.keyvalue.core.KeyValueOperations;
3232
import org.springframework.data.keyvalue.core.KeyValueTemplate;
33+
import org.springframework.data.keyvalue.core.SortAccessor;
3334
import org.springframework.data.keyvalue.repository.config.QueryCreatorType;
3435
import org.springframework.data.keyvalue.repository.query.CachingKeyValuePartTreeQuery;
3536
import org.springframework.data.keyvalue.repository.query.SpelQueryCreator;
@@ -143,4 +144,12 @@
143144
*/
144145
@SuppressWarnings("rawtypes")
145146
Class<? extends Map> mapType() default ConcurrentHashMap.class;
147+
148+
/**
149+
* Configures the {@link SortAccessor accessor} for sorting results.
150+
*
151+
* @return {@link SortAccessor} to indicate usage of default implementation.
152+
* @since 3.1.10
153+
*/
154+
Class<? extends SortAccessor> sortAccessor() default SortAccessor.class;
146155
}

src/main/java/org/springframework/data/map/repository/config/MapRepositoryConfigurationExtension.java

+21
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,18 @@
1717

1818
import java.util.Map;
1919

20+
import org.springframework.beans.BeanUtils;
2021
import org.springframework.beans.factory.config.BeanDefinition;
2122
import org.springframework.beans.factory.support.AbstractBeanDefinition;
2223
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
2324
import org.springframework.core.type.AnnotationMetadata;
2425
import org.springframework.data.config.ParsingUtils;
2526
import org.springframework.data.keyvalue.core.KeyValueTemplate;
27+
import org.springframework.data.keyvalue.core.SortAccessor;
2628
import org.springframework.data.keyvalue.repository.config.KeyValueRepositoryConfigurationExtension;
2729
import org.springframework.data.map.MapKeyValueAdapter;
2830
import org.springframework.data.repository.config.RepositoryConfigurationSource;
31+
import org.springframework.lang.Nullable;
2932

3033
/**
3134
* @author Christoph Strobl
@@ -54,6 +57,11 @@ protected AbstractBeanDefinition getDefaultKeyValueTemplateBeanDefinition(
5457
BeanDefinitionBuilder adapterBuilder = BeanDefinitionBuilder.rootBeanDefinition(MapKeyValueAdapter.class);
5558
adapterBuilder.addConstructorArgValue(getMapTypeToUse(configurationSource));
5659

60+
SortAccessor<?> sortAccessor = getSortAccessor(configurationSource);
61+
if(sortAccessor != null) {
62+
adapterBuilder.addConstructorArgValue(sortAccessor);
63+
}
64+
5765
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(KeyValueTemplate.class);
5866
builder
5967
.addConstructorArgValue(ParsingUtils.getSourceBeanDefinition(adapterBuilder, configurationSource.getSource()));
@@ -68,4 +76,17 @@ private static Class<? extends Map> getMapTypeToUse(RepositoryConfigurationSourc
6876
return (Class<? extends Map>) ((AnnotationMetadata) source.getSource()).getAnnotationAttributes(
6977
EnableMapRepositories.class.getName()).get("mapType");
7078
}
79+
80+
@Nullable
81+
private static SortAccessor<?> getSortAccessor(RepositoryConfigurationSource source) {
82+
83+
Class<? extends SortAccessor<?>> sortAccessorType = (Class<? extends SortAccessor<?>>) ((AnnotationMetadata) source.getSource()).getAnnotationAttributes(
84+
EnableMapRepositories.class.getName()).get("sortAccessor");
85+
86+
if(sortAccessorType != null && !sortAccessorType.isInterface()) {
87+
return BeanUtils.instantiateClass(sortAccessorType);
88+
}
89+
90+
return null;
91+
}
7192
}

0 commit comments

Comments
 (0)