/*
* Copyright 2016-2017 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.aggregation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.bson.Document;
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation;
import org.springframework.data.mongodb.core.query.CriteriaDefinition;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Encapsulates the aggregation framework {@code $graphLookup}-operation. <br />
* Performs a recursive search on a collection, with options for restricting the search by recursion depth and query
* filter. <br />
* We recommend to use the static factory method {@link Aggregation#graphLookup(String)} instead of creating instances
* of this class directly.
*
* @see <a href=
* "https://docs.mongodb.org/manual/reference/aggregation/graphLookup/">https://docs.mongodb.org/manual/reference/aggregation/graphLookup/</a>
* @author Mark Paluch
* @author Christoph Strobl
* @since 1.10
*/
public class GraphLookupOperation implements InheritsFieldsAggregationOperation {
private static final Set<Class<?>> ALLOWED_START_TYPES = new HashSet<Class<?>>(
Arrays.<Class<?>> asList(AggregationExpression.class, String.class, Field.class, Document.class));
private final String from;
private final List<Object> startWith;
private final Field connectFrom;
private final Field connectTo;
private final Field as;
private final Long maxDepth;
private final Field depthField;
private final CriteriaDefinition restrictSearchWithMatch;
private GraphLookupOperation(String from, List<Object> startWith, Field connectFrom, Field connectTo, Field as,
Long maxDepth, Field depthField, CriteriaDefinition restrictSearchWithMatch) {
this.from = from;
this.startWith = startWith;
this.connectFrom = connectFrom;
this.connectTo = connectTo;
this.as = as;
this.maxDepth = maxDepth;
this.depthField = depthField;
this.restrictSearchWithMatch = restrictSearchWithMatch;
}
/**
* Creates a new {@link FromBuilder} to build {@link GraphLookupOperation}.
*
* @return a new {@link FromBuilder}.
*/
public static FromBuilder builder() {
return new GraphLookupOperationFromBuilder();
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
*/
@Override
public Document toDocument(AggregationOperationContext context) {
Document graphLookup = new Document();
graphLookup.put("from", from);
List<Object> mappedStartWith = new ArrayList<Object>(startWith.size());
for (Object startWithElement : startWith) {
if (startWithElement instanceof AggregationExpression) {
mappedStartWith.add(((AggregationExpression) startWithElement).toDocument(context));
} else if (startWithElement instanceof Field) {
mappedStartWith.add(context.getReference((Field) startWithElement).toString());
} else {
mappedStartWith.add(startWithElement);
}
}
graphLookup.put("startWith", mappedStartWith.size() == 1 ? mappedStartWith.iterator().next() : mappedStartWith);
graphLookup.put("connectFromField", connectFrom.getName());
graphLookup.put("connectToField", connectTo.getName());
graphLookup.put("as", as.getName());
if (maxDepth != null) {
graphLookup.put("maxDepth", maxDepth);
}
if (depthField != null) {
graphLookup.put("depthField", depthField.getName());
}
if (restrictSearchWithMatch != null) {
graphLookup.put("restrictSearchWithMatch", context.getMappedObject(restrictSearchWithMatch.getCriteriaObject()));
}
return new Document("$graphLookup", graphLookup);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation#getFields()
*/
@Override
public ExposedFields getFields() {
return ExposedFields.from(new ExposedField(as, true));
}
/**
* @author Mark Paluch
*/
public interface FromBuilder {
/**
* Set the {@literal collectionName} to apply the {@code $graphLookup} to.
*
* @param collectionName must not be {@literal null} or empty.
* @return
*/
StartWithBuilder from(String collectionName);
}
/**
* @author Mark Paluch
* @author Christoph Strobl
*/
public interface StartWithBuilder {
/**
* Set the startWith {@literal fieldReferences} to apply the {@code $graphLookup} to.
*
* @param fieldReferences must not be {@literal null}.
* @return
*/
ConnectFromBuilder startWith(String... fieldReferences);
/**
* Set the startWith {@literal expressions} to apply the {@code $graphLookup} to.
*
* @param expressions must not be {@literal null}.
* @return
*/
ConnectFromBuilder startWith(AggregationExpression... expressions);
/**
* Set the startWith as either {@literal fieldReferences}, {@link Fields}, {@link Document} or
* {@link AggregationExpression} to apply the {@code $graphLookup} to.
*
* @param expressions must not be {@literal null}.
* @return
* @throws IllegalArgumentException
*/
ConnectFromBuilder startWith(Object... expressions);
}
/**
* @author Mark Paluch
*/
public interface ConnectFromBuilder {
/**
* Set the connectFrom {@literal fieldName} to apply the {@code $graphLookup} to.
*
* @param fieldName must not be {@literal null} or empty.
* @return
*/
ConnectToBuilder connectFrom(String fieldName);
}
/**
* @author Mark Paluch
*/
public interface ConnectToBuilder {
/**
* Set the connectTo {@literal fieldName} to apply the {@code $graphLookup} to.
*
* @param fieldName must not be {@literal null} or empty.
* @return
*/
GraphLookupOperationBuilder connectTo(String fieldName);
}
/**
* Builder to build the initial {@link GraphLookupOperationBuilder} that configures the initial mandatory set of
* {@link GraphLookupOperation} properties.
*
* @author Mark Paluch
*/
static final class GraphLookupOperationFromBuilder
implements FromBuilder, StartWithBuilder, ConnectFromBuilder, ConnectToBuilder {
private String from;
private List<? extends Object> startWith;
private String connectFrom;
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.GraphLookupOperation.FromBuilder#from(java.lang.String)
*/
@Override
public StartWithBuilder from(String collectionName) {
Assert.hasText(collectionName, "CollectionName must not be null or empty!");
this.from = collectionName;
return this;
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.GraphLookupOperation.StartWithBuilder#startWith(java.lang.String[])
*/
@Override
public ConnectFromBuilder startWith(String... fieldReferences) {
Assert.notNull(fieldReferences, "FieldReferences must not be null!");
Assert.noNullElements(fieldReferences, "FieldReferences must not contain null elements!");
List<Object> fields = new ArrayList<Object>(fieldReferences.length);
for (String fieldReference : fieldReferences) {
fields.add(Fields.field(fieldReference));
}
this.startWith = fields;
return this;
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.GraphLookupOperation.StartWithBuilder#startWith(org.springframework.data.mongodb.core.aggregation.AggregationExpression[])
*/
@Override
public ConnectFromBuilder startWith(AggregationExpression... expressions) {
Assert.notNull(expressions, "AggregationExpressions must not be null!");
Assert.noNullElements(expressions, "AggregationExpressions must not contain null elements!");
this.startWith = Arrays.asList(expressions);
return this;
}
@Override
public ConnectFromBuilder startWith(Object... expressions) {
Assert.notNull(expressions, "Expressions must not be null!");
Assert.noNullElements(expressions, "Expressions must not contain null elements!");
this.startWith = verifyAndPotentiallyTransformStartsWithTypes(expressions);
return this;
}
private List<Object> verifyAndPotentiallyTransformStartsWithTypes(Object... expressions) {
List<Object> expressionsToUse = new ArrayList<Object>(expressions.length);
for (Object expression : expressions) {
assertStartWithType(expression);
if (expression instanceof String) {
expressionsToUse.add(Fields.field((String) expression));
} else {
expressionsToUse.add(expression);
}
}
return expressionsToUse;
}
private void assertStartWithType(Object expression) {
for (Class<?> type : ALLOWED_START_TYPES) {
if (ClassUtils.isAssignable(type, expression.getClass())) {
return;
}
}
throw new IllegalArgumentException(
String.format("Expression must be any of %s but was %s", ALLOWED_START_TYPES, expression.getClass()));
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.GraphLookupOperation.ConnectFromBuilder#connectFrom(java.lang.String)
*/
@Override
public ConnectToBuilder connectFrom(String fieldName) {
Assert.hasText(fieldName, "ConnectFrom must not be null or empty!");
this.connectFrom = fieldName;
return this;
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.GraphLookupOperation.ConnectToBuilder#connectTo(java.lang.String)
*/
@Override
public GraphLookupOperationBuilder connectTo(String fieldName) {
Assert.hasText(fieldName, "ConnectTo must not be null or empty!");
return new GraphLookupOperationBuilder(from, startWith, connectFrom, fieldName);
}
}
/**
* @author Mark Paluch
*/
public static final class GraphLookupOperationBuilder {
private final String from;
private final List<Object> startWith;
private final Field connectFrom;
private final Field connectTo;
private Long maxDepth;
private Field depthField;
private CriteriaDefinition restrictSearchWithMatch;
protected GraphLookupOperationBuilder(String from, List<? extends Object> startWith, String connectFrom,
String connectTo) {
this.from = from;
this.startWith = new ArrayList<Object>(startWith);
this.connectFrom = Fields.field(connectFrom);
this.connectTo = Fields.field(connectTo);
}
/**
* Optionally limit the number of recursions.
*
* @param numberOfRecursions must be greater or equal to zero.
* @return
*/
public GraphLookupOperationBuilder maxDepth(long numberOfRecursions) {
Assert.isTrue(numberOfRecursions >= 0, "Max depth must be >= 0!");
this.maxDepth = numberOfRecursions;
return this;
}
/**
* Optionally add a depth field {@literal fieldName} to each traversed document in the search path.
*
* @param fieldName must not be {@literal null} or empty.
* @return
*/
public GraphLookupOperationBuilder depthField(String fieldName) {
Assert.hasText(fieldName, "Depth field name must not be null or empty!");
this.depthField = Fields.field(fieldName);
return this;
}
/**
* Optionally add a query specifying conditions to the recursive search.
*
* @param criteriaDefinition must not be {@literal null}.
* @return
*/
public GraphLookupOperationBuilder restrict(CriteriaDefinition criteriaDefinition) {
Assert.notNull(criteriaDefinition, "CriteriaDefinition must not be null!");
this.restrictSearchWithMatch = criteriaDefinition;
return this;
}
/**
* Set the name of the array field added to each output document and return the final {@link GraphLookupOperation}.
* Contains the documents traversed in the {@literal $graphLookup} stage to reach the document.
*
* @param fieldName must not be {@literal null} or empty.
* @return the final {@link GraphLookupOperation}.
*/
public GraphLookupOperation as(String fieldName) {
Assert.hasText(fieldName, "As field name must not be null or empty!");
return new GraphLookupOperation(from, startWith, connectFrom, connectTo, Fields.field(fieldName), maxDepth,
depthField, restrictSearchWithMatch);
}
}
}