From 9c9ef80c899e71f3195313326a6ed084a9b19191 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 31 Aug 2021 15:26:50 +0200 Subject: [PATCH] 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 { Stream streamAllByLastName(String lastName); PersonProjection findByLastName(String lastname); T findByLastName(String lastname, Class projection); } Closes #275 --- src/main/asciidoc/new-features.adoc | 5 + .../asciidoc/reference/ldap-repositories.adoc | 1 + .../query/AbstractLdapRepositoryQuery.java | 47 ++++- .../query/AnnotatedLdapRepositoryQuery.java | 18 +- .../query/LdapParameterAccessor.java | 34 ++++ .../LdapParametersParameterAccessor.java | 59 ++++++ .../repository/query/LdapQueryCreator.java | 17 +- .../repository/query/LdapQueryExecution.java | 172 ++++++++++++++++++ .../query/PartTreeLdapRepositoryQuery.java | 30 ++- .../support/LdapRepositoryFactory.java | 21 ++- .../repository/LdapRepositoryUnitTests.java | 143 +++++++++++++++ .../PartTreeLdapRepositoryQueryTests.java | 7 +- 12 files changed, 518 insertions(+), 36 deletions(-) create mode 100644 src/main/java/org/springframework/data/ldap/repository/query/LdapParameterAccessor.java create mode 100644 src/main/java/org/springframework/data/ldap/repository/query/LdapParametersParameterAccessor.java create mode 100644 src/main/java/org/springframework/data/ldap/repository/query/LdapQueryExecution.java create mode 100644 src/test/java/org/springframework/data/ldap/repository/LdapRepositoryUnitTests.java diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 89f8e4a..aba6b51 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -1,6 +1,11 @@ [[new-features]] = New and Noteworthy +[[new-features.2.6]] +== What's New in Spring Data LDAP 2.6 +* <> from the underlying result `List`. +* <>. + [[new-features.2.1]] == What's New in Spring Data LDAP 2.1 * CDI extension to create LDAP repositories within a CDI container. diff --git a/src/main/asciidoc/reference/ldap-repositories.adoc b/src/main/asciidoc/reference/ldap-repositories.adoc index 034b92a..e146589 100644 --- a/src/main/asciidoc/reference/ldap-repositories.adoc +++ b/src/main/asciidoc/reference/ldap-repositories.adoc @@ -224,6 +224,7 @@ The following table provides samples of the keywords that you can use with query |=== +include::../{spring-data-commons-docs}/repository-projections.adoc[leveloffset=+2] === QueryDSL Support Basic QueryDSL support is included in Spring LDAP. This support includes the following: diff --git a/src/main/java/org/springframework/data/ldap/repository/query/AbstractLdapRepositoryQuery.java b/src/main/java/org/springframework/data/ldap/repository/query/AbstractLdapRepositoryQuery.java index b6caeb7..cf3ca63 100644 --- a/src/main/java/org/springframework/data/ldap/repository/query/AbstractLdapRepositoryQuery.java +++ b/src/main/java/org/springframework/data/ldap/repository/query/AbstractLdapRepositoryQuery.java @@ -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 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 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 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 diff --git a/src/main/java/org/springframework/data/ldap/repository/query/AnnotatedLdapRepositoryQuery.java b/src/main/java/org/springframework/data/ldap/repository/query/AnnotatedLdapRepositoryQuery.java index c8bf9ab..831a283 100644 --- a/src/main/java/org/springframework/data/ldap/repository/query/AnnotatedLdapRepositoryQuery.java +++ b/src/main/java/org/springframework/data/ldap/repository/query/AnnotatedLdapRepositoryQuery.java @@ -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 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()); } } diff --git a/src/main/java/org/springframework/data/ldap/repository/query/LdapParameterAccessor.java b/src/main/java/org/springframework/data/ldap/repository/query/LdapParameterAccessor.java new file mode 100644 index 0000000..0d97d54 --- /dev/null +++ b/src/main/java/org/springframework/data/ldap/repository/query/LdapParameterAccessor.java @@ -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(); +} diff --git a/src/main/java/org/springframework/data/ldap/repository/query/LdapParametersParameterAccessor.java b/src/main/java/org/springframework/data/ldap/repository/query/LdapParametersParameterAccessor.java new file mode 100644 index 0000000..348fcaf --- /dev/null +++ b/src/main/java/org/springframework/data/ldap/repository/query/LdapParametersParameterAccessor.java @@ -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; + } + +} diff --git a/src/main/java/org/springframework/data/ldap/repository/query/LdapQueryCreator.java b/src/main/java/org/springframework/data/ldap/repository/query/LdapQueryCreator.java index 42049d5..f6a2339 100644 --- a/src/main/java/org/springframework/data/ldap/repository/query/LdapQueryCreator.java +++ b/src/main/java/org/springframework/data/ldap/repository/query/LdapQueryCreator.java @@ -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 entityType; private final ObjectDirectoryMapper mapper; + private final List 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 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 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 resultProcessing; + + StreamExecution(LdapOperations operations, Class entityType, Converter 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 converter; + + public ResultProcessingExecution(LdapQueryExecution delegate, Converter 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 { + + private final ResultProcessor processor; + private final MappingContext, ? extends PersistentProperty> mappingContext; + private final EntityInstantiators instantiators; + + public ResultProcessingConverter(ResultProcessor processor, + MappingContext, ? 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 converter = new DtoInstantiatingConverter(returnedType.getReturnedType(), + mappingContext, instantiators); + + return processor.processResult(source, converter); + } + } +} diff --git a/src/main/java/org/springframework/data/ldap/repository/query/PartTreeLdapRepositoryQuery.java b/src/main/java/org/springframework/data/ldap/repository/query/PartTreeLdapRepositoryQuery.java index acbd9e1..7dec2f9 100644 --- a/src/main/java/org/springframework/data/ldap/repository/query/PartTreeLdapRepositoryQuery.java +++ b/src/main/java/org/springframework/data/ldap/repository/query/PartTreeLdapRepositoryQuery.java @@ -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 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 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(); } } diff --git a/src/main/java/org/springframework/data/ldap/repository/support/LdapRepositoryFactory.java b/src/main/java/org/springframework/data/ldap/repository/support/LdapRepositoryFactory.java index 13230a2..00855ac 100644 --- a/src/main/java/org/springframework/data/ldap/repository/support/LdapRepositoryFactory.java +++ b/src/main/java/org/springframework/data/ldap/repository/support/LdapRepositoryFactory.java @@ -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 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 PersistentProperty> mappingContext; + + public LdapQueryLookupStrategy(LdapOperations ldapOperations, EntityInstantiators instantiators, + MappingContext, ? 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); } } } diff --git a/src/test/java/org/springframework/data/ldap/repository/LdapRepositoryUnitTests.java b/src/test/java/org/springframework/data/ldap/repository/LdapRepositoryUnitTests.java new file mode 100644 index 0000000..e398819 --- /dev/null +++ b/src/test/java/org/springframework/data/ldap/repository/LdapRepositoryUnitTests.java @@ -0,0 +1,143 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import lombok.Data; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.naming.ldap.LdapName; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoSettings; + +import org.springframework.data.ldap.repository.support.LdapRepositoryFactory; +import org.springframework.data.ldap.repository.support.UnitTestPerson; +import org.springframework.ldap.core.LdapOperations; +import org.springframework.ldap.odm.core.impl.DefaultObjectDirectoryMapper; +import org.springframework.ldap.query.LdapQuery; + +/** + * Unit tests for {@link LdapRepository}. + * + * @author Mark Paluch + */ +@MockitoSettings +class LdapRepositoryUnitTests { + + @Mock LdapOperations ldapOperations; + + UnitTestPerson walter, hank; + + PersonRepository repository; + + @BeforeEach + void before() throws Exception { + + when(ldapOperations.getObjectDirectoryMapper()).thenReturn(new DefaultObjectDirectoryMapper()); + + walter = new UnitTestPerson(new LdapName("cn=walter"), "Walter", "White", Collections.emptyList(), "US", + "Heisenberg", "000"); + hank = new UnitTestPerson(new LdapName("cn=hank"), "Hank", "Schrader", Collections.emptyList(), "US", "DEA", "000"); + + repository = new LdapRepositoryFactory(ldapOperations).getRepository(PersonRepository.class); + } + + @Test + void shouldReturnInterfaceProjection() { + + when(ldapOperations.findOne(any(LdapQuery.class), eq(UnitTestPerson.class))).thenReturn(walter); + + PersonProjection walter = repository.findByLastName("White"); + + assertThat(walter).isNotNull(); + assertThat(walter.getLastName()).isEqualTo("White"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(LdapQuery.class); + + verify(ldapOperations).findOne(captor.capture(), any()); + + LdapQuery query = captor.getValue(); + assertThat(query.attributes()).containsOnly("lastName"); + } + + @Test + void shouldReturnDynamicDtoProjection() { + + when(ldapOperations.findOne(any(LdapQuery.class), eq(UnitTestPerson.class))).thenReturn(walter); + + PersonDto walter = repository.findByLastName("White", PersonDto.class); + + assertThat(walter).isNotNull(); + assertThat(walter.getLastName()).isEqualTo("White"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(LdapQuery.class); + + verify(ldapOperations).findOne(captor.capture(), any()); + + LdapQuery query = captor.getValue(); + assertThat(query.attributes()).isNullOrEmpty(); + } + + @Test + void shouldReturnInterfaceProjectionAsStream() { + + when(ldapOperations.find(any(LdapQuery.class), eq(UnitTestPerson.class))) + .thenReturn(Collections.singletonList(walter)); + + Stream walter = repository.streamAllByLastName("White"); + + List list = walter.collect(Collectors.toList()); + assertThat(list).hasSize(1).hasOnlyElementsOfType(PersonProjection.class); + + ArgumentCaptor captor = ArgumentCaptor.forClass(LdapQuery.class); + + verify(ldapOperations).find(captor.capture(), any()); + + LdapQuery query = captor.getValue(); + assertThat(query.attributes()).containsOnly("lastName"); + } + + interface PersonRepository extends LdapRepository { + + Stream streamAllByLastName(String lastName); + + PersonProjection findByLastName(String lastname); + + T findByLastName(String lastname, Class projection); + + } + + interface PersonProjection { + String getLastName(); + } + + @Data + static class PersonDto { + + String lastName; + } +} diff --git a/src/test/java/org/springframework/data/ldap/repository/query/PartTreeLdapRepositoryQueryTests.java b/src/test/java/org/springframework/data/ldap/repository/query/PartTreeLdapRepositoryQueryTests.java index b1684fc..e2c8b4a 100644 --- a/src/test/java/org/springframework/data/ldap/repository/query/PartTreeLdapRepositoryQueryTests.java +++ b/src/test/java/org/springframework/data/ldap/repository/query/PartTreeLdapRepositoryQueryTests.java @@ -22,8 +22,10 @@ import java.lang.reflect.Method; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.ldap.core.mapping.LdapMappingContext; import org.springframework.data.ldap.repository.support.BaseUnitTestPerson; import org.springframework.data.ldap.repository.support.UnitTestPerson; +import org.springframework.data.mapping.model.EntityInstantiators; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; @@ -135,9 +137,10 @@ class PartTreeLdapRepositoryQueryTests { Object... expectedParams) { LdapQueryMethod queryMethod = new LdapQueryMethod(targetMethod, repositoryMetadata, factory); - PartTreeLdapRepositoryQuery tested = new PartTreeLdapRepositoryQuery(queryMethod, entityClass, ldapTemplate); + PartTreeLdapRepositoryQuery tested = new PartTreeLdapRepositoryQuery(queryMethod, entityClass, ldapTemplate, + new LdapMappingContext(), new EntityInstantiators()); - LdapQuery query = tested.createQuery(expectedParams); + LdapQuery query = tested.createQuery(new LdapParametersParameterAccessor(queryMethod, expectedParams)); String base = query.base().toString(); assertThat(base).isEqualTo(expectedBase); assertThat(query.filter().encode()).isEqualTo(expectedFilter);