Introduce PropertyPath based SortAccessor.

Allow easy configuration of PropertyPath based sorting.

See: #565
This commit is contained in:
Christoph Strobl
2024-03-07 10:34:33 +01:00
parent 4a0dc8b647
commit 5406c0b40f
10 changed files with 503 additions and 2 deletions

View File

@@ -16,6 +16,7 @@
package org.springframework.data.keyvalue.core;
import java.util.Collection;
import java.util.Comparator;
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
import org.springframework.lang.Nullable;
@@ -35,7 +36,17 @@ public abstract class AbstractKeyValueAdapter implements KeyValueAdapter {
* Creates new {@link AbstractKeyValueAdapter} with using the default query engine.
*/
protected AbstractKeyValueAdapter() {
this(null);
this((QueryEngine<? extends KeyValueAdapter, ?, ?>) null);
}
/**
* Creates new {@link AbstractKeyValueAdapter} with using the default query engine and provided comparator for sorting.
*
* @param sortAccessor must not be {@literal null}.
* @since 3.1.10
*/
protected AbstractKeyValueAdapter(SortAccessor<Comparator<?>> sortAccessor) {
this(new SpelQueryEngine(sortAccessor));
}
/**

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.keyvalue.core;
import java.util.Comparator;
import java.util.Optional;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.NullHandling;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
import org.springframework.lang.Nullable;
/**
* @author Christoph Strobl
* @since 3.1.10
*/
public class PathSortAccessor implements SortAccessor<Comparator<?>> {
@Nullable
@Override
public Comparator<?> resolve(KeyValueQuery<?> query) {
if (query.getSort().isUnsorted()) {
return null;
}
Optional<Comparator<?>> comparator = Optional.empty();
for (Order order : query.getSort()) {
PropertyPathComparator<Object> pathSort = new PropertyPathComparator<>(order.getProperty());
if (Direction.DESC.equals(order.getDirection())) {
pathSort.desc();
if (!NullHandling.NATIVE.equals(order.getNullHandling())) {
pathSort = NullHandling.NULLS_FIRST.equals(order.getNullHandling()) ? pathSort.nullsFirst()
: pathSort.nullsLast();
}
}
if (!comparator.isPresent()) {
comparator = Optional.of(pathSort);
} else {
PropertyPathComparator<Object> pathSortToUse = pathSort;
comparator = comparator.map(it -> it.thenComparing(pathSortToUse));
}
}
return comparator.orElseThrow(
() -> new IllegalStateException("No sort definitions have been added to this CompoundComparator to compare"));
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.keyvalue.core;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.util.Lazy;
import org.springframework.util.comparator.NullSafeComparator;
/**
* @author Christoph Strobl
* @since 3.1.10
*/
public class PropertyPathComparator<T> implements Comparator<T> {
private final String path;
private boolean asc = true;
private boolean nullsFirst = true;
private final Map<Class<?>, PropertyPath> pathCache = new HashMap<>(2);
private Lazy<Comparator<Object>> comparator = Lazy
.of(() -> new NullSafeComparator(Comparator.naturalOrder(), this.nullsFirst));
public PropertyPathComparator(String path) {
this.path = path;
}
@Override
public int compare(T o1, T o2) {
if (o1 == null && o2 == null) {
return 0;
}
if (o1 == null) {
return nullsFirst ? 1 : -1;
}
if (o2 == null) {
return nullsFirst ? 1 : -1;
}
PropertyPath propertyPath = pathCache.computeIfAbsent(o1.getClass(), it -> PropertyPath.from(path, it));
Object value1 = new SimplePropertyPathAccessor<>(o1).getValue(propertyPath);
Object value2 = new SimplePropertyPathAccessor<>(o2).getValue(propertyPath);
return comparator.get().compare(value1, value2) * (asc ? 1 : -1);
}
/**
* Sort {@literal ascending}.
*
* @return
*/
public PropertyPathComparator<T> asc() {
this.asc = true;
return this;
}
/**
* Sort {@literal descending}.
*
* @return
*/
public PropertyPathComparator<T> desc() {
this.asc = false;
return this;
}
/**
* Sort {@literal null} values first.
*
* @return
*/
public PropertyPathComparator<T> nullsFirst() {
this.nullsFirst = true;
return this;
}
/**
* Sort {@literal null} values last.
*
* @return
*/
public PropertyPathComparator<T> nullsLast() {
this.nullsFirst = false;
return this;
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.keyvalue.core;
import org.springframework.beans.BeanWrapper;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper;
/**
* @author Christoph Strobl
* @since 3.1.10
*/
class SimplePropertyPathAccessor<T> {
private final Object root;
public SimplePropertyPathAccessor(Object source) {
this.root = source;
}
Object getValue(PropertyPath path) {
Object currentValue = root;
for (PropertyPath current : path) {
currentValue = wrap(currentValue).getPropertyValue(current.getSegment());
if (currentValue == null) {
break;
}
}
return currentValue;
}
BeanWrapper wrap(Object o) {
return new DirectFieldAccessFallbackBeanWrapper(o);
}
}

View File

@@ -44,7 +44,16 @@ class SpelQueryEngine extends QueryEngine<KeyValueAdapter, SpelCriteria, Compara
* Creates a new {@link SpelQueryEngine}.
*/
public SpelQueryEngine() {
super(new SpelCriteriaAccessor(PARSER), new SpelSortAccessor(PARSER));
this(new SpelSortAccessor(PARSER));
}
/**
* Creates a new query engine using provided {@link SortAccessor accessor} for sorting results.
*
* @since 3.1.10
*/
public SpelQueryEngine(SortAccessor<Comparator<?>> sortAccessor) {
super(new SpelCriteriaAccessor(PARSER), sortAccessor);
}
@Override

View File

@@ -16,6 +16,7 @@
package org.springframework.data.map;
import java.util.Collection;
import java.util.Comparator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
@@ -25,6 +26,7 @@ import org.springframework.data.keyvalue.core.AbstractKeyValueAdapter;
import org.springframework.data.keyvalue.core.ForwardingCloseableIterator;
import org.springframework.data.keyvalue.core.KeyValueAdapter;
import org.springframework.data.keyvalue.core.QueryEngine;
import org.springframework.data.keyvalue.core.SortAccessor;
import org.springframework.data.util.CloseableIterator;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -69,6 +71,23 @@ public class MapKeyValueAdapter extends AbstractKeyValueAdapter {
this(CollectionFactory.createMap(mapType, 100), mapType, null);
}
/**
* Creates a new {@link MapKeyValueAdapter} using the given {@link Map} as backing store.
*
* @param mapType must not be {@literal null}.
* @param sortAccessor accessor granting access to sorting implementation
* @since 3.1.10
*/
public MapKeyValueAdapter(Class<? extends Map> mapType, SortAccessor<Comparator<?>> sortAccessor) {
super(sortAccessor);
Assert.notNull(mapType, "Store must not be null");
this.store = CollectionFactory.createMap(mapType, 100);
this.keySpaceMapType = (Class<? extends Map>) ClassUtils.getUserClass(store);
}
/**
* Creates a new {@link MapKeyValueAdapter} using the given {@link Map} as backing store and query engine.
*

View File

@@ -30,6 +30,7 @@ import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Import;
import org.springframework.data.keyvalue.core.KeyValueOperations;
import org.springframework.data.keyvalue.core.KeyValueTemplate;
import org.springframework.data.keyvalue.core.SortAccessor;
import org.springframework.data.keyvalue.repository.config.QueryCreatorType;
import org.springframework.data.keyvalue.repository.query.CachingKeyValuePartTreeQuery;
import org.springframework.data.keyvalue.repository.query.SpelQueryCreator;
@@ -143,4 +144,12 @@ public @interface EnableMapRepositories {
*/
@SuppressWarnings("rawtypes")
Class<? extends Map> mapType() default ConcurrentHashMap.class;
/**
* Configures the {@link SortAccessor accessor} for sorting results.
*
* @return {@link SortAccessor} to indicate usage of default implementation.
* @since 3.1.10
*/
Class<? extends SortAccessor> sortAccessor() default SortAccessor.class;
}

View File

@@ -17,15 +17,18 @@ package org.springframework.data.map.repository.config;
import java.util.Map;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.config.ParsingUtils;
import org.springframework.data.keyvalue.core.KeyValueTemplate;
import org.springframework.data.keyvalue.core.SortAccessor;
import org.springframework.data.keyvalue.repository.config.KeyValueRepositoryConfigurationExtension;
import org.springframework.data.map.MapKeyValueAdapter;
import org.springframework.data.repository.config.RepositoryConfigurationSource;
import org.springframework.lang.Nullable;
/**
* @author Christoph Strobl
@@ -54,6 +57,11 @@ public class MapRepositoryConfigurationExtension extends KeyValueRepositoryConfi
BeanDefinitionBuilder adapterBuilder = BeanDefinitionBuilder.rootBeanDefinition(MapKeyValueAdapter.class);
adapterBuilder.addConstructorArgValue(getMapTypeToUse(configurationSource));
SortAccessor<?> sortAccessor = getSortAccessor(configurationSource);
if(sortAccessor != null) {
adapterBuilder.addConstructorArgValue(sortAccessor);
}
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(KeyValueTemplate.class);
builder
.addConstructorArgValue(ParsingUtils.getSourceBeanDefinition(adapterBuilder, configurationSource.getSource()));
@@ -68,4 +76,17 @@ public class MapRepositoryConfigurationExtension extends KeyValueRepositoryConfi
return (Class<? extends Map>) ((AnnotationMetadata) source.getSource()).getAnnotationAttributes(
EnableMapRepositories.class.getName()).get("mapType");
}
@Nullable
private static SortAccessor<?> getSortAccessor(RepositoryConfigurationSource source) {
Class<? extends SortAccessor<?>> sortAccessorType = (Class<? extends SortAccessor<?>>) ((AnnotationMetadata) source.getSource()).getAnnotationAttributes(
EnableMapRepositories.class.getName()).get("sortAccessor");
if(sortAccessorType != null && !sortAccessorType.isInterface()) {
return BeanUtils.instantiateClass(sortAccessorType);
}
return null;
}
}