From 1fe292834a5597d0955de8934da9367934a6830b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 31 Aug 2021 14:18:25 +0200 Subject: [PATCH] Add FluentQuery support to QuerydslLdapRepository. Closes #269. --- .../query/DtoInstantiatingConverter.java | 101 ++++++ .../support/LdapRepositoryFactory.java | 26 +- .../support/LdapRepositoryFactoryBean.java | 7 +- .../repository/support/QuerydslLdapQuery.java | 44 ++- .../support/QuerydslLdapRepository.java | 306 +++++++++++++++++- .../support/SimpleLdapRepository.java | 24 ++ .../QuerydslLdapRepositoryUnitTests.java | 223 +++++++++++++ .../repository/support/UnitTestPerson.java | 6 + 8 files changed, 719 insertions(+), 18 deletions(-) create mode 100644 src/main/java/org/springframework/data/ldap/repository/query/DtoInstantiatingConverter.java create mode 100644 src/test/java/org/springframework/data/ldap/repository/support/QuerydslLdapRepositoryUnitTests.java diff --git a/src/main/java/org/springframework/data/ldap/repository/query/DtoInstantiatingConverter.java b/src/main/java/org/springframework/data/ldap/repository/query/DtoInstantiatingConverter.java new file mode 100644 index 0000000..6b57511 --- /dev/null +++ b/src/main/java/org/springframework/data/ldap/repository/query/DtoInstantiatingConverter.java @@ -0,0 +1,101 @@ +/* + * Copyright 2015-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 + * + * 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.ldap.repository.query; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PreferredConstructor; +import org.springframework.data.mapping.PreferredConstructor.Parameter; +import org.springframework.data.mapping.SimplePropertyHandler; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.EntityInstantiator; +import org.springframework.data.mapping.model.EntityInstantiators; +import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.util.Assert; + +/** + * {@link Converter} to instantiate DTOs from fully equipped domain objects. + * + * @author Mark Paluch + */ +public class DtoInstantiatingConverter implements Converter { + + private final Class targetType; + private final MappingContext, ? extends PersistentProperty> context; + private final EntityInstantiator instantiator; + + /** + * Creates a new {@link Converter} to instantiate DTOs. + * + * @param dtoType must not be {@literal null}. + * @param context must not be {@literal null}. + * @param entityInstantiators must not be {@literal null}. + */ + public DtoInstantiatingConverter(Class dtoType, + MappingContext, ? extends PersistentProperty> context, + EntityInstantiators entityInstantiators) { + + Assert.notNull(dtoType, "DTO type must not be null!"); + Assert.notNull(context, "MappingContext must not be null!"); + Assert.notNull(entityInstantiators, "EntityInstantiators must not be null!"); + + this.targetType = dtoType; + this.context = context; + this.instantiator = entityInstantiators.getInstantiatorFor(context.getRequiredPersistentEntity(dtoType)); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public Object convert(Object source) { + + if (targetType.isInterface()) { + return source; + } + + PersistentEntity sourceEntity = context.getRequiredPersistentEntity(source.getClass()); + PersistentPropertyAccessor sourceAccessor = sourceEntity.getPropertyAccessor(source); + PersistentEntity targetEntity = context.getRequiredPersistentEntity(targetType); + PreferredConstructor> constructor = targetEntity.getPersistenceConstructor(); + + Object dto = instantiator.createInstance(targetEntity, new ParameterValueProvider() { + + @Override + public Object getParameterValue(Parameter parameter) { + return sourceAccessor.getProperty(sourceEntity.getPersistentProperty(parameter.getName())); + } + }); + + PersistentPropertyAccessor dtoAccessor = targetEntity.getPropertyAccessor(dto); + + targetEntity.doWithProperties((SimplePropertyHandler) property -> { + + if (constructor.isConstructorParameter(property)) { + return; + } + + dtoAccessor.setProperty(property, + sourceAccessor.getProperty(sourceEntity.getPersistentProperty(property.getName()))); + }); + + return dto; + } +} 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 832ea1b..4a06e7b 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 @@ -20,9 +20,13 @@ import static org.springframework.data.querydsl.QuerydslUtils.*; import java.lang.reflect.Method; import java.util.Optional; +import org.springframework.data.ldap.core.mapping.LdapMappingContext; import org.springframework.data.ldap.repository.query.AnnotatedLdapRepositoryQuery; import org.springframework.data.ldap.repository.query.LdapQueryMethod; import org.springframework.data.ldap.repository.query.PartTreeLdapRepositoryQuery; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.repository.core.EntityInformation; @@ -50,6 +54,7 @@ public class LdapRepositoryFactory extends RepositoryFactorySupport { private final LdapQueryLookupStrategy queryLookupStrategy; private final LdapOperations ldapOperations; + private final MappingContext, ? extends PersistentProperty> mappingContext; /** * Creates a new {@link LdapRepositoryFactory}. @@ -62,6 +67,24 @@ public class LdapRepositoryFactory extends RepositoryFactorySupport { this.queryLookupStrategy = new LdapQueryLookupStrategy(ldapOperations); this.ldapOperations = ldapOperations; + this.mappingContext = new LdapMappingContext(); + } + + /** + * Creates a new {@link LdapRepositoryFactory}. + * + * @param ldapOperations must not be {@literal null}. + * @param mappingContext must not be {@literal null}. + */ + LdapRepositoryFactory(LdapOperations ldapOperations, + MappingContext, ? extends PersistentProperty> mappingContext) { + + Assert.notNull(ldapOperations, "LdapOperations must not be null!"); + Assert.notNull(mappingContext, "LdapMappingContext must not be null!"); + + this.queryLookupStrategy = new LdapQueryLookupStrategy(ldapOperations); + this.ldapOperations = ldapOperations; + this.mappingContext = mappingContext; } /* (non-Javadoc) @@ -92,7 +115,8 @@ public class LdapRepositoryFactory extends RepositoryFactorySupport { */ @Override protected Object getTargetRepository(RepositoryInformation information) { - return getTargetRepositoryViaReflection(information, ldapOperations, ldapOperations.getObjectDirectoryMapper(), + return getTargetRepositoryViaReflection(information, ldapOperations, mappingContext, + ldapOperations.getObjectDirectoryMapper(), information.getDomainType()); } diff --git a/src/main/java/org/springframework/data/ldap/repository/support/LdapRepositoryFactoryBean.java b/src/main/java/org/springframework/data/ldap/repository/support/LdapRepositoryFactoryBean.java index 7b4a938..b422b40 100644 --- a/src/main/java/org/springframework/data/ldap/repository/support/LdapRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/ldap/repository/support/LdapRepositoryFactoryBean.java @@ -18,6 +18,8 @@ package org.springframework.data.ldap.repository.support; import javax.naming.Name; import org.springframework.data.ldap.core.mapping.LdapMappingContext; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; @@ -39,6 +41,7 @@ public class LdapRepositoryFactoryBean, S> private @Nullable LdapOperations ldapOperations; private boolean mappingContextConfigured = false; + private @Nullable MappingContext, ? extends PersistentProperty> mappingContext; /** * Creates a new {@link LdapRepositoryFactoryBean} for the given repository interface. @@ -64,6 +67,7 @@ public class LdapRepositoryFactoryBean, S> public void setMappingContext(MappingContext mappingContext) { super.setMappingContext(mappingContext); + this.mappingContext = mappingContext; this.mappingContextConfigured = true; } @@ -76,7 +80,8 @@ public class LdapRepositoryFactoryBean, S> Assert.state(ldapOperations != null, "LdapOperations must be set"); - return new LdapRepositoryFactory(ldapOperations); + return mappingContext != null ? new LdapRepositoryFactory(ldapOperations, mappingContext) + : new LdapRepositoryFactory(ldapOperations); } /* diff --git a/src/main/java/org/springframework/data/ldap/repository/support/QuerydslLdapQuery.java b/src/main/java/org/springframework/data/ldap/repository/support/QuerydslLdapQuery.java index acde7b1..f4c1ab0 100644 --- a/src/main/java/org/springframework/data/ldap/repository/support/QuerydslLdapQuery.java +++ b/src/main/java/org/springframework/data/ldap/repository/support/QuerydslLdapQuery.java @@ -18,10 +18,13 @@ package org.springframework.data.ldap.repository.support; import static org.springframework.ldap.query.LdapQueryBuilder.*; import java.util.List; +import java.util.function.Consumer; +import org.springframework.ldap.core.ContextMapper; import org.springframework.ldap.core.LdapOperations; import org.springframework.ldap.filter.AbsoluteTrueFilter; import org.springframework.ldap.query.LdapQuery; +import org.springframework.ldap.query.LdapQueryBuilder; import org.springframework.util.Assert; import com.querydsl.core.DefaultQueryMetadata; @@ -40,8 +43,9 @@ import com.querydsl.core.types.Predicate; public class QuerydslLdapQuery implements FilteredClause> { private final LdapOperations ldapOperations; - private final Class entityType; + private final Class entityType; private final LdapSerializer filterGenerator; + private final Consumer queryCustomizer; private QueryMixin> queryMixin = new QueryMixin<>(this, new DefaultQueryMetadata().noValidate()); @@ -52,7 +56,7 @@ public class QuerydslLdapQuery implements FilteredClause * @param entityPath must not be {@literal null}. */ public QuerydslLdapQuery(LdapOperations ldapOperations, EntityPath entityPath) { - this(ldapOperations, entityPath.getType()); + this(ldapOperations, (Class) entityPath.getType()); } /** @@ -61,13 +65,30 @@ public class QuerydslLdapQuery implements FilteredClause * @param ldapOperations must not be {@literal null}. * @param entityType must not be {@literal null}. */ - public QuerydslLdapQuery(LdapOperations ldapOperations, Class entityType) { + public QuerydslLdapQuery(LdapOperations ldapOperations, Class entityType) { + this(ldapOperations, entityType, it -> { + + }); + } + + /** + * Creates a new {@link QuerydslLdapQuery}. + * + * @param ldapOperations must not be {@literal null}. + * @param entityType must not be {@literal null}. + * @param queryCustomizer must not be {@literal null}. + * @since 2.6 + */ + public QuerydslLdapQuery(LdapOperations ldapOperations, Class entityType, + Consumer queryCustomizer) { Assert.notNull(ldapOperations, "LdapOperations must not be null!"); Assert.notNull(entityType, "Type must not be null!"); + Assert.notNull(queryCustomizer, "Query customizer must not be null!"); this.ldapOperations = ldapOperations; this.entityType = entityType; + this.queryCustomizer = queryCustomizer; this.filterGenerator = new LdapSerializer(ldapOperations.getObjectDirectoryMapper(), this.entityType); } @@ -89,10 +110,17 @@ public class QuerydslLdapQuery implements FilteredClause LdapQuery ldapQuery = buildQuery(); if (ldapQuery.filter() instanceof AbsoluteTrueFilter) { - return (List) ldapOperations.findAll(entityType); + return ldapOperations.findAll(entityType); } - return (List) ldapOperations.find(ldapQuery, entityType); + return ldapOperations.find(ldapQuery, entityType); + } + + List search(ContextMapper mapper) { + + LdapQuery ldapQuery = buildQuery(); + + return ldapOperations.search(ldapQuery, mapper); } public K uniqueResult() { @@ -102,6 +130,10 @@ public class QuerydslLdapQuery implements FilteredClause LdapQuery buildQuery() { Predicate where = queryMixin.getMetadata().getWhere(); - return where != null ? query().filter(filterGenerator.handle(where)) : query().filter(new AbsoluteTrueFilter()); + + LdapQueryBuilder builder = query(); + queryCustomizer.accept(builder); + + return where != null ? builder.filter(filterGenerator.handle(where)) : builder.filter(new AbsoluteTrueFilter()); } } diff --git a/src/main/java/org/springframework/data/ldap/repository/support/QuerydslLdapRepository.java b/src/main/java/org/springframework/data/ldap/repository/support/QuerydslLdapRepository.java index decbe59..9de17cd 100644 --- a/src/main/java/org/springframework/data/ldap/repository/support/QuerydslLdapRepository.java +++ b/src/main/java/org/springframework/data/ldap/repository/support/QuerydslLdapRepository.java @@ -15,16 +15,42 @@ */ package org.springframework.data.ldap.repository.support; +import java.beans.FeatureDescriptor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.data.domain.Example; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.ldap.core.mapping.LdapMappingContext; +import org.springframework.data.ldap.repository.query.DtoInstantiatingConverter; +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.ProjectionInformation; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.repository.query.FluentQuery; +import org.springframework.lang.Nullable; import org.springframework.ldap.core.LdapOperations; import org.springframework.ldap.odm.core.ObjectDirectoryMapper; +import org.springframework.ldap.query.LdapQueryBuilder; +import org.springframework.util.Assert; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Predicate; @@ -36,10 +62,14 @@ import com.querydsl.core.types.Predicate; * @author Eddu Melendez * @author Mark Paluch */ -public class QuerydslLdapRepository extends SimpleLdapRepository implements QuerydslPredicateExecutor { +public class QuerydslLdapRepository extends SimpleLdapRepository + implements QuerydslPredicateExecutor, BeanFactoryAware, BeanClassLoaderAware { private final LdapOperations ldapOperations; + private final MappingContext, ? extends PersistentProperty> context; private final Class entityType; + private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); + private final EntityInstantiators entityInstantiators = new EntityInstantiators(); /** * Creates a new {@link QuerydslLdapRepository}. @@ -49,10 +79,42 @@ public class QuerydslLdapRepository extends SimpleLdapRepository implement * @param entityType must not be {@literal null}. */ public QuerydslLdapRepository(LdapOperations ldapOperations, ObjectDirectoryMapper odm, Class entityType) { + super(ldapOperations, odm, entityType); this.ldapOperations = ldapOperations; this.entityType = entityType; + this.context = new LdapMappingContext(); + } + + /** + * Creates a new {@link QuerydslLdapRepository}. + * + * @param ldapOperations must not be {@literal null}. + * @param context must not be {@literal null}. + * @param odm must not be {@literal null}. + * @param entityType must not be {@literal null}. + * @since 2.6 + */ + QuerydslLdapRepository(LdapOperations ldapOperations, + MappingContext, ? extends PersistentProperty> context, + ObjectDirectoryMapper odm, Class entityType) { + + super(ldapOperations, context, odm, entityType); + + this.ldapOperations = ldapOperations; + this.context = context; + this.entityType = entityType; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + projectionFactory.setBeanClassLoader(classLoader); + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + projectionFactory.setBeanFactory(beanFactory); } /* @@ -61,12 +123,7 @@ public class QuerydslLdapRepository extends SimpleLdapRepository implement */ @Override public Optional findOne(Predicate predicate) { - - try { - return Optional.of(queryFor(predicate).uniqueResult()); - } catch (EmptyResultDataAccessException o_O) { - return Optional.empty(); - } + return findBy(predicate, Function.identity()).one(); } /* (non-Javadoc) @@ -99,9 +156,6 @@ public class QuerydslLdapRepository extends SimpleLdapRepository implement throw new UnsupportedOperationException(); } - private QuerydslLdapQuery queryFor(Predicate predicate) { - return new QuerydslLdapQuery<>(ldapOperations, entityType).where(predicate); - } /* (non-Javadoc) * @see org.springframework.data.querydsl.QueryDslPredicateExecutor#findAll(com.querydsl.core.types.OrderSpecifier[]) @@ -125,4 +179,236 @@ public class QuerydslLdapRepository extends SimpleLdapRepository implement public Page findAll(Predicate predicate, Pageable pageable) { throw new UnsupportedOperationException(); } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.QuerydslPredicateExecutor#findBy(com.querydsl.core.types.Predicate, java.util.function.Function) + */ + @Override + @SuppressWarnings("unchecked") + public R findBy(Predicate predicate, + Function, R> queryFunction) { + + Assert.notNull(predicate, "Predicate must not be null!"); + Assert.notNull(queryFunction, "Query function must not be null!"); + + return queryFunction.apply(new FluentQuerydsl<>(predicate, (Class) entityType)); + } + + private QuerydslLdapQuery queryFor(Predicate predicate) { + return queryFor(predicate, it -> { + + }); + } + + private QuerydslLdapQuery queryFor(Predicate predicate, Consumer queryBuilderConsumer) { + return new QuerydslLdapQuery<>(ldapOperations, entityType, queryBuilderConsumer).where(predicate); + } + + /** + * {@link org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery} using {@link Example}. + * + * @author Mark Paluch + * @since 2.6 + */ + class FluentQuerydsl implements FluentQuery.FetchableFluentQuery { + + private final Predicate predicate; + private final Sort sort; + private final Class resultType; + private final List projection; + + FluentQuerydsl(Predicate predicate, Class resultType) { + this(predicate, Sort.unsorted(), resultType, Collections.emptyList()); + } + + FluentQuerydsl(Predicate predicate, Sort sort, Class resultType, List projection) { + this.predicate = predicate; + this.sort = sort; + this.resultType = resultType; + this.projection = projection; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#sortBy(org.springframework.data.domain.Sort) + */ + @Override + public FetchableFluentQuery sortBy(Sort sort) { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#as(java.lang.Class) + */ + @Override + public FetchableFluentQuery as(Class resultType) { + + Assert.notNull(projection, "Projection target type must not be null!"); + + return new FluentQuerydsl<>(predicate, sort, resultType, projection); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#project(java.util.Collection) + */ + @Override + public FetchableFluentQuery project(Collection properties) { + + Assert.notNull(properties, "Projection properties must not be null!"); + + return new FluentQuerydsl<>(predicate, sort, resultType, new ArrayList<>(properties)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#oneValue() + */ + @Nullable + @Override + public R oneValue() { + + List results = findTop(2); + + if (results.isEmpty()) { + return null; + } + + if (results.size() > 1) { + throw new IncorrectResultSizeDataAccessException(1); + } + + T one = results.get(0); + return getConversionFunction(entityType, resultType).apply(one); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#firstValue() + */ + @Nullable + @Override + public R firstValue() { + + List results = findTop(2); + + if (results.isEmpty()) { + return null; + } + + T one = results.get(0); + return getConversionFunction(entityType, resultType).apply(one); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#all() + */ + @Override + public List all() { + return stream().collect(Collectors.toList()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#page(org.springframework.data.domain.Pageable) + */ + @Override + public Page page(Pageable pageable) { + + Assert.notNull(pageable, "Pageable must not be null!"); + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#stream() + */ + @Override + public Stream stream() { + + Function conversionFunction = getConversionFunction(entityType, resultType); + + return search(null, QuerydslLdapQuery::list).stream().map(conversionFunction); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#count() + */ + @Override + public long count() { + return search(null, q -> q.search(it -> true)).size(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#exists() + */ + @Override + public boolean exists() { + return !search(1, q -> q.search(it -> true)).isEmpty(); + } + + private List findTop(int limit) { + return search(limit, QuerydslLdapQuery::list); + } + + private S search(@Nullable Integer limit, Function, S> searchFunction) { + + QuerydslLdapQuery q = queryFor(predicate, query -> { + + List projection = getProjection(); + + if (!projection.isEmpty()) { + query.attributes(projection.toArray(new String[0])); + } + + if (limit != null) { + query.countLimit(limit); + } + }); + + return searchFunction.apply(q); + } + + @SuppressWarnings("unchecked") + private

Function getConversionFunction(Class inputType, Class

targetType) { + + if (targetType.isAssignableFrom(inputType)) { + return (Function) Function.identity(); + } + + if (targetType.isInterface()) { + return o -> projectionFactory.createProjection(targetType, o); + } + + DtoInstantiatingConverter converter = new DtoInstantiatingConverter(targetType, context, entityInstantiators); + + return o -> (P) converter.convert(o); + } + + private List getProjection() { + + if (projection.isEmpty()) { + + if (resultType.isAssignableFrom(entityType)) { + return projection; + } + + if (resultType.isInterface()) { + ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(resultType); + + if (projectionInformation.isClosed()) { + return projectionInformation.getInputProperties().stream().map(FeatureDescriptor::getName) + .collect(Collectors.toList()); + } + } + } + + return projection; + } + } } diff --git a/src/main/java/org/springframework/data/ldap/repository/support/SimpleLdapRepository.java b/src/main/java/org/springframework/data/ldap/repository/support/SimpleLdapRepository.java index d38664b..5b0a1be 100644 --- a/src/main/java/org/springframework/data/ldap/repository/support/SimpleLdapRepository.java +++ b/src/main/java/org/springframework/data/ldap/repository/support/SimpleLdapRepository.java @@ -27,6 +27,9 @@ import javax.naming.Name; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.data.domain.Persistable; import org.springframework.data.ldap.repository.LdapRepository; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.util.Optionals; import org.springframework.lang.Nullable; import org.springframework.ldap.NameNotFoundException; @@ -70,6 +73,27 @@ public class SimpleLdapRepository implements LdapRepository { this.entityType = entityType; } + /** + * Creates a new {@link SimpleLdapRepository}. + * + * @param ldapOperations must not be {@literal null}. + * @param odm must not be {@literal null}. + * @param entityType must not be {@literal null}. + */ + SimpleLdapRepository(LdapOperations ldapOperations, + MappingContext, ? extends PersistentProperty> context, + ObjectDirectoryMapper odm, Class entityType) { + + Assert.notNull(ldapOperations, "LdapOperations must not be null!"); + Assert.notNull(context, "MappingContext must not be null!"); + Assert.notNull(odm, "ObjectDirectoryMapper must not be null!"); + Assert.notNull(entityType, "Entity type must not be null!"); + + this.ldapOperations = ldapOperations; + this.odm = odm; + this.entityType = entityType; + } + // ------------------------------------------------------------------------- // Methods from CrudRepository // ------------------------------------------------------------------------- diff --git a/src/test/java/org/springframework/data/ldap/repository/support/QuerydslLdapRepositoryUnitTests.java b/src/test/java/org/springframework/data/ldap/repository/support/QuerydslLdapRepositoryUnitTests.java new file mode 100644 index 0000000..2bff347 --- /dev/null +++ b/src/test/java/org/springframework/data/ldap/repository/support/QuerydslLdapRepositoryUnitTests.java @@ -0,0 +1,223 @@ +/* + * 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.support; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import lombok.Data; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +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.dao.IncorrectResultSizeDataAccessException; +import org.springframework.data.repository.query.FluentQuery; +import org.springframework.ldap.core.ContextMapper; +import org.springframework.ldap.core.LdapOperations; +import org.springframework.ldap.odm.core.impl.DefaultObjectDirectoryMapper; +import org.springframework.ldap.query.LdapQuery; + +/** + * Unit tests for {@link QuerydslLdapRepository}. + * + * @author Mark Paluch + */ +@MockitoSettings +class QuerydslLdapRepositoryUnitTests { + + @Mock LdapOperations ldapOperations; + + UnitTestPerson walter, hank; + + QuerydslLdapRepository repository; + + @BeforeEach + void before() throws Exception { + when(ldapOperations.getObjectDirectoryMapper()).thenReturn(new DefaultObjectDirectoryMapper()); + repository = new QuerydslLdapRepository<>(ldapOperations, ldapOperations.getObjectDirectoryMapper(), + UnitTestPerson.class); + + 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"); + + } + + @Test // GH-269 + void findByShouldReturnFirst() { + + when(ldapOperations.find(any(LdapQuery.class), eq(UnitTestPerson.class))).thenReturn(Arrays.asList(walter, hank), + Collections.emptyList()); + + UnitTestPerson first = repository.findBy(QPerson.person.fullName.eq("Walter"), + FluentQuery.FetchableFluentQuery::firstValue); + + assertThat(first).isEqualTo(walter); + + first = repository.findBy(QPerson.person.fullName.eq("Walter"), Function.identity()).firstValue(); + + assertThat(first).isNull(); + } + + @Test // GH-269 + void findByShouldReturnOne() { + + when(ldapOperations.find(any(LdapQuery.class), eq(UnitTestPerson.class))).thenReturn(Arrays.asList(walter, hank), + Collections.singletonList(walter)); + + assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class).isThrownBy( + () -> repository.findBy(QPerson.person.fullName.eq("Walter"), FluentQuery.FetchableFluentQuery::one)); + + UnitTestPerson one = repository.findBy(QPerson.person.fullName.eq("Walter"), + FluentQuery.FetchableFluentQuery::oneValue); + + assertThat(one).isEqualTo(walter); + } + + @Test // GH-269 + void findByShouldReturnFirstWithProjection() { + + when(ldapOperations.find(any(LdapQuery.class), eq(UnitTestPerson.class))).thenReturn(Arrays.asList(walter)); + + PersonProjection interfaceProjection = repository.findBy(QPerson.person.fullName.eq("Walter"), + it -> it.as(PersonProjection.class).firstValue()); + assertThat(interfaceProjection.getLastName()).isEqualTo("White"); + + PersonDto dto = repository.findBy(QPerson.person.fullName.eq("Walter"), it -> it.as(PersonDto.class).firstValue()); + assertThat(dto.getLastName()).isEqualTo("White"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(LdapQuery.class); + + verify(ldapOperations, times(2)).find(captor.capture(), any()); + + List queries = captor.getAllValues(); + + assertThat(queries.get(0).attributes()).containsOnly("lastName"); + assertThat(queries.get(1).attributes()).isNullOrEmpty(); + } + + @Test // GH-269 + void findByShouldReturnOneWithProjection() { + + when(ldapOperations.find(any(LdapQuery.class), eq(UnitTestPerson.class))) + .thenReturn(Collections.singletonList(walter)); + + PersonProjection interfaceProjection = repository.findBy(QPerson.person.fullName.eq("Walter"), + it -> it.as(PersonProjection.class).oneValue()); + assertThat(interfaceProjection.getLastName()).isEqualTo("White"); + + PersonDto dto = repository.findBy(QPerson.person.fullName.eq("Walter"), it -> it.as(PersonDto.class).oneValue()); + assertThat(dto.getLastName()).isEqualTo("White"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(LdapQuery.class); + + verify(ldapOperations, times(2)).find(captor.capture(), any()); + + List queries = captor.getAllValues(); + + assertThat(queries.get(0).attributes()).containsOnly("lastName"); + assertThat(queries.get(1).attributes()).isNullOrEmpty(); + } + + @Test // GH-269 + void findByShouldReturnAll() { + + when(ldapOperations.find(any(LdapQuery.class), eq(UnitTestPerson.class))).thenReturn(Arrays.asList(walter, hank)); + + List all = repository.findBy(QPerson.person.fullName.eq("Walter"), + FluentQuery.FetchableFluentQuery::all); + + assertThat(all).contains(walter); + } + + @Test // GH-269 + void findByShouldReturnAllWithProjection() { + + when(ldapOperations.find(any(LdapQuery.class), eq(UnitTestPerson.class))).thenReturn(Arrays.asList(walter, hank)); + + Stream all = repository.findBy(QPerson.person.fullName.eq("Walter"), + q -> q.as(PersonProjection.class).stream()); + + assertThat(all).hasOnlyElementsOfType(PersonProjection.class); + } + + @Test // GH-269 + void findByShouldReturnStream() { + + when(ldapOperations.find(any(LdapQuery.class), eq(UnitTestPerson.class))).thenReturn(Arrays.asList(walter, hank)); + + List all = repository.findBy(QPerson.person.fullName.eq("Walter"), + FluentQuery.FetchableFluentQuery::all); + + assertThat(all).contains(walter); + } + + @Test // GH-269 + void findByShouldReturnStreamWithProjection() { + + when(ldapOperations.find(any(LdapQuery.class), eq(UnitTestPerson.class))).thenReturn(Arrays.asList(walter, hank)); + + Stream all = repository.findBy(QPerson.person.fullName.eq("Walter"), + q -> q.as(PersonProjection.class).stream()); + + assertThat(all).hasOnlyElementsOfType(PersonProjection.class); + } + + @Test // GH-269 + void findByShouldReturnCount() { + + when(ldapOperations.find(any(LdapQuery.class), eq(UnitTestPerson.class))).thenReturn(Arrays.asList(walter, hank)); + + long count = repository.findBy(QPerson.person.fullName.eq("Walter"), FluentQuery.FetchableFluentQuery::count); + + assertThat(count).isEqualTo(2); + } + + @Test // GH-269 + void findByShouldReturnExists() { + + when(ldapOperations.search(any(LdapQuery.class), any(ContextMapper.class))).thenReturn(Arrays.asList(true, true), + Collections.emptyList()); + + boolean exists = repository.findBy(QPerson.person.fullName.eq("Walter"), FluentQuery.FetchableFluentQuery::exists); + assertThat(exists).isTrue(); + + exists = repository.findBy(QPerson.person.fullName.eq("Walter"), FluentQuery.FetchableFluentQuery::exists); + assertThat(exists).isFalse(); + } + + interface PersonProjection { + String getLastName(); + } + + @Data + static class PersonDto { + + String lastName; + } +} diff --git a/src/test/java/org/springframework/data/ldap/repository/support/UnitTestPerson.java b/src/test/java/org/springframework/data/ldap/repository/support/UnitTestPerson.java index 333a6d5..3213396 100644 --- a/src/test/java/org/springframework/data/ldap/repository/support/UnitTestPerson.java +++ b/src/test/java/org/springframework/data/ldap/repository/support/UnitTestPerson.java @@ -16,6 +16,10 @@ package org.springframework.data.ldap.repository.support; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + import java.util.List; import javax.naming.Name; @@ -30,6 +34,8 @@ import org.springframework.ldap.odm.annotations.Transient; * @author Mattias Hellborg Arthursson */ @Entry(objectClasses = {"inetOrgPerson", "organizationalPerson", "person", "top"}) +@AllArgsConstructor +@NoArgsConstructor public class UnitTestPerson { @Id private Name dn;