diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperation.java new file mode 100644 index 000000000..508024e2e --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperation.java @@ -0,0 +1,199 @@ +/* + * Copyright 2018 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.core; + +import java.util.List; + +import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind; +import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; +import org.springframework.data.mongodb.core.query.Query; + +/** + * {@link ExecutableMapReduceOperation} allows creation and execution of MongoDB mapReduce operations in a fluent API + * style. The starting {@literal domainType} is used for mapping an optional {@link Query} provided via {@code matching} + * into the MongoDB specific representation. By default, the originating {@literal domainType} is also used for mapping + * back the results from the {@link org.bson.Document}. However, it is possible to define an different + * {@literal returnType} via {@code as} to mapping the result.
+ * The collection to operate on is by default derived from the initial {@literal domainType} and can be defined there + * via {@link org.springframework.data.mongodb.core.mapping.Document}. Using {@code inCollection} allows to override the + * collection name for the execution. + * + *
+ *     
+ *         mapReduce(Human.class)
+ *             .map("function() { emit(this.id, this.firstname) }")
+ *             .reduce("function(id, name) { return sum(id, name); }")
+ *             .inCollection("star-wars")
+ *             .as(Jedi.class)
+ *             .matching(query(where("lastname").is("skywalker")))
+ *             .all();
+ *     
+ * 
+ * + * @author Christoph Strobl + * @since 2.1 + */ +public interface ExecutableMapReduceOperation { + + /** + * Start creating a mapReduce operation for the given {@literal domainType}. + * + * @param domainType must not be {@literal null}. + * @return new instance of {@link ExecutableFind}. + * @throws IllegalArgumentException if domainType is {@literal null}. + */ + MapReduceWithMapFunction mapReduce(Class domainType); + + /** + * Trigger mapReduce execution by calling one of the terminating methods. + * + * @author Christoph Strobl + * @since 2.1 + */ + interface TerminatingMapReduce { + + /** + * Get the mapReduce results. + * + * @return never {@literal null}. + */ + List all(); + } + + /** + * Provide the Javascript {@code function()} used to map matching documents. + * + * @author Christoph Strobl + * @since 2.1 + */ + interface MapReduceWithMapFunction { + + /** + * Set the Javascript map {@code function()}. + * + * @param mapFunction must not be {@literal null} nor empty. + * @return new instance of {@link MapReduceWithReduceFunction}. + * @throws IllegalArgumentException if {@literal mapFunction} is {@literal null} or empty. + */ + MapReduceWithReduceFunction map(String mapFunction); + + } + + /** + * Provide the Javascript {@code function()} used to reduce matching documents. + * + * @author Christoph Strobl + * @since 2.1 + */ + interface MapReduceWithReduceFunction { + + /** + * Set the Javascript map {@code function()}. + * + * @param reduceFunction must not be {@literal null} nor empty. + * @return new instance of {@link ExecutableMapReduce}. + * @throws IllegalArgumentException if {@literal reduceFunction} is {@literal null} or empty. + */ + ExecutableMapReduce reduce(String reduceFunction); + + } + + /** + * Collection override (Optional). + * + * @author Christoph Strobl + * @since 2.1 + */ + interface MapReduceWithCollection extends MapReduceWithQuery { + + /** + * Explicitly set the name of the collection to perform the mapReduce operation on.
+ * Skip this step to use the default collection derived from the domain type. + * + * @param collection must not be {@literal null} nor {@literal empty}. + * @return new instance of {@link MapReduceWithProjection}. + * @throws IllegalArgumentException if collection is {@literal null}. + */ + MapReduceWithProjection inCollection(String collection); + } + + /** + * Input document filter query (Optional). + * + * @author Christoph Strobl + * @since 2.1 + */ + interface MapReduceWithQuery extends TerminatingMapReduce { + + /** + * Set the filter query to be used. + * + * @param query must not be {@literal null}. + * @return new instance of {@link TerminatingMapReduce}. + * @throws IllegalArgumentException if query is {@literal null}. + */ + TerminatingMapReduce matching(Query query); + } + + /** + * Result type override (Optional). + * + * @author Christoph Strobl + * @since 2.1 + */ + interface MapReduceWithProjection extends MapReduceWithQuery { + + /** + * Define the target type fields should be mapped to.
+ * Skip this step if you are anyway only interested in the original domain type. + * + * @param resultType must not be {@literal null}. + * @param result type. + * @return new instance of {@link TerminatingMapReduce}. + * @throws IllegalArgumentException if resultType is {@literal null}. + */ + MapReduceWithQuery as(Class resultType); + } + + /** + * Additional mapReduce options (Optional). + * + * @author Christoph Strobl + * @since 2.1 + */ + interface MapReduceWithOptions { + + /** + * Set additional options to apply to the mapReduce operation. + * + * @param options must not be {@literal null}. + * @return new instance of {@link ExecutableMapReduce}. + * @throws IllegalArgumentException if options is {@literal null}. + */ + ExecutableMapReduce with(MapReduceOptions options); + } + + /** + * {@link ExecutableMapReduce} provides methods for constructing mapReduce operations in a fluent way. + * + * @author Christoph Strobl + * @since 2.1 + */ + interface ExecutableMapReduce extends MapReduceWithMapFunction, MapReduceWithReduceFunction, + MapReduceWithCollection, MapReduceWithProjection, MapReduceWithOptions { + + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupport.java new file mode 100644 index 000000000..3e7defa53 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupport.java @@ -0,0 +1,184 @@ +/* + * Copyright 2018 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.core; + +import java.util.List; + +import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Implementation of {@link ExecutableMapReduceOperation}. + * + * @author Christoph Strobl + * @since 2.1 + */ +class ExecutableMapReduceOperationSupport implements ExecutableMapReduceOperation { + + private static final Query ALL_QUERY = new Query(); + + private final MongoTemplate template; + + /** + * Create new {@link ExecutableMapReduceOperationSupport}. + * + * @param template must not be {@literal null}. + * @throws IllegalArgumentException if template is {@literal null}. + */ + ExecutableMapReduceOperationSupport(MongoTemplate template) { + + Assert.notNull(template, "Template must not be null!"); + this.template = template; + } + + /* + * (non-Javascript) + * @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation#mapReduce(java.lang.Class) + */ + @Override + public ExecutableMapReduceSupport mapReduce(Class domainType) { + + Assert.notNull(domainType, "DomainType must not be null!"); + + return new ExecutableMapReduceSupport(template, domainType, domainType, null, ALL_QUERY, null, null, null); + } + + /** + * @author Christoph Strobl + * @since 2.1 + */ + static class ExecutableMapReduceSupport + implements ExecutableMapReduce, MapReduceWithOptions, MapReduceWithCollection, + MapReduceWithProjection, MapReduceWithQuery, MapReduceWithReduceFunction, MapReduceWithMapFunction { + + private final MongoTemplate template; + private final Class domainType; + private final Class returnType; + private final @Nullable String collection; + private final Query query; + private final @Nullable String mapFunction; + private final @Nullable String reduceFunction; + private final @Nullable MapReduceOptions options; + + ExecutableMapReduceSupport(MongoTemplate template, Class domainType, Class returnType, String collection, + Query query, @Nullable String mapFunction, @Nullable String reduceFunction, MapReduceOptions options) { + + this.template = template; + this.domainType = domainType; + this.returnType = returnType; + this.collection = collection; + this.query = query; + this.mapFunction = mapFunction; + this.reduceFunction = reduceFunction; + this.options = options; + } + + /* + * (non-Javascript) + * @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.TerminatingMapReduce#all() + */ + @Override + public List all() { + return template.mapReduce(query, domainType, getCollectionName(), mapFunction, reduceFunction, options, + returnType); + } + + /* + * (non-Javascript) + * @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.MapReduceWithCollection#inCollection(java.lang.String) + */ + @Override + public MapReduceWithProjection inCollection(String collection) { + + Assert.hasText(collection, "Collection name must not be null nor empty!"); + + return new ExecutableMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction, + reduceFunction, options); + } + + /* + * (non-Javascript) + * @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.MapReduceWithQuery#query(org.springframework.data.mongodb.core.query.Query) + */ + @Override + public TerminatingMapReduce matching(Query query) { + + Assert.notNull(query, "Query must not be null!"); + + return new ExecutableMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction, + reduceFunction, options); + } + + /* + * (non-Javascript) + * @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.MapReduceWithProjection#as(java.lang.Class) + */ + @Override + public MapReduceWithQuery as(Class resultType) { + + Assert.notNull(resultType, "ResultType must not be null!"); + + return new ExecutableMapReduceSupport<>(template, domainType, resultType, collection, query, mapFunction, + reduceFunction, options); + } + + /* + * (non-Javascript) + * @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.MapReduceWithOptions#with(org.springframework.data.mongodb.core.mapreduce.MapReduceOptions) + */ + @Override + public ExecutableMapReduce with(MapReduceOptions options) { + + Assert.notNull(options, "Options must not be null! Please consider empty MapReduceOptions#options() instead."); + + return new ExecutableMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction, + reduceFunction, options); + } + + /* + * (non-Javascript) + * @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.MapReduceWithMapFunction#map(java.lang.String) + */ + @Override + public MapReduceWithReduceFunction map(String mapFunction) { + + Assert.hasText(mapFunction, "MapFunction name must not be null nor empty!"); + + return new ExecutableMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction, + reduceFunction, options); + } + + /* + * (non-Javascript) + * @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.MapReduceWithReduceFunction#reduce(java.lang.String) + */ + @Override + public ExecutableMapReduce reduce(String reduceFunction) { + + Assert.hasText(reduceFunction, "ReduceFunction name must not be null nor empty!"); + + return new ExecutableMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction, + reduceFunction, options); + } + + private String getCollectionName() { + return StringUtils.hasText(collection) ? collection : template.determineCollectionName(domainType); + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FluentMongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FluentMongoOperations.java index ece0cb776..ff2812020 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FluentMongoOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FluentMongoOperations.java @@ -22,4 +22,4 @@ package org.springframework.data.mongodb.core; * @since 2.0 */ public interface FluentMongoOperations extends ExecutableFindOperation, ExecutableInsertOperation, - ExecutableUpdateOperation, ExecutableRemoveOperation, ExecutableAggregationOperation {} + ExecutableUpdateOperation, ExecutableRemoveOperation, ExecutableAggregationOperation, ExecutableMapReduceOperation {} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 795464704..421ebffa2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -71,7 +71,16 @@ import org.springframework.data.mongodb.core.aggregation.AggregationResults; import org.springframework.data.mongodb.core.aggregation.Fields; import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext; import org.springframework.data.mongodb.core.aggregation.TypedAggregation; -import org.springframework.data.mongodb.core.convert.*; +import org.springframework.data.mongodb.core.convert.DbRefResolver; +import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver; +import org.springframework.data.mongodb.core.convert.JsonSchemaMapper; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions; +import org.springframework.data.mongodb.core.convert.MongoJsonSchemaMapper; +import org.springframework.data.mongodb.core.convert.MongoWriter; +import org.springframework.data.mongodb.core.convert.QueryMapper; +import org.springframework.data.mongodb.core.convert.UpdateMapper; import org.springframework.data.mongodb.core.index.IndexOperations; import org.springframework.data.mongodb.core.index.IndexOperationsProvider; import org.springframework.data.mongodb.core.index.MongoMappingEventPublisher; @@ -132,7 +141,16 @@ import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoCursor; import com.mongodb.client.MongoDatabase; import com.mongodb.client.MongoIterable; -import com.mongodb.client.model.*; +import com.mongodb.client.model.CountOptions; +import com.mongodb.client.model.CreateCollectionOptions; +import com.mongodb.client.model.DeleteOptions; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.FindOneAndDeleteOptions; +import com.mongodb.client.model.FindOneAndUpdateOptions; +import com.mongodb.client.model.ReturnDocument; +import com.mongodb.client.model.UpdateOptions; +import com.mongodb.client.model.ValidationAction; +import com.mongodb.client.model.ValidationLevel; import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.UpdateResult; import com.mongodb.session.ClientSession; @@ -1798,9 +1816,29 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, public MapReduceResults mapReduce(Query query, String inputCollectionName, String mapFunction, String reduceFunction, @Nullable MapReduceOptions mapReduceOptions, Class entityClass) { + return new MapReduceResults( + mapReduce(query, entityClass, inputCollectionName, mapFunction, reduceFunction, mapReduceOptions, entityClass), + new Document()); + } + + /** + * @param query + * @param domainType + * @param inputCollectionName + * @param mapFunction + * @param reduceFunction + * @param mapReduceOptions + * @param resultType + * @param + * @return + * @since 2.1 + */ + public List mapReduce(Query query, Class domainType, String inputCollectionName, String mapFunction, + String reduceFunction, @Nullable MapReduceOptions mapReduceOptions, Class resultType) { + Assert.notNull(query, "Query must not be null!"); Assert.notNull(inputCollectionName, "InputCollectionName must not be null!"); - Assert.notNull(entityClass, "EntityClass must not be null!"); + Assert.notNull(resultType, "EntityClass must not be null!"); Assert.notNull(reduceFunction, "ReduceFunction must not be null!"); Assert.notNull(mapFunction, "MapFunction must not be null!"); @@ -1818,9 +1856,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, if (query.getMeta() != null && query.getMeta().getMaxTimeMsec() != null) { result = result.maxTime(query.getMeta().getMaxTimeMsec(), TimeUnit.MILLISECONDS); } - result = result.sort(getMappedSortObject(query, entityClass)); + result = result.sort(getMappedSortObject(query, domainType)); - result = result.filter(queryMapper.getMappedObject(query.getQueryObject(), Optional.empty())); + result = result + .filter(queryMapper.getMappedObject(query.getQueryObject(), mappingContext.getPersistentEntity(domainType))); } Optional collation = query.getCollation(); @@ -1856,13 +1895,13 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, result = collation.map(Collation::toMongoCollation).map(result::collation).orElse(result); List mappedResults = new ArrayList(); - DocumentCallback callback = new ReadDocumentCallback(mongoConverter, entityClass, inputCollectionName); + DocumentCallback callback = new ReadDocumentCallback(mongoConverter, resultType, inputCollectionName); for (Document document : result) { mappedResults.add(callback.doWith(document)); } - return new MapReduceResults(mappedResults, new Document()); + return mappedResults; } public GroupByResults group(String inputCollectionName, GroupBy groupBy, Class entityClass) { @@ -2184,6 +2223,15 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, return new ExecutableAggregationOperationSupport(this).aggregateAndReturn(domainType); } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.ExecutableAggregationOperation#aggregateAndReturn(java.lang.Class) + */ + @Override + public ExecutableMapReduce mapReduce(Class domainType) { + return new ExecutableMapReduceOperationSupport(this).mapReduce(domainType); + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.ExecutableInsertOperation#insert(java.lang.Class) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFluentMongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFluentMongoOperations.java index 85ebb5973..15936c65d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFluentMongoOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFluentMongoOperations.java @@ -19,7 +19,8 @@ package org.springframework.data.mongodb.core; * Stripped down interface providing access to a fluent API that specifies a basic set of reactive MongoDB operations. * * @author Mark Paluch + * @author Christoph Strobl * @since 2.0 */ public interface ReactiveFluentMongoOperations extends ReactiveFindOperation, ReactiveInsertOperation, - ReactiveUpdateOperation, ReactiveRemoveOperation, ReactiveAggregationOperation {} + ReactiveUpdateOperation, ReactiveRemoveOperation, ReactiveAggregationOperation, ReactiveMapReduceOperation {} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperation.java new file mode 100644 index 000000000..fca611f74 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperation.java @@ -0,0 +1,199 @@ +/* + * Copyright 2018 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.core; + +import reactor.core.publisher.Flux; + +import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind; +import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; +import org.springframework.data.mongodb.core.query.Query; + +/** + * {@link ReactiveMapReduceOperation} allows creation and execution of MongoDB mapReduce operations in a fluent API + * style. The starting {@literal domainType} is used for mapping an optional {@link Query} provided via {@code matching} + * into the MongoDB specific representation. By default, the originating {@literal domainType} is also used for mapping + * back the results from the {@link org.bson.Document}. However, it is possible to define an different + * {@literal returnType} via {@code as} to mapping the result.
+ * The collection to operate on is by default derived from the initial {@literal domainType} and can be defined there + * via {@link org.springframework.data.mongodb.core.mapping.Document}. Using {@code inCollection} allows to override the + * collection name for the execution. + * + *
+ *     
+ *         mapReduce(Human.class)
+ *             .map("function() { emit(this.id, this.firstname) }")
+ *             .reduce("function(id, name) { return sum(id, name); }")
+ *             .inCollection("star-wars")
+ *             .as(Jedi.class)
+ *             .matching(query(where("lastname").is("skywalker")))
+ *             .all();
+ *     
+ * 
+ * + * @author Christoph Strobl + * @since 2.1 + */ +public interface ReactiveMapReduceOperation { + + /** + * Start creating a mapReduce operation for the given {@literal domainType}. + * + * @param domainType must not be {@literal null}. + * @return new instance of {@link ExecutableFind}. + * @throws IllegalArgumentException if domainType is {@literal null}. + */ + MapReduceWithMapFunction mapReduce(Class domainType); + + /** + * Trigger mapReduce execution by calling one of the terminating methods. + * + * @author Christoph Strobl + * @since 2.1 + */ + interface TerminatingMapReduce { + + /** + * Get the {@link Flux} emitting mapReduce results. + * + * @return a {@link Flux} emitting the already mapped operation results. + */ + Flux all(); + } + + /** + * Provide the Javascript {@code function()} used to map matching documents. + * + * @author Christoph Strobl + * @since 2.1 + */ + interface MapReduceWithMapFunction { + + /** + * Set the Javascript map {@code function()}. + * + * @param mapFunction must not be {@literal null} nor empty. + * @return new instance of {@link MapReduceWithReduceFunction}. + * @throws IllegalArgumentException if {@literal mapFunction} is {@literal null} or empty. + */ + MapReduceWithReduceFunction map(String mapFunction); + + } + + /** + * Provide the Javascript {@code function()} used to reduce matching documents. + * + * @author Christoph Strobl + * @since 2.1 + */ + interface MapReduceWithReduceFunction { + + /** + * Set the Javascript map {@code function()}. + * + * @param reduceFunction must not be {@literal null} nor empty. + * @return new instance of {@link ReactiveMapReduce}. + * @throws IllegalArgumentException if {@literal reduceFunction} is {@literal null} or empty. + */ + ReactiveMapReduce reduce(String reduceFunction); + + } + + /** + * Collection override (Optional). + * + * @author Christoph Strobl + * @since 2.1 + */ + interface MapReduceWithCollection extends MapReduceWithQuery { + + /** + * Explicitly set the name of the collection to perform the mapReduce operation on.
+ * Skip this step to use the default collection derived from the domain type. + * + * @param collection must not be {@literal null} nor {@literal empty}. + * @return new instance of {@link MapReduceWithProjection}. + * @throws IllegalArgumentException if collection is {@literal null}. + */ + MapReduceWithProjection inCollection(String collection); + } + + /** + * Input document filter query (Optional). + * + * @author Christoph Strobl + * @since 2.1 + */ + interface MapReduceWithQuery extends TerminatingMapReduce { + + /** + * Set the filter query to be used. + * + * @param query must not be {@literal null}. + * @return new instance of {@link TerminatingMapReduce}. + * @throws IllegalArgumentException if query is {@literal null}. + */ + TerminatingMapReduce matching(Query query); + } + + /** + * Result type override (Optional). + * + * @author Christoph Strobl + * @since 2.1 + */ + interface MapReduceWithProjection extends MapReduceWithQuery { + + /** + * Define the target type fields should be mapped to.
+ * Skip this step if you are anyway only interested in the original domain type. + * + * @param resultType must not be {@literal null}. + * @param result type. + * @return new instance of {@link TerminatingMapReduce}. + * @throws IllegalArgumentException if resultType is {@literal null}. + */ + MapReduceWithQuery as(Class resultType); + } + + /** + * Additional mapReduce options (Optional). + * + * @author Christoph Strobl + * @since 2.1 + */ + interface MapReduceWithOptions { + + /** + * Set additional options to apply to the mapReduce operation. + * + * @param options must not be {@literal null}. + * @return new instance of {@link ReactiveMapReduce}. + * @throws IllegalArgumentException if options is {@literal null}. + */ + ReactiveMapReduce with(MapReduceOptions options); + } + + /** + * {@link ReactiveMapReduce} provides methods for constructing reactive mapReduce operations in a fluent way. + * + * @author Christoph Strobl + * @since 2.1 + */ + interface ReactiveMapReduce extends MapReduceWithMapFunction, MapReduceWithReduceFunction, + MapReduceWithCollection, MapReduceWithProjection, MapReduceWithOptions { + + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupport.java new file mode 100644 index 000000000..c1a48c4a4 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupport.java @@ -0,0 +1,186 @@ +/* + * Copyright 2018 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.core; + +import reactor.core.publisher.Flux; + +import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Implementation of {@link ReactiveMapReduceOperation}. + * + * @author Christoph Strobl + * @since 2.1 + */ +class ReactiveMapReduceOperationSupport implements ReactiveMapReduceOperation { + + private static final Query ALL_QUERY = new Query(); + + private final ReactiveMongoTemplate template; + + /** + * Create new {@link ReactiveMapReduceOperationSupport}. + * + * @param template must not be {@literal null}. + * @throws IllegalArgumentException if template is {@literal null}. + */ + ReactiveMapReduceOperationSupport(ReactiveMongoTemplate template) { + + Assert.notNull(template, "Template must not be null!"); + this.template = template; + } + + /* + * (non-Javascript) + * @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation#mapReduce(java.lang.Class) + */ + @Override + public ReactiveMapReduceSupport mapReduce(Class domainType) { + + Assert.notNull(domainType, "DomainType must not be null!"); + + return new ReactiveMapReduceSupport(template, domainType, domainType, null, ALL_QUERY, null, null, null); + } + + /** + * @author Christoph Strobl + * @since 2.1 + */ + static class ReactiveMapReduceSupport + implements ReactiveMapReduce, MapReduceWithOptions, MapReduceWithCollection, MapReduceWithProjection, + MapReduceWithQuery, MapReduceWithReduceFunction, MapReduceWithMapFunction { + + private final ReactiveMongoTemplate template; + private final Class domainType; + private final Class returnType; + private final @Nullable String collection; + private final Query query; + private final @Nullable String mapFunction; + private final @Nullable String reduceFunction; + private final @Nullable MapReduceOptions options; + + ReactiveMapReduceSupport(ReactiveMongoTemplate template, Class domainType, Class returnType, + String collection, Query query, @Nullable String mapFunction, @Nullable String reduceFunction, + MapReduceOptions options) { + + this.template = template; + this.domainType = domainType; + this.returnType = returnType; + this.collection = collection; + this.query = query; + this.mapFunction = mapFunction; + this.reduceFunction = reduceFunction; + this.options = options; + } + + /* + * (non-Javascript) + * @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.TerminatingMapReduce#all() + */ + @Override + public Flux all() { + + return template.mapReduce(query, domainType, getCollectionName(), returnType, mapFunction, reduceFunction, + options); + } + + /* + * (non-Javascript) + * @see in org.springframework.data.mongodb.core.ReactiveMapReduceOperation.MapReduceWithCollection#inCollection(java.lang.String) + */ + @Override + public MapReduceWithProjection inCollection(String collection) { + + Assert.hasText(collection, "Collection name must not be null nor empty!"); + + return new ReactiveMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction, + reduceFunction, options); + } + + /* + * (non-Javascript) + * @see in org.springframework.data.mongodb.core.ReactiveMapReduceOperation.MapReduceWithQuery#query(org.springframework.data.mongodb.core.query.Query) + */ + @Override + public TerminatingMapReduce matching(Query query) { + + Assert.notNull(query, "Query must not be null!"); + + return new ReactiveMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction, + reduceFunction, options); + } + + /* + * (non-Javascript) + * @see in org.springframework.data.mongodb.core.ReactiveMapReduceOperation.MapReduceWithProjection#as(java.lang.Class) + */ + @Override + public MapReduceWithQuery as(Class resultType) { + + Assert.notNull(resultType, "ResultType must not be null!"); + + return new ReactiveMapReduceSupport<>(template, domainType, resultType, collection, query, mapFunction, + reduceFunction, options); + } + + /* + * (non-Javascript) + * @see in org.springframework.data.mongodb.core.ReactiveMapReduceOperation.MapReduceWithOptions#with(org.springframework.data.mongodb.core.mapreduce.MapReduceOptions) + */ + @Override + public ReactiveMapReduce with(MapReduceOptions options) { + + Assert.notNull(options, "Options must not be null! Please consider empty MapReduceOptions#options() instead."); + + return new ReactiveMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction, + reduceFunction, options); + } + + /* + * (non-Javascript) + * @see in org.springframework.data.mongodb.core.ReactiveMapReduceOperation.MapReduceWithMapFunction#map(java.lang.String) + */ + @Override + public MapReduceWithReduceFunction map(String mapFunction) { + + Assert.hasText(mapFunction, "MapFunction name must not be null nor empty!"); + + return new ReactiveMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction, + reduceFunction, options); + } + + /* + * (non-Javascript) + * @see in org.springframework.data.mongodb.core.ReactiveMapReduceOperation.MapReduceWithReduceFunction#reduce(java.lang.String) + */ + @Override + public ReactiveMapReduce reduce(String reduceFunction) { + + Assert.hasText(reduceFunction, "ReduceFunction name must not be null nor empty!"); + + return new ReactiveMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction, + reduceFunction, options); + } + + private String getCollectionName() { + return StringUtils.hasText(collection) ? collection : template.determineCollectionName(domainType); + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index a53ef8168..e2fd433ea 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -2001,7 +2001,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati if (filterQuery.getLimit() > 0 || (options.getLimit() != null)) { if (filterQuery.getLimit() > 0 && (options.getLimit() != null)) { - throw new IllegalArgumentException("Both Query and MapReduceOptions define a limit. Please provide the limit only via one of the two."); + throw new IllegalArgumentException( + "Both Query and MapReduceOptions define a limit. Please provide the limit only via one of the two."); } if (filterQuery.getLimit() > 0) { @@ -2105,6 +2106,15 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati return new ReactiveAggregationOperationSupport(this).aggregateAndReturn(domainType); } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.ReactiveMapReduceOperation#mapReduce(java.lang.Class) + */ + @Override + public ReactiveMapReduce mapReduce(Class domainType) { + return new ReactiveMapReduceOperationSupport(this).mapReduce(domainType); + } + /** * Retrieve and remove all documents matching the given {@code query} by calling {@link #find(Query, Class, String)} * and {@link #remove(Query, Class, String)}, whereas the {@link Query} for {@link #remove(Query, Class, String)} is diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupportUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupportUnitTests.java new file mode 100644 index 000000000..a68b72a16 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupportUnitTests.java @@ -0,0 +1,141 @@ +/* + * Copyright 2018 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.core; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.*; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; +import org.springframework.data.mongodb.core.query.BasicQuery; +import org.springframework.data.mongodb.core.query.Query; + +/** + * Unit tests for {@link ExecutableMapReduceOperationSupport}. + * + * @author Christoph Strobl + * @currentRead Beyond the Shadows - Brent Weeks + */ +@RunWith(MockitoJUnitRunner.class) +public class ExecutableMapReduceOperationSupportUnitTests { + + private static final String STAR_WARS = "star-wars"; + private static final String MAP_FUNCTION = "function() { emit(this.id, this.firstname) }"; + private static final String REDUCE_FUNCTION = "function(id, name) { return sum(id, name); }"; + + @Mock MongoTemplate template; + + ExecutableMapReduceOperationSupport mapReduceOpsSupport; + + @Before + public void setUp() { + + when(template.determineCollectionName(eq(Person.class))).thenReturn(STAR_WARS); + + mapReduceOpsSupport = new ExecutableMapReduceOperationSupport(template); + } + + @Test(expected = IllegalArgumentException.class) // DATAMONGO-1929 + public void throwsExceptionOnNullTemplate() { + new ExecutableMapReduceOperationSupport(null); + } + + @Test(expected = IllegalArgumentException.class) // DATAMONGO-1929 + public void throwsExceptionOnNullDomainType() { + mapReduceOpsSupport.mapReduce(null); + } + + @Test // DATAMONGO-1929 + public void usesExtractedCollectionName() { + + mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION).all(); + + verify(template).mapReduce(any(Query.class), eq(Person.class), eq(STAR_WARS), eq(MAP_FUNCTION), eq(REDUCE_FUNCTION), + isNull(), eq(Person.class)); + } + + @Test // DATAMONGO-1929 + public void usesExplicitCollectionName() { + + mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION) + .inCollection("the-night-angel").all(); + + verify(template).mapReduce(any(Query.class), eq(Person.class), eq("the-night-angel"), eq(MAP_FUNCTION), + eq(REDUCE_FUNCTION), isNull(), eq(Person.class)); + } + + @Test // DATAMONGO-1929 + public void usesMapReduceOptionsWhenPresent() { + + MapReduceOptions options = MapReduceOptions.options(); + mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION).with(options).all(); + + verify(template).mapReduce(any(Query.class), eq(Person.class), eq(STAR_WARS), eq(MAP_FUNCTION), eq(REDUCE_FUNCTION), + eq(options), eq(Person.class)); + } + + @Test // DATAMONGO-1929 + public void usesQueryWhenPresent() { + + Query query = new BasicQuery("{ 'lastname' : 'skywalker' }"); + mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION).matching(query).all(); + + verify(template).mapReduce(eq(query), eq(Person.class), eq(STAR_WARS), eq(MAP_FUNCTION), eq(REDUCE_FUNCTION), + isNull(), eq(Person.class)); + } + + @Test // DATAMONGO-1929 + public void usesProjectionWhenPresent() { + + mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION).as(Jedi.class).all(); + + verify(template).mapReduce(any(Query.class), eq(Person.class), eq(STAR_WARS), eq(MAP_FUNCTION), eq(REDUCE_FUNCTION), + isNull(), eq(Jedi.class)); + } + + interface Contact {} + + @Data + @org.springframework.data.mongodb.core.mapping.Document(collection = STAR_WARS) + static class Person implements Contact { + + @Id String id; + String firstname; + String lastname; + Object ability; + Person father; + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + static class Jedi { + + @Field("firstname") String name; + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupportUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupportUnitTests.java new file mode 100644 index 000000000..3f3209721 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupportUnitTests.java @@ -0,0 +1,141 @@ +/* + * Copyright 2018 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.core; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.*; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; +import org.springframework.data.mongodb.core.query.BasicQuery; +import org.springframework.data.mongodb.core.query.Query; + +/** + * Unit tests for {@link ReactiveMapReduceOperationSupport}. + * + * @author Christoph Strobl + * @currentRead Beyond the Shadows - Brent Weeks + */ +@RunWith(MockitoJUnitRunner.class) +public class ReactiveMapReduceOperationSupportUnitTests { + + private static final String STAR_WARS = "star-wars"; + private static final String MAP_FUNCTION = "function() { emit(this.id, this.firstname) }"; + private static final String REDUCE_FUNCTION = "function(id, name) { return sum(id, name); }"; + + @Mock ReactiveMongoTemplate template; + + ReactiveMapReduceOperationSupport mapReduceOpsSupport; + + @Before + public void setUp() { + + when(template.determineCollectionName(eq(Person.class))).thenReturn(STAR_WARS); + + mapReduceOpsSupport = new ReactiveMapReduceOperationSupport(template); + } + + @Test(expected = IllegalArgumentException.class) // DATAMONGO-1929 + public void throwsExceptionOnNullTemplate() { + new ExecutableMapReduceOperationSupport(null); + } + + @Test(expected = IllegalArgumentException.class) // DATAMONGO-1929 + public void throwsExceptionOnNullDomainType() { + mapReduceOpsSupport.mapReduce(null); + } + + @Test // DATAMONGO-1929 + public void usesExtractedCollectionName() { + + mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION).all(); + + verify(template).mapReduce(any(Query.class), eq(Person.class), eq(STAR_WARS), eq(Person.class), eq(MAP_FUNCTION), + eq(REDUCE_FUNCTION), isNull()); + } + + @Test // DATAMONGO-1929 + public void usesExplicitCollectionName() { + + mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION) + .inCollection("the-night-angel").all(); + + verify(template).mapReduce(any(Query.class), eq(Person.class), eq("the-night-angel"), eq(Person.class), + eq(MAP_FUNCTION), eq(REDUCE_FUNCTION), isNull()); + } + + @Test // DATAMONGO-1929 + public void usesMapReduceOptionsWhenPresent() { + + MapReduceOptions options = MapReduceOptions.options(); + mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION).with(options).all(); + + verify(template).mapReduce(any(Query.class), eq(Person.class), eq(STAR_WARS), eq(Person.class), eq(MAP_FUNCTION), + eq(REDUCE_FUNCTION), eq(options)); + } + + @Test // DATAMONGO-1929 + public void usesQueryWhenPresent() { + + Query query = new BasicQuery("{ 'lastname' : 'skywalker' }"); + mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION).matching(query).all(); + + verify(template).mapReduce(eq(query), eq(Person.class), eq(STAR_WARS), eq(Person.class), eq(MAP_FUNCTION), + eq(REDUCE_FUNCTION), isNull()); + } + + @Test // DATAMONGO-1929 + public void usesProjectionWhenPresent() { + + mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION).as(Jedi.class).all(); + + verify(template).mapReduce(any(Query.class), eq(Person.class), eq(STAR_WARS), eq(Jedi.class), eq(MAP_FUNCTION), + eq(REDUCE_FUNCTION), isNull()); + } + + interface Contact {} + + @Data + @org.springframework.data.mongodb.core.mapping.Document(collection = STAR_WARS) + static class Person implements Contact { + + @Id String id; + String firstname; + String lastname; + Object ability; + Person father; + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + static class Jedi { + + @Field("firstname") String name; + } +}