Add support for projection and Stream queries.

We now support projections on repository query methods and query methods returning a Stream.

interface PersonRepository extends LdapRepository<Person> {

	Stream<PersonProjection> streamAllByLastName(String lastName);

	PersonProjection findByLastName(String lastname);

	<T> T findByLastName(String lastname, Class<T> projection);

}

Closes #275
This commit is contained in:
Mark Paluch
2021-08-31 15:26:50 +02:00
parent 79fe9244f4
commit 9c9ef80c89
12 changed files with 518 additions and 36 deletions

View File

@@ -15,10 +15,17 @@
*/
package org.springframework.data.ldap.repository.query;
import org.springframework.dao.EmptyResultDataAccessException;
import static org.springframework.data.ldap.repository.query.LdapQueryExecution.*;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.ldap.repository.Query;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.EntityInstantiators;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.ldap.core.LdapOperations;
import org.springframework.ldap.query.LdapQuery;
import org.springframework.util.Assert;
@@ -34,6 +41,8 @@ public abstract class AbstractLdapRepositoryQuery implements RepositoryQuery {
private final LdapQueryMethod queryMethod;
private final Class<?> entityType;
private final LdapOperations ldapOperations;
private final MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> mappingContext;
private final EntityInstantiators instantiators;
/**
* Creates a new {@link AbstractLdapRepositoryQuery} instance given {@link LdapQuery}, {@link Class} and
@@ -42,8 +51,12 @@ public abstract class AbstractLdapRepositoryQuery implements RepositoryQuery {
* @param queryMethod must not be {@literal null}.
* @param entityType must not be {@literal null}.
* @param ldapOperations must not be {@literal null}.
* @param mappingContext must not be {@literal null}.
* @param instantiators must not be {@literal null}.
*/
public AbstractLdapRepositoryQuery(LdapQueryMethod queryMethod, Class<?> entityType, LdapOperations ldapOperations) {
public AbstractLdapRepositoryQuery(LdapQueryMethod queryMethod, Class<?> entityType, LdapOperations ldapOperations,
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> mappingContext,
EntityInstantiators instantiators) {
Assert.notNull(queryMethod, "LdapQueryMethod must not be null!");
Assert.notNull(entityType, "Entity type must not be null!");
@@ -52,6 +65,8 @@ public abstract class AbstractLdapRepositoryQuery implements RepositoryQuery {
this.queryMethod = queryMethod;
this.entityType = entityType;
this.ldapOperations = ldapOperations;
this.mappingContext = mappingContext;
this.instantiators = instantiators;
}
/* (non-Javadoc)
@@ -61,16 +76,28 @@ public abstract class AbstractLdapRepositoryQuery implements RepositoryQuery {
@SuppressWarnings("ConstantConditions")
public final Object execute(Object[] parameters) {
LdapQuery query = createQuery(parameters);
LdapParametersParameterAccessor parameterAccessor = new LdapParametersParameterAccessor(queryMethod, parameters);
LdapQuery query = createQuery(parameterAccessor);
ResultProcessor processor = queryMethod.getResultProcessor().withDynamicProjection(parameterAccessor);
Class<?> typeToRead = processor.getReturnedType().getDomainType();
ResultProcessingConverter converter = new ResultProcessingConverter(processor, mappingContext, instantiators);
ResultProcessingExecution execution = new ResultProcessingExecution(
getLdapQueryExecutionToWrap(typeToRead, converter), converter);
return execution.execute(query);
}
private LdapQueryExecution getLdapQueryExecutionToWrap(Class<?> typeToRead,
Converter<Object, Object> resultProcessing) {
if (queryMethod.isCollectionQuery()) {
return ldapOperations.find(query, entityType);
return new CollectionExecution(ldapOperations, typeToRead);
} else if (queryMethod.isStreamQuery()) {
return new StreamExecution(ldapOperations, typeToRead, resultProcessing);
} else {
try {
return ldapOperations.findOne(query, entityType);
} catch (EmptyResultDataAccessException e) {
return null;
}
return new FindOneExecution(ldapOperations, typeToRead);
}
}
@@ -80,7 +107,7 @@ public abstract class AbstractLdapRepositoryQuery implements RepositoryQuery {
* @param parameters must not be {@literal null}.
* @return
*/
protected abstract LdapQuery createQuery(Object[] parameters);
protected abstract LdapQuery createQuery(LdapParameterAccessor parameters);
/**
* @return

View File

@@ -18,6 +18,10 @@ package org.springframework.data.ldap.repository.query;
import static org.springframework.ldap.query.LdapQueryBuilder.*;
import org.springframework.data.ldap.repository.Query;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.EntityInstantiators;
import org.springframework.ldap.core.LdapOperations;
import org.springframework.ldap.query.LdapQuery;
import org.springframework.util.Assert;
@@ -38,10 +42,14 @@ public class AnnotatedLdapRepositoryQuery extends AbstractLdapRepositoryQuery {
* @param queryMethod the QueryMethod.
* @param entityType the managed class.
* @param ldapOperations the LdapOperations instance to use.
* @param mappingContext must not be {@literal null}.
* @param instantiators must not be {@literal null}.
*/
public AnnotatedLdapRepositoryQuery(LdapQueryMethod queryMethod, Class<?> entityType, LdapOperations ldapOperations) {
public AnnotatedLdapRepositoryQuery(LdapQueryMethod queryMethod, Class<?> entityType, LdapOperations ldapOperations,
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> mappingContext,
EntityInstantiators instantiators) {
super(queryMethod, entityType, ldapOperations);
super(queryMethod, entityType, ldapOperations, mappingContext, instantiators);
Assert.notNull(queryMethod.getQueryAnnotation(), "Annotation must be present");
Assert.hasLength(queryMethod.getQueryAnnotation().value(), "Query filter must be specified");
@@ -50,15 +58,15 @@ public class AnnotatedLdapRepositoryQuery extends AbstractLdapRepositoryQuery {
}
/* (non-Javadoc)
* @see org.springframework.data.ldap.repository.query.AbstractLdapRepositoryQuery#createQuery(java.lang.Object[])
* @see org.springframework.data.ldap.repository.query.AbstractLdapRepositoryQuery#createQuery(org.springframework.data.ldap.repository.query.LdapParameterAccessor)
*/
@Override
protected LdapQuery createQuery(Object[] parameters) {
protected LdapQuery createQuery(LdapParameterAccessor parameters) {
return query().base(queryAnnotation.base()) //
.searchScope(queryAnnotation.searchScope()) //
.countLimit(queryAnnotation.countLimit()) //
.timeLimit(queryAnnotation.timeLimit()) //
.filter(queryAnnotation.value(), parameters);
.filter(queryAnnotation.value(), parameters.getBindableParameterValues());
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2021 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
*
* http://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.ldap.repository.query;
import org.springframework.data.repository.query.ParameterAccessor;
/**
* LDAP-specific {@link ParameterAccessor} exposing all bindable parameters.
*
* @author Mark Paluch
* @since 2.6
*/
interface LdapParameterAccessor extends ParameterAccessor {
/**
* Returns the bindable parameter values of the underlying query method.
*
* @return
*/
Object[] getBindableParameterValues();
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2021 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
*
* http://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.ldap.repository.query;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.data.repository.query.QueryMethod;
/**
* LDAP-specific {@link ParametersParameterAccessor}.
*
* @author Mark Paluch
* @since 2.6
*/
class LdapParametersParameterAccessor extends ParametersParameterAccessor implements LdapParameterAccessor {
/**
* Creates a new {@link LdapParametersParameterAccessor}.
*
* @param method must not be {@literal null}.
* @param values must not be {@literal null}.
*/
public LdapParametersParameterAccessor(QueryMethod method, Object[] values) {
super(method.getParameters(), values);
}
@Override
public Object[] getBindableParameterValues() {
Parameters<?, ?> bindableParameters = getParameters().getBindableParameters();
int count = bindableParameters.getNumberOfParameters();
if (count == 0) {
return new Object[0];
}
Object[] values = new Object[count];
for (int i = 0; i < count; i++) {
values[i] = getBindableValue(i);
}
return values;
}
}

View File

@@ -18,12 +18,11 @@ package org.springframework.data.ldap.repository.query;
import static org.springframework.ldap.query.LdapQueryBuilder.*;
import java.util.Iterator;
import java.util.List;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
import org.springframework.data.repository.query.parser.Part;
import org.springframework.data.repository.query.parser.PartTree;
@@ -45,26 +44,28 @@ class LdapQueryCreator extends AbstractQueryCreator<LdapQuery, ContainerCriteria
private final Class<?> entityType;
private final ObjectDirectoryMapper mapper;
private final List<String> inputProperties;
/**
* Constructs a new {@link LdapQueryCreator}.
*
* @param tree must not be {@literal null}.
* @param parameters must not be {@literal null}.
* @param entityType must not be {@literal null}.
* @param mapper must not be {@literal null}.
* @param values must not be {@literal null}.
* @param inputProperties must not be {@literal null}.
*/
LdapQueryCreator(PartTree tree, Parameters<?, ?> parameters, Class<?> entityType, ObjectDirectoryMapper mapper,
Object[] values) {
LdapQueryCreator(PartTree tree, Class<?> entityType, ObjectDirectoryMapper mapper,
LdapParameterAccessor parameterAccessor, List<String> inputProperties) {
super(tree, new ParametersParameterAccessor(parameters, values));
super(tree, parameterAccessor);
Assert.notNull(entityType, "Entity type must not be null!");
Assert.notNull(mapper, "ObjectDirectoryMapper must not be null!");
this.entityType = entityType;
this.mapper = mapper;
this.inputProperties = inputProperties;
}
/* (non-Javadoc)
@@ -81,6 +82,10 @@ class LdapQueryCreator extends AbstractQueryCreator<LdapQuery, ContainerCriteria
query = query.base(entry.base());
}
if (!inputProperties.isEmpty()) {
query.attributes(inputProperties.toArray(new String[0]));
}
ConditionCriteria criteria = query.where(getAttribute(part));
return appendCondition(part, iterator, criteria);

View File

@@ -0,0 +1,172 @@
/*
* Copyright 2021 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
*
* http://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.ldap.repository.query;
import org.springframework.core.convert.converter.Converter;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.EntityInstantiators;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.ldap.core.LdapOperations;
import org.springframework.ldap.query.LdapQuery;
import org.springframework.util.ClassUtils;
/**
* Set of classes to contain query execution strategies. Depending (mostly) on the return type of a
* {@link org.springframework.data.repository.query.QueryMethod} a {@link AbstractLdapRepositoryQuery} can be executed
* in various flavors.
*
* @author Mark Paluch
* @since 2.6
*/
@FunctionalInterface
interface LdapQueryExecution {
Object execute(LdapQuery query);
/**
* {@link LdapQueryExecution} returning a single object.
*
* @author Mark Paluch
*/
final class FindOneExecution implements LdapQueryExecution {
private final LdapOperations operations;
private final Class<?> entityType;
FindOneExecution(LdapOperations operations, Class<?> entityType) {
this.operations = operations;
this.entityType = entityType;
}
@Override
public Object execute(LdapQuery query) {
try {
return operations.findOne(query, entityType);
} catch (EmptyResultDataAccessException e) {
return null;
}
}
}
/**
* {@link LdapQueryExecution} returning a list of objects.
*
* @author Mark Paluch
*/
final class CollectionExecution implements LdapQueryExecution {
private final LdapOperations operations;
private final Class<?> entityType;
CollectionExecution(LdapOperations operations, Class<?> entityType) {
this.operations = operations;
this.entityType = entityType;
}
@Override
public Object execute(LdapQuery query) {
return operations.find(query, entityType);
}
}
/**
* {@link LdapQueryExecution} for a Stream.
*
* @author Mark Paluch
*/
final class StreamExecution implements LdapQueryExecution {
private final LdapOperations operations;
private final Class<?> entityType;
private final Converter<Object, Object> resultProcessing;
StreamExecution(LdapOperations operations, Class<?> entityType, Converter<Object, Object> resultProcessing) {
this.operations = operations;
this.entityType = entityType;
this.resultProcessing = resultProcessing;
}
@Override
public Object execute(LdapQuery query) {
return operations.find(query, entityType).stream().map(resultProcessing::convert);
}
}
/**
* An {@link LdapQueryExecution} that wraps the results of the given delegate with the given result processing.
*/
final class ResultProcessingExecution implements LdapQueryExecution {
private final LdapQueryExecution delegate;
private final Converter<Object, Object> converter;
public ResultProcessingExecution(LdapQueryExecution delegate, Converter<Object, Object> converter) {
this.delegate = delegate;
this.converter = converter;
}
@Override
public Object execute(LdapQuery query) {
return converter.convert(delegate.execute(query));
}
}
/**
* A {@link Converter} to post-process all source objects using the given {@link ResultProcessor}.
*
* @author Mark Paluch
*/
final class ResultProcessingConverter implements Converter<Object, Object> {
private final ResultProcessor processor;
private final MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> mappingContext;
private final EntityInstantiators instantiators;
public ResultProcessingConverter(ResultProcessor processor,
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> mappingContext,
EntityInstantiators instantiators) {
this.processor = processor;
this.mappingContext = mappingContext;
this.instantiators = instantiators;
}
/*
* (non-Javadoc)
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
*/
@Override
public Object convert(Object source) {
ReturnedType returnedType = processor.getReturnedType();
if (ClassUtils.isPrimitiveOrWrapper(returnedType.getReturnedType())) {
return source;
}
if (source != null && returnedType.isInstance(source)) {
return source;
}
Converter<Object, Object> converter = new DtoInstantiatingConverter(returnedType.getReturnedType(),
mappingContext, instantiators);
return processor.processResult(source, converter);
}
}
}

View File

@@ -15,8 +15,16 @@
*/
package org.springframework.data.ldap.repository.query;
import java.util.Collections;
import java.util.List;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.EntityInstantiators;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.ldap.core.LdapOperations;
import org.springframework.ldap.odm.core.ObjectDirectoryMapper;
@@ -40,10 +48,14 @@ public class PartTreeLdapRepositoryQuery extends AbstractLdapRepositoryQuery {
* @param queryMethod must not be {@literal null}.
* @param entityType must not be {@literal null}.
* @param ldapOperations must not be {@literal null}.
* @param mappingContext must not be {@literal null}.
* @param instantiators must not be {@literal null}.
*/
public PartTreeLdapRepositoryQuery(LdapQueryMethod queryMethod, Class<?> entityType, LdapOperations ldapOperations) {
public PartTreeLdapRepositoryQuery(LdapQueryMethod queryMethod, Class<?> entityType, LdapOperations ldapOperations,
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> mappingContext,
EntityInstantiators instantiators) {
super(queryMethod, entityType, ldapOperations);
super(queryMethod, entityType, ldapOperations, mappingContext, instantiators);
partTree = new PartTree(queryMethod.getName(), entityType);
parameters = queryMethod.getParameters();
@@ -51,13 +63,21 @@ public class PartTreeLdapRepositoryQuery extends AbstractLdapRepositoryQuery {
}
/* (non-Javadoc)
* @see org.springframework.data.ldap.repository.query.AbstractLdapRepositoryQuery#createQuery(java.lang.Object[])
* @see org.springframework.data.ldap.repository.query.AbstractLdapRepositoryQuery#createQuery(org.springframework.data.ldap.repository.query.LdapParameterAccessor)
*/
@Override
protected LdapQuery createQuery(Object[] actualParameters) {
protected LdapQuery createQuery(LdapParameterAccessor parameters) {
List<String> inputProperties = Collections.emptyList();
ReturnedType returnedType = getQueryMethod().getResultProcessor().withDynamicProjection(parameters)
.getReturnedType();
if (returnedType.needsCustomConstruction()) {
inputProperties = returnedType.getInputProperties();
}
org.springframework.data.ldap.repository.query.LdapQueryCreator queryCreator = new LdapQueryCreator(partTree,
this.parameters, getEntityClass(), objectDirectoryMapper, actualParameters);
getEntityClass(), objectDirectoryMapper, parameters, inputProperties);
return queryCreator.createQuery();
}
}

View File

@@ -28,6 +28,7 @@ import org.springframework.data.ldap.repository.query.PartTreeLdapRepositoryQuer
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.EntityInstantiators;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.core.EntityInformation;
@@ -56,6 +57,7 @@ public class LdapRepositoryFactory extends RepositoryFactorySupport {
private final LdapQueryLookupStrategy queryLookupStrategy;
private final LdapOperations ldapOperations;
private final MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> mappingContext;
private final EntityInstantiators instantiators = new EntityInstantiators();
/**
* Creates a new {@link LdapRepositoryFactory}.
@@ -66,9 +68,9 @@ public class LdapRepositoryFactory extends RepositoryFactorySupport {
Assert.notNull(ldapOperations, "LdapOperations must not be null!");
this.queryLookupStrategy = new LdapQueryLookupStrategy(ldapOperations);
this.ldapOperations = ldapOperations;
this.mappingContext = new LdapMappingContext();
this.queryLookupStrategy = new LdapQueryLookupStrategy(ldapOperations, instantiators, mappingContext);
}
/**
@@ -83,7 +85,7 @@ public class LdapRepositoryFactory extends RepositoryFactorySupport {
Assert.notNull(ldapOperations, "LdapOperations must not be null!");
Assert.notNull(mappingContext, "LdapMappingContext must not be null!");
this.queryLookupStrategy = new LdapQueryLookupStrategy(ldapOperations);
this.queryLookupStrategy = new LdapQueryLookupStrategy(ldapOperations, instantiators, mappingContext);
this.ldapOperations = ldapOperations;
this.mappingContext = mappingContext;
}
@@ -168,12 +170,15 @@ public class LdapRepositoryFactory extends RepositoryFactorySupport {
private static final class LdapQueryLookupStrategy implements QueryLookupStrategy {
private final LdapOperations ldapOperations;
private final EntityInstantiators instantiators;
private final MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> mappingContext;
public LdapQueryLookupStrategy(LdapOperations ldapOperations, EntityInstantiators instantiators,
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> mappingContext) {
/**
* @param ldapOperations must not be {@literal null}.
*/
LdapQueryLookupStrategy(LdapOperations ldapOperations) {
this.ldapOperations = ldapOperations;
this.instantiators = instantiators;
this.mappingContext = mappingContext;
}
@Override
@@ -184,9 +189,9 @@ public class LdapRepositoryFactory extends RepositoryFactorySupport {
Class<?> domainType = metadata.getDomainType();
if (queryMethod.hasQueryAnnotation()) {
return new AnnotatedLdapRepositoryQuery(queryMethod, domainType, ldapOperations);
return new AnnotatedLdapRepositoryQuery(queryMethod, domainType, ldapOperations, mappingContext, instantiators);
} else {
return new PartTreeLdapRepositoryQuery(queryMethod, domainType, ldapOperations);
return new PartTreeLdapRepositoryQuery(queryMethod, domainType, ldapOperations, mappingContext, instantiators);
}
}
}