diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java index ebe585872..d6b65be25 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2015 the original author or authors. + * Copyright 2010-2016 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. @@ -15,31 +15,25 @@ */ package org.springframework.data.mongodb.repository.query; -import java.util.Collections; -import java.util.List; - -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Range; -import org.springframework.data.domain.Slice; -import org.springframework.data.domain.SliceImpl; -import org.springframework.data.geo.Distance; -import org.springframework.data.geo.GeoPage; -import org.springframework.data.geo.GeoResult; -import org.springframework.data.geo.GeoResults; -import org.springframework.data.geo.Point; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.mongodb.core.MongoOperations; -import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.CollectionExecution; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.DeleteExecution; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.GeoNearExecution; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.PagedExecution; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.PagingGeoNearExecution; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.ResultProcessingConverter; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.ResultProcessingExecution; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.SingleEntityExecution; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.SlicedExecution; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.StreamExecution; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.RepositoryQuery; -import org.springframework.data.util.CloseableIterator; -import org.springframework.data.util.StreamUtils; -import org.springframework.data.util.TypeInformation; +import org.springframework.data.repository.query.ResultProcessor; import org.springframework.util.Assert; -import com.mongodb.WriteResult; - /** * Base class for {@link RepositoryQuery} implementations for Mongo. * @@ -51,6 +45,7 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { private final MongoQueryMethod method; private final MongoOperations operations; + private final EntityInstantiators instantiators; /** * Creates a new {@link AbstractMongoQuery} from the given {@link MongoQueryMethod} and {@link MongoOperations}. @@ -65,6 +60,7 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { this.method = method; this.operations = operations; + this.instantiators = new EntityInstantiators(); } /* @@ -86,30 +82,53 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { applyQueryMetaAttributesWhenPresent(query); + ResultProcessor processor = method.getResultProcessor().withDynamicProjection(accessor); + String collection = method.getEntityInformation().getCollectionName(); + + MongoQueryExecution execution = getExecution(query, accessor, + new ResultProcessingConverter(processor, operations, instantiators)); + + return execution.execute(query, processor.getReturnedType().getDomainType(), collection); + } + + /** + * Returns the execution instance to use. + * + * @param query must not be {@literal null}. + * @param parameters must not be {@literal null}. + * @param accessor must not be {@literal null}. + * @return + */ + private MongoQueryExecution getExecution(Query query, MongoParameterAccessor accessor, + Converter resultProcessing) { + if (method.isStreamQuery()) { - return new StreamExecution().execute(query); - } else if (isDeleteQuery()) { - return new DeleteExecution().execute(query); - } else if (method.isGeoNearQuery() && method.isPageQuery()) { + return new StreamExecution(operations, resultProcessing); + } + + return new ResultProcessingExecution(getExecutionToWrap(query, accessor), resultProcessing); + } - MongoParameterAccessor countAccessor = new MongoParametersParameterAccessor(method, parameters); - Query countQuery = createCountQuery(new ConvertingParameterAccessor(operations.getConverter(), countAccessor)); + private MongoQueryExecution getExecutionToWrap(Query query, MongoParameterAccessor accessor) { - return new GeoNearExecution(accessor).execute(query, countQuery); + if (isDeleteQuery()) { + return new DeleteExecution(operations, method); + } else if (method.isGeoNearQuery() && method.isPageQuery()) { + return new PagingGeoNearExecution(operations, accessor, method.getReturnType(), this); } else if (method.isGeoNearQuery()) { - return new GeoNearExecution(accessor).execute(query); + return new GeoNearExecution(operations, accessor, method.getReturnType()); } else if (method.isSliceQuery()) { - return new SlicedExecution(accessor.getPageable()).execute(query); + return new SlicedExecution(operations, accessor.getPageable()); } else if (method.isCollectionQuery()) { - return new CollectionExecution(accessor.getPageable()).execute(query); + return new CollectionExecution(operations, accessor.getPageable()); } else if (method.isPageQuery()) { - return new PagedExecution(accessor.getPageable()).execute(query); + return new PagedExecution(operations, accessor.getPageable()); } else { - return new SingleEntityExecution(isCountQuery()).execute(query); + return new SingleEntityExecution(operations, isCountQuery()); } } - private Query applyQueryMetaAttributesWhenPresent(Query query) { + Query applyQueryMetaAttributesWhenPresent(Query query) { if (method.hasQueryMetaAttributes()) { query.setMeta(method.getQueryMetaAttributes()); @@ -127,12 +146,7 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { * @return */ protected Query createCountQuery(ConvertingParameterAccessor accessor) { - - Query query = createQuery(accessor); - - applyQueryMetaAttributesWhenPresent(query); - - return query; + return applyQueryMetaAttributesWhenPresent(createQuery(accessor)); } /** @@ -157,292 +171,4 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { * @since 1.5 */ protected abstract boolean isDeleteQuery(); - - private abstract class Execution { - - abstract Object execute(Query query); - - protected List readCollection(Query query) { - - MongoEntityMetadata metadata = method.getEntityInformation(); - - String collectionName = metadata.getCollectionName(); - return operations.find(query, metadata.getJavaType(), collectionName); - } - } - - /** - * {@link Execution} for collection returning queries. - * - * @author Oliver Gierke - */ - final class CollectionExecution extends Execution { - - private final Pageable pageable; - - CollectionExecution(Pageable pageable) { - this.pageable = pageable; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery.Execution#execute(org.springframework.data.mongodb.core.query.Query) - */ - @Override - public Object execute(Query query) { - return readCollection(query.with(pageable)); - } - } - - /** - * {@link Execution} for {@link Slice} query methods. - * - * @author Oliver Gierke - * @author Christoph Strobl - * @since 1.5 - */ - - final class SlicedExecution extends Execution { - - private final Pageable pageable; - - SlicedExecution(Pageable pageable) { - this.pageable = pageable; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery.Execution#execute(org.springframework.data.mongodb.core.query.Query) - */ - @Override - @SuppressWarnings({ "unchecked", "rawtypes" }) - Object execute(Query query) { - - MongoEntityMetadata metadata = method.getEntityInformation(); - int pageSize = pageable.getPageSize(); - - // Apply Pageable but tweak limit to peek into next page - Query modifiedQuery = query.with(pageable).limit(pageSize + 1); - - List result = operations.find(modifiedQuery, metadata.getJavaType(), metadata.getCollectionName()); - - boolean hasNext = result.size() > pageSize; - - return new SliceImpl(hasNext ? result.subList(0, pageSize) : result, pageable, hasNext); - } - } - - /** - * {@link Execution} for pagination queries. - * - * @author Oliver Gierke - */ - final class PagedExecution extends Execution { - - private final Pageable pageable; - - /** - * Creates a new {@link PagedExecution}. - * - * @param pageable - */ - public PagedExecution(Pageable pageable) { - - Assert.notNull(pageable); - this.pageable = pageable; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.repository.AbstractMongoQuery.Execution#execute(org.springframework.data.mongodb.core.query.Query) - */ - @Override - @SuppressWarnings({ "rawtypes", "unchecked" }) - Object execute(Query query) { - - MongoEntityMetadata metadata = method.getEntityInformation(); - String collectionName = metadata.getCollectionName(); - Class type = metadata.getJavaType(); - - int overallLimit = query.getLimit(); - long count = operations.count(query, type, collectionName); - count = overallLimit != 0 ? Math.min(count, query.getLimit()) : count; - - boolean pageableOutOfScope = pageable.getOffset() > count; - - if (pageableOutOfScope) { - return new PageImpl(Collections.emptyList(), pageable, count); - } - - // Apply raw pagination - query = query.with(pageable); - - // Adjust limit if page would exceed the overall limit - if (overallLimit != 0 && pageable.getOffset() + pageable.getPageSize() > overallLimit) { - query.limit(overallLimit - pageable.getOffset()); - } - - List result = operations.find(query, type, collectionName); - return new PageImpl(result, pageable, count); - } - } - - /** - * {@link Execution} to return a single entity. - * - * @author Oliver Gierke - */ - final class SingleEntityExecution extends Execution { - - private final boolean countProjection; - - private SingleEntityExecution(boolean countProjection) { - this.countProjection = countProjection; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.repository.AbstractMongoQuery.Execution#execute(org.springframework.data.mongodb.core.core.query.Query) - */ - @Override - Object execute(Query query) { - - MongoEntityMetadata metadata = method.getEntityInformation(); - return countProjection ? operations.count(query, metadata.getJavaType()) - : operations.findOne(query, metadata.getJavaType(), metadata.getCollectionName()); - } - } - - /** - * {@link Execution} to execute geo-near queries. - * - * @author Oliver Gierke - */ - final class GeoNearExecution extends Execution { - - private final MongoParameterAccessor accessor; - - public GeoNearExecution(MongoParameterAccessor accessor) { - this.accessor = accessor; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.repository.AbstractMongoQuery.Execution#execute(org.springframework.data.mongodb.core.query.Query) - */ - @Override - Object execute(Query query) { - - GeoResults results = doExecuteQuery(query); - return isListOfGeoResult() ? results.getContent() : results; - } - - /** - * Executes the given {@link Query} to return a page. - * - * @param query must not be {@literal null}. - * @param countQuery must not be {@literal null}. - * @return - */ - Object execute(Query query, Query countQuery) { - - MongoEntityMetadata metadata = method.getEntityInformation(); - long count = operations.count(countQuery, metadata.getCollectionName()); - - return new GeoPage(doExecuteQuery(query), accessor.getPageable(), count); - } - - @SuppressWarnings("unchecked") - private GeoResults doExecuteQuery(Query query) { - - Point nearLocation = accessor.getGeoNearLocation(); - NearQuery nearQuery = NearQuery.near(nearLocation); - - if (query != null) { - nearQuery.query(query); - } - - Range distances = accessor.getDistanceRange(); - Distance maxDistance = distances.getUpperBound(); - - if (maxDistance != null) { - nearQuery.maxDistance(maxDistance).in(maxDistance.getMetric()); - } - - Distance minDistance = distances.getLowerBound(); - - if (minDistance != null) { - nearQuery.minDistance(minDistance).in(minDistance.getMetric()); - } - - Pageable pageable = accessor.getPageable(); - if (pageable != null) { - nearQuery.with(pageable); - } - - MongoEntityMetadata metadata = method.getEntityInformation(); - return (GeoResults) operations.geoNear(nearQuery, metadata.getJavaType(), metadata.getCollectionName()); - } - - private boolean isListOfGeoResult() { - - TypeInformation returnType = method.getReturnType(); - - if (!returnType.getType().equals(List.class)) { - return false; - } - - TypeInformation componentType = returnType.getComponentType(); - return componentType == null ? false : GeoResult.class.equals(componentType.getType()); - } - } - - /** - * {@link Execution} removing documents matching the query. - * - * @since 1.5 - */ - final class DeleteExecution extends Execution { - - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery.Execution#execute(org.springframework.data.mongodb.core.query.Query) - */ - @Override - Object execute(Query query) { - - MongoEntityMetadata metadata = method.getEntityInformation(); - return deleteAndConvertResult(query, metadata); - } - - private Object deleteAndConvertResult(Query query, MongoEntityMetadata metadata) { - - if (method.isCollectionQuery()) { - return operations.findAllAndRemove(query, metadata.getJavaType(), metadata.getCollectionName()); - } - - WriteResult writeResult = operations.remove(query, metadata.getJavaType(), metadata.getCollectionName()); - return writeResult != null ? writeResult.getN() : 0L; - } - } - - /** - * @author Thomas Darimont - * @since 1.7 - */ - final class StreamExecution extends Execution { - - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery.Execution#execute(org.springframework.data.mongodb.core.query.Query) - */ - @Override - @SuppressWarnings("unchecked") - Object execute(Query query) { - - Class entityType = getQueryMethod().getEntityInformation().getJavaType(); - - return StreamUtils.createStreamFromIterator((CloseableIterator) operations.stream(query, entityType)); - } - } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/DtoInstantiatingConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/DtoInstantiatingConverter.java new file mode 100644 index 000000000..0aa06e5e5 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/DtoInstantiatingConverter.java @@ -0,0 +1,108 @@ +/* + * Copyright 2015-2016 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.mongodb.repository.query; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.EntityInstantiator; +import org.springframework.data.convert.EntityInstantiators; +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.ParameterValueProvider; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.util.Assert; + +/** + * {@link Converter} to instantiate DTOs from fully equipped domain objects. + * + * @author Oliver Gierke + */ +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 instantiators must not be {@literal null}. + */ + public DtoInstantiatingConverter(Class dtoType, + MappingContext, MongoPersistentProperty> context, + EntityInstantiators instantiator) { + + Assert.notNull(dtoType, "DTO type must not be null!"); + Assert.notNull(context, "MappingContext must not be null!"); + Assert.notNull(instantiator, "EntityInstantiators must not be null!"); + + this.targetType = dtoType; + this.context = context; + this.instantiator = instantiator.getInstantiatorFor(context.getPersistentEntity(dtoType)); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Object convert(Object source) { + + if (targetType.isInterface()) { + return source; + } + + final PersistentEntity sourceEntity = context.getPersistentEntity(source.getClass()); + final PersistentPropertyAccessor sourceAccessor = sourceEntity.getPropertyAccessor(source); + final PersistentEntity targetEntity = context.getPersistentEntity(targetType); + final PreferredConstructor> constructor = targetEntity + .getPersistenceConstructor(); + + @SuppressWarnings({ "rawtypes", "unchecked" }) + Object dto = instantiator.createInstance(targetEntity, new ParameterValueProvider() { + + @Override + public Object getParameterValue(Parameter parameter) { + return sourceAccessor.getProperty(sourceEntity.getPersistentProperty(parameter.getName())); + } + }); + + final PersistentPropertyAccessor dtoAccessor = targetEntity.getPropertyAccessor(dto); + + targetEntity.doWithProperties(new SimplePropertyHandler() { + + @Override + public void doWithPersistentProperty(PersistentProperty property) { + + if (constructor.isConstructorParameter(property)) { + return; + } + + dtoAccessor.setProperty(property, + sourceAccessor.getProperty(sourceEntity.getPersistentProperty(property.getName()))); + } + }); + + return dto; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java index a0cb9aa00..caee21e2a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java @@ -169,32 +169,31 @@ class MongoQueryCreator extends AbstractQueryCreator { * @param parameters * @return */ - private Criteria from(Part part, MongoPersistentProperty property, Criteria criteria, - PotentiallyConvertingIterator parameters) { + private Criteria from(Part part, MongoPersistentProperty property, Criteria criteria, Iterator parameters) { Type type = part.getType(); switch (type) { case AFTER: case GREATER_THAN: - return criteria.gt(parameters.nextConverted(property)); + return criteria.gt(parameters.next()); case GREATER_THAN_EQUAL: - return criteria.gte(parameters.nextConverted(property)); + return criteria.gte(parameters.next()); case BEFORE: case LESS_THAN: - return criteria.lt(parameters.nextConverted(property)); + return criteria.lt(parameters.next()); case LESS_THAN_EQUAL: - return criteria.lte(parameters.nextConverted(property)); + return criteria.lte(parameters.next()); case BETWEEN: - return criteria.gt(parameters.nextConverted(property)).lt(parameters.nextConverted(property)); + return criteria.gt(parameters.next()).lt(parameters.next()); case IS_NOT_NULL: return criteria.ne(null); case IS_NULL: return criteria.is(null); case NOT_IN: - return criteria.nin(nextAsArray(parameters, property)); + return criteria.nin(nextAsArray(parameters)); case IN: - return criteria.in(nextAsArray(parameters, property)); + return criteria.in(nextAsArray(parameters)); case LIKE: case STARTING_WITH: case ENDING_WITH: @@ -241,12 +240,12 @@ class MongoQueryCreator extends AbstractQueryCreator { return criteria.within((Shape) parameter); case SIMPLE_PROPERTY: - return isSimpleComparisionPossible(part) ? criteria.is(parameters.nextConverted(property)) + return isSimpleComparisionPossible(part) ? criteria.is(parameters.next()) : createLikeRegexCriteriaOrThrow(part, property, criteria, parameters, false); case NEGATING_SIMPLE_PROPERTY: - return isSimpleComparisionPossible(part) ? criteria.ne(parameters.nextConverted(property)) + return isSimpleComparisionPossible(part) ? criteria.ne(parameters.next()) : createLikeRegexCriteriaOrThrow(part, property, criteria, parameters, true); default: throw new IllegalArgumentException("Unsupported keyword!"); @@ -278,7 +277,7 @@ class MongoQueryCreator extends AbstractQueryCreator { * @return the criteria extended with the like-regex. */ private Criteria createLikeRegexCriteriaOrThrow(Part part, MongoPersistentProperty property, Criteria criteria, - PotentiallyConvertingIterator parameters, boolean shouldNegateExpression) { + Iterator parameters, boolean shouldNegateExpression) { PropertyPath path = part.getProperty().getLeafProperty(); @@ -297,7 +296,7 @@ class MongoQueryCreator extends AbstractQueryCreator { criteria = criteria.not(); } - return addAppropriateLikeRegexTo(criteria, part, parameters.nextConverted(property).toString()); + return addAppropriateLikeRegexTo(criteria, part, parameters.next().toString()); case NEVER: // intentional no-op @@ -319,10 +318,10 @@ class MongoQueryCreator extends AbstractQueryCreator { * @return */ private Criteria createContainingCriteria(Part part, MongoPersistentProperty property, Criteria criteria, - PotentiallyConvertingIterator parameters) { + Iterator parameters) { if (property.isCollectionLike()) { - return criteria.in(nextAsArray(parameters, property)); + return criteria.in(nextAsArray(parameters)); } return addAppropriateLikeRegexTo(criteria, part, parameters.next().toString()); @@ -377,8 +376,9 @@ class MongoQueryCreator extends AbstractQueryCreator { String.format("Expected parameter type of %s but got %s!", type, parameter.getClass())); } - private Object[] nextAsArray(PotentiallyConvertingIterator iterator, MongoPersistentProperty property) { - Object next = iterator.nextConverted(property); + private Object[] nextAsArray(Iterator iterator) { + + Object next = iterator.next(); if (next instanceof Collection) { return ((Collection) next).toArray(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java new file mode 100644 index 000000000..c4274977b --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java @@ -0,0 +1,381 @@ +/* + * Copyright 2016 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.mongodb.repository.query; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.EntityInstantiators; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Range; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.GeoPage; +import org.springframework.data.geo.GeoResult; +import org.springframework.data.geo.GeoResults; +import org.springframework.data.geo.Point; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.query.NearQuery; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.ReturnedType; +import org.springframework.data.util.CloseableIterator; +import org.springframework.data.util.StreamUtils; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.ClassUtils; + +import com.mongodb.WriteResult; + +interface MongoQueryExecution { + + Object execute(Query query, Class type, String collection); + + /** + * {@link MongoQueryExecution} for collection returning queries. + * + * @author Oliver Gierke + */ + @RequiredArgsConstructor + static final class CollectionExecution implements MongoQueryExecution { + + private final @NonNull MongoOperations operations; + private final Pageable pageable; + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery.Execution#execute(org.springframework.data.mongodb.core.query.Query, java.lang.Class, java.lang.String) + */ + @Override + public Object execute(Query query, Class type, String collection) { + return operations.find(query.with(pageable), type, collection); + } + } + + /** + * {@link MongoQueryExecution} for {@link Slice} query methods. + * + * @author Oliver Gierke + * @author Christoph Strobl + * @since 1.5 + */ + @RequiredArgsConstructor + static final class SlicedExecution implements MongoQueryExecution { + + private final @NonNull MongoOperations operations; + private final @NonNull Pageable pageable; + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery.Execution#execute(org.springframework.data.mongodb.core.query.Query, java.lang.Class, java.lang.String) + */ + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Object execute(Query query, Class type, String collection) { + + int pageSize = pageable.getPageSize(); + + // Apply Pageable but tweak limit to peek into next page + Query modifiedQuery = query.with(pageable).limit(pageSize + 1); + List result = operations.find(modifiedQuery, type, collection); + + boolean hasNext = result.size() > pageSize; + + return new SliceImpl(hasNext ? result.subList(0, pageSize) : result, pageable, hasNext); + } + } + + /** + * {@link MongoQueryExecution} for pagination queries. + * + * @author Oliver Gierke + */ + @RequiredArgsConstructor + static final class PagedExecution implements MongoQueryExecution { + + private final @NonNull MongoOperations operations; + private final @NonNull Pageable pageable; + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery.Execution#execute(org.springframework.data.mongodb.core.query.Query, java.lang.Class, java.lang.String) + */ + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public Object execute(Query query, Class type, String collection) { + + int overallLimit = query.getLimit(); + long count = operations.count(query, type, collection); + count = overallLimit != 0 ? Math.min(count, query.getLimit()) : count; + + boolean pageableOutOfScope = pageable.getOffset() > count; + + if (pageableOutOfScope) { + return new PageImpl(Collections.emptyList(), pageable, count); + } + + // Apply raw pagination + query = query.with(pageable); + + // Adjust limit if page would exceed the overall limit + if (overallLimit != 0 && pageable.getOffset() + pageable.getPageSize() > overallLimit) { + query.limit(overallLimit - pageable.getOffset()); + } + + List result = operations.find(query, type, collection); + return new PageImpl(result, pageable, count); + } + } + + /** + * {@link MongoQueryExecution} to return a single entity. + * + * @author Oliver Gierke + */ + @RequiredArgsConstructor + static final class SingleEntityExecution implements MongoQueryExecution { + + private final MongoOperations operations; + private final boolean countProjection; + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery.Execution#execute(org.springframework.data.mongodb.core.query.Query, java.lang.Class, java.lang.String) + */ + @Override + public Object execute(Query query, Class type, String collection) { + return countProjection ? operations.count(query, type, collection) : operations.findOne(query, type, collection); + } + } + + /** + * {@link MongoQueryExecution} to execute geo-near queries. + * + * @author Oliver Gierke + */ + @RequiredArgsConstructor + static class GeoNearExecution implements MongoQueryExecution { + + private final MongoOperations operations; + private final MongoParameterAccessor accessor; + private final TypeInformation returnType; + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery.Execution#execute(org.springframework.data.mongodb.core.query.Query, java.lang.Class, java.lang.String) + */ + @Override + public Object execute(Query query, Class type, String collection) { + + GeoResults results = doExecuteQuery(query, type, collection); + return isListOfGeoResult() ? results.getContent() : results; + } + + @SuppressWarnings("unchecked") + protected GeoResults doExecuteQuery(Query query, Class type, String collection) { + + Point nearLocation = accessor.getGeoNearLocation(); + NearQuery nearQuery = NearQuery.near(nearLocation); + + if (query != null) { + nearQuery.query(query); + } + + Range distances = accessor.getDistanceRange(); + Distance maxDistance = distances.getUpperBound(); + + if (maxDistance != null) { + nearQuery.maxDistance(maxDistance).in(maxDistance.getMetric()); + } + + Distance minDistance = distances.getLowerBound(); + + if (minDistance != null) { + nearQuery.minDistance(minDistance).in(minDistance.getMetric()); + } + + Pageable pageable = accessor.getPageable(); + + if (pageable != null) { + nearQuery.with(pageable); + } + + return (GeoResults) operations.geoNear(nearQuery, type, collection); + } + + private boolean isListOfGeoResult() { + + if (!returnType.getType().equals(List.class)) { + return false; + } + + TypeInformation componentType = returnType.getComponentType(); + return componentType == null ? false : GeoResult.class.equals(componentType.getType()); + } + } + + static final class PagingGeoNearExecution extends GeoNearExecution { + + private final MongoOperations operations; + private final MongoParameterAccessor accessor; + private final AbstractMongoQuery mongoQuery; + + public PagingGeoNearExecution(MongoOperations operations, MongoParameterAccessor accessor, + TypeInformation returnType, AbstractMongoQuery query) { + + super(operations, accessor, returnType); + + this.accessor = accessor; + this.operations = operations; + this.mongoQuery = query; + } + + /** + * Executes the given {@link Query} to return a page. + * + * @param query must not be {@literal null}. + * @param countQuery must not be {@literal null}. + * @return + */ + @Override + public Object execute(Query query, Class type, String collection) { + + ConvertingParameterAccessor parameterAccessor = new ConvertingParameterAccessor(operations.getConverter(), + accessor); + Query countQuery = mongoQuery.applyQueryMetaAttributesWhenPresent(mongoQuery.createCountQuery(parameterAccessor)); + long count = operations.count(countQuery, collection); + + return new GeoPage(doExecuteQuery(query, type, collection), accessor.getPageable(), count); + } + } + + /** + * {@link MongoQueryExecution} removing documents matching the query. + * + * @since 1.5 + */ + @RequiredArgsConstructor + static final class DeleteExecution implements MongoQueryExecution { + + private final MongoOperations operations; + private final MongoQueryMethod method; + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery.Execution#execute(org.springframework.data.mongodb.core.query.Query, java.lang.Class, java.lang.String) + */ + @Override + public Object execute(Query query, Class type, String collection) { + + if (method.isCollectionQuery()) { + return operations.findAllAndRemove(query, type, collection); + } + + WriteResult writeResult = operations.remove(query, type, collection); + return writeResult != null ? writeResult.getN() : 0L; + } + } + + /** + * @author Thomas Darimont + * @since 1.7 + */ + @RequiredArgsConstructor + static final class StreamExecution implements MongoQueryExecution { + + private final @NonNull MongoOperations operations; + private final @NonNull Converter resultProcessing; + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery.Execution#execute(org.springframework.data.mongodb.core.query.Query, java.lang.Class, java.lang.String) + */ + @Override + @SuppressWarnings("unchecked") + public Object execute(Query query, Class type, String collection) { + + return StreamUtils.createStreamFromIterator((CloseableIterator) operations.stream(query, type)) + .map(new Function() { + + @Override + public Object apply(Object t) { + return resultProcessing.convert(t); + } + }); + } + } + + /** + * An {@link MongoQueryExecution} that wraps the results of the given delegate with the given result processing. + * + * @author Oliver Gierke + * @since 1.9 + */ + @RequiredArgsConstructor + static final class ResultProcessingExecution implements MongoQueryExecution { + + private final @NonNull MongoQueryExecution delegate; + private final @NonNull Converter converter; + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery.Execution#execute(org.springframework.data.mongodb.core.query.Query, java.lang.Class, java.lang.String) + */ + @Override + public Object execute(Query query, Class type, String collection) { + return converter.convert(delegate.execute(query, type, collection)); + } + } + + /** + * A {@link Converter} to post-process all source objects using the given {@link ResultProcessor}. + * + * @author Oliver Gierke + * @since 1.9 + */ + @RequiredArgsConstructor + static final class ResultProcessingConverter implements Converter { + + private final @NonNull ResultProcessor processor; + private final @NonNull MongoOperations operations; + private final @NonNull EntityInstantiators 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; + } + + Converter converter = new DtoInstantiatingConverter(returnedType.getReturnedType(), + operations.getConverter().getMappingContext(), instantiators); + + return processor.processResult(source, converter); + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java index f000fc392..85f0805f5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java @@ -136,7 +136,8 @@ public class MongoQueryMethod extends QueryMethod { MongoPersistentEntity returnedEntity = mappingContext.getPersistentEntity(returnedObjectType); MongoPersistentEntity managedEntity = mappingContext.getPersistentEntity(domainClass); - returnedEntity = returnedEntity == null ? managedEntity : returnedEntity; + returnedEntity = returnedEntity == null || returnedEntity.getType().isInterface() ? managedEntity + : returnedEntity; MongoPersistentEntity collectionEntity = domainClass.isAssignableFrom(returnedObjectType) ? returnedEntity : managedEntity; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java index ffbfe3505..349df7a2c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java @@ -44,6 +44,7 @@ public class PartTreeMongoQuery extends AbstractMongoQuery { private final PartTree tree; private final boolean isGeoNearQuery; private final MappingContext context; + private final ResultProcessor processor; /** * Creates a new {@link PartTreeMongoQuery} from the given {@link QueryMethod} and {@link MongoTemplate}. @@ -54,7 +55,9 @@ public class PartTreeMongoQuery extends AbstractMongoQuery { public PartTreeMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations) { super(method, mongoOperations); - this.tree = new PartTree(method.getName(), method.getEntityInformation().getJavaType()); + + this.processor = method.getResultProcessor(); + this.tree = new PartTree(method.getName(), processor.getReturnedType().getDomainType()); this.isGeoNearQuery = method.isGeoNearQuery(); this.context = mongoOperations.getConverter().getMappingContext(); } @@ -91,7 +94,6 @@ public class PartTreeMongoQuery extends AbstractMongoQuery { if (!StringUtils.hasText(fieldSpec)) { - ResultProcessor processor = getQueryMethod().getResultProcessor(); ReturnedType returnedType = processor.withDynamicProjection(accessor).getReturnedType(); if (returnedType.isProjecting()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java index f73676008..03d75f2c8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java @@ -54,7 +54,7 @@ public class MongoRepositoryFactory extends RepositoryFactorySupport { private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); - private final MongoOperations mongoOperations; + private final MongoOperations operations; private final MappingContext, MongoPersistentProperty> mappingContext; /** @@ -66,7 +66,7 @@ public class MongoRepositoryFactory extends RepositoryFactorySupport { Assert.notNull(mongoOperations); - this.mongoOperations = mongoOperations; + this.operations = mongoOperations; this.mappingContext = mongoOperations.getConverter().getMappingContext(); } @@ -92,7 +92,7 @@ public class MongoRepositoryFactory extends RepositoryFactorySupport { MongoEntityInformation entityInformation = getEntityInformation(information.getDomainType(), information); - return getTargetRepositoryViaReflection(information, entityInformation, mongoOperations); + return getTargetRepositoryViaReflection(information, entityInformation, operations); } /* @@ -101,7 +101,7 @@ public class MongoRepositoryFactory extends RepositoryFactorySupport { */ @Override protected QueryLookupStrategy getQueryLookupStrategy(Key key, EvaluationContextProvider evaluationContextProvider) { - return new MongoQueryLookupStrategy(evaluationContextProvider); + return new MongoQueryLookupStrategy(operations, evaluationContextProvider, mappingContext); } /* @@ -133,12 +133,18 @@ public class MongoRepositoryFactory extends RepositoryFactorySupport { * @author Oliver Gierke * @author Thomas Darimont */ - private class MongoQueryLookupStrategy implements QueryLookupStrategy { + private static class MongoQueryLookupStrategy implements QueryLookupStrategy { + private final MongoOperations operations; private final EvaluationContextProvider evaluationContextProvider; + MappingContext, MongoPersistentProperty> mappingContext; - public MongoQueryLookupStrategy(EvaluationContextProvider evaluationContextProvider) { + public MongoQueryLookupStrategy(MongoOperations operations, EvaluationContextProvider evaluationContextProvider, + MappingContext, MongoPersistentProperty> mappingContext) { + + this.operations = operations; this.evaluationContextProvider = evaluationContextProvider; + this.mappingContext = mappingContext; } /* @@ -154,12 +160,12 @@ public class MongoRepositoryFactory extends RepositoryFactorySupport { if (namedQueries.hasQuery(namedQueryName)) { String namedQuery = namedQueries.getQuery(namedQueryName); - return new StringBasedMongoQuery(namedQuery, queryMethod, mongoOperations, EXPRESSION_PARSER, + return new StringBasedMongoQuery(namedQuery, queryMethod, operations, EXPRESSION_PARSER, evaluationContextProvider); } else if (queryMethod.hasAnnotatedQuery()) { - return new StringBasedMongoQuery(queryMethod, mongoOperations, EXPRESSION_PARSER, evaluationContextProvider); + return new StringBasedMongoQuery(queryMethod, operations, EXPRESSION_PARSER, evaluationContextProvider); } else { - return new PartTreeMongoQuery(queryMethod, mongoOperations); + return new PartTreeMongoQuery(queryMethod, operations); } } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java index d2ac7fd53..9780f32be 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java @@ -17,8 +17,6 @@ package org.springframework.data.mongodb.repository.query; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; -import static org.mockito.Matchers.*; -import static org.mockito.Matchers.any; import static org.mockito.Mockito.*; import static org.springframework.data.mongodb.core.query.Criteria.*; import static org.springframework.data.mongodb.core.query.Query.*; @@ -27,16 +25,11 @@ import static org.springframework.data.mongodb.repository.query.StubParameterAcc import java.lang.reflect.Method; import java.util.List; +import org.bson.types.ObjectId; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.runners.MockitoJUnitRunner; -import org.mockito.stubbing.Answer; import org.springframework.data.domain.Range; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Metrics; @@ -44,14 +37,19 @@ import org.springframework.data.geo.Point; import org.springframework.data.geo.Polygon; import org.springframework.data.geo.Shape; import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mongodb.MongoDbFactory; import org.springframework.data.mongodb.core.Person; import org.springframework.data.mongodb.core.Venue; +import org.springframework.data.mongodb.core.convert.DbRefResolver; +import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.index.GeoSpatialIndexType; import org.springframework.data.mongodb.core.index.GeoSpatialIndexed; import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; @@ -59,7 +57,8 @@ import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.query.parser.PartTree; -import org.springframework.data.util.TypeInformation; + +import com.mongodb.DBObject; /** * Unit test for {@link MongoQueryCreator}. @@ -68,14 +67,12 @@ import org.springframework.data.util.TypeInformation; * @author Thomas Darimont * @author Christoph Strobl */ -@RunWith(MockitoJUnitRunner.class) public class MongoQueryCreatorUnitTests { Method findByFirstname, findByFirstnameAndFriend, findByFirstnameNotNull; - @Mock MongoConverter converter; - - MappingContext context; + MappingContext, MongoPersistentProperty> context; + MongoConverter converter; @Rule public ExpectedException expection = ExpectedException.none(); @@ -84,11 +81,8 @@ public class MongoQueryCreatorUnitTests { context = new MongoMappingContext(); - doAnswer(new Answer() { - public Object answer(InvocationOnMock invocation) throws Throwable { - return invocation.getArguments()[0]; - } - }).when(converter).convertToMongoType(any(), Mockito.any(TypeInformation.class)); + DbRefResolver resolver = new DefaultDbRefResolver(mock(MongoDbFactory.class)); + converter = new MappingMongoConverter(resolver, context); } @Test @@ -243,14 +237,13 @@ public class MongoQueryCreatorUnitTests { public void createsQueryReferencingADBRefCorrectly() { User user = new User(); - com.mongodb.DBRef dbref = new com.mongodb.DBRef("user", "id"); - when(converter.toDBRef(eq(user), Mockito.any(MongoPersistentProperty.class))).thenReturn(dbref); + user.id = new ObjectId(); PartTree tree = new PartTree("findByCreator", User.class); MongoQueryCreator creator = new MongoQueryCreator(tree, getAccessor(converter, user), context); - Query query = creator.createQuery(); + DBObject queryObject = creator.createQuery().getQueryObject(); - assertThat(query, is(query(where("creator").is(dbref)))); + assertThat(queryObject.get("creator"), is((Object) user)); } /** @@ -294,8 +287,6 @@ public class MongoQueryCreatorUnitTests { private void assertBindsDistanceToQuery(Point point, Distance distance, Query reference) throws Exception { - when(converter.convertToMongoType("Dave")).thenReturn("Dave"); - PartTree tree = new PartTree("findByLocationNearAndFirstname", org.springframework.data.mongodb.repository.Person.class); Method method = PersonRepository.class.getMethod("findByLocationNearAndFirstname", Point.class, Distance.class, @@ -684,6 +675,8 @@ public class MongoQueryCreatorUnitTests { class User { + ObjectId id; + @Field("foo") String username; @DBRef User creator; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQueryUnitTests.java index 6323417b5..dc61cfbfd 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQueryUnitTests.java @@ -173,7 +173,7 @@ public class PartTreeMongoQueryUnitTests { @Test public void restrictsQueryToFieldsRequiredForDto() { - DBObject fieldsObject = deriveQueryFromMethod("findPersonDtoBy", new Object[0]).getFieldsObject(); + DBObject fieldsObject = deriveQueryFromMethod("findPersonDtoByAge", new Object[] { 42 }).getFieldsObject(); assertThat(fieldsObject.get("firstname"), is((Object) 1)); assertThat(fieldsObject.get("lastname"), is((Object) 1)); @@ -246,7 +246,7 @@ public class PartTreeMongoQueryUnitTests { PersonProjection findPersonProjectedBy(); - PersonDto findPersonDtoBy(); + PersonDto findPersonDtoByAge(Integer age); T findDynamicallyProjectedBy(Class type); } diff --git a/spring-data-mongodb/template.mf b/spring-data-mongodb/template.mf index 570624241..1bad89f75 100644 --- a/spring-data-mongodb/template.mf +++ b/spring-data-mongodb/template.mf @@ -2,6 +2,8 @@ Bundle-SymbolicName: org.springframework.data.mongodb Bundle-Name: Spring Data MongoDB Support Bundle-Vendor: Pivotal Software, Inc. Bundle-ManifestVersion: 2 +Excluded-Imports: + lombok.* Import-Package: sun.reflect;version="0";resolution:=optional Export-Template: