Compare commits

...

32 Commits

Author SHA1 Message Date
Spring Buildmaster
d861fecdb8 DATAMONGO-981 - Release version 1.6.0.M1. 2014-07-10 10:38:55 -07:00
Oliver Gierke
f280e23095 DATAMONGO-981 - Prepare 1.6.0.M1 (Evans M1). 2014-07-10 19:28:34 +02:00
Oliver Gierke
ed0e1d92c0 DATAMONGO-981 - Updated changelog. 2014-07-10 17:14:24 +02:00
Christoph Strobl
d82fc22659 DATAMONGO-944 - Add support for $currentDate to Update.
Added currentDate and currentTimestamp to Update.

Original pull request: #200.
2014-07-10 15:13:59 +02:00
Thomas Darimont
6616d6788c DATAMONGO-975 - Add support for extracting date/time components from a field projection.
We added some extract-methods to ProjectionOperationBuilder to be able to extract date / time components from projected fields.

Original pull request: #204.
2014-07-10 12:45:17 +02:00
Christoph Strobl
322a7cf033 DATAMONGO-969 - Fixed nested id handling in SpringDataMongodbSerializer.
SpringDataMongodbSerializer now defensively triggers mapping of the DBObject created by the default serializer. This asserts that ids buried in nested structures like { "_id" : { "$in" : ["x", "y"] } } are converted correctly.

Original pull request: #202.
2014-07-09 21:48:02 +02:00
Christoph Strobl
0f487c10ba DATAMONGO-983 - Remove links to forum.spring.io.
Replace forum links with those to stackoverflow.

Original Pull Request: #205
2014-07-09 21:21:04 +02:00
Christoph Strobl
11417144bd DATAMONGO-980 - Use meta annotations from commons for @Score.
We now use Spring Data Commons' @ReadOnlyProperty to meta-annotate @Score to mark it as read-only property.

Original pull request: #201.
Related tickets: DATACMNS-534.
2014-07-09 14:51:21 +02:00
Christoph Strobl
dafc59b163 DATAMONGO-972 - Querydsl integration now handles references correctly.
SpringDataMongodbSerializer now overrides the necessary methods to create the appropriate DBRef objects when serializing data via Querydsl.

We currently disable the test case as it the fix taking effect requires Querydsl 3.4.1 which unfortunately breaks Java 6 compatibility. We include the fix nonetheless to allow users on Java 7 to potentially use the latest Querydsl.

Original pull request: #203.
Related tickets: querydsl/querydsl#803.
2014-07-09 13:47:04 +02:00
Oliver Gierke
566f9a80c4 DATAMONGO-982 - Added build profiles to build against next MongoDB driver versions.
Added build profile for MongoDB Java driver versions 2.12.3-SNAPSHOT and 3.0.0-SNAPSHOT. Added another property to be able to build manifests correctly as the snapshot versions aren't valid OSGi versions.

Adapted MongoExceptionTranslator to convert the new Exceptions being thrown for server timeouts and the deprecated values we currently handle.
2014-07-08 17:36:01 +02:00
Christoph Strobl
89a42c5648 DATAMONGO-976 - Add support for reading $meta projection on textScore into document.
We introduced @TextScore that can be used to mark a property to take { $meta : “textScore” }. In contrast to @Transient the value will be considered when reading documents.
The value can and will only get picked up if the score field is retrieved from the store.

Original pull request: #198.
2014-07-07 11:43:43 +02:00
Christoph Strobl
83ffbb00e8 DATAMONGO-850 - Add support for full text search via $text
Using TextQuery and TextCriteria allows creation of queries using $text $search.

{ $meta : “textScore” } can be included using TextQuery.includeScore. As the fieldname used for textScore must not be fixed to “score” one can use the overload taking the fieldname to override the default.

Original pull request: #198.
2014-07-07 11:39:47 +02:00
Christoph Strobl
84913cecab DATAMONGO-937 - Add support for creating text index.
We now support creating text index based on information gathered on domain types.

Using @TextIndexed marks properties to be considered for the full text index. Use the weight attribute to influence document scoring during search operations.

Please note that using @TextIndexed on entity properties forces all properties of any sub document to be considered as part of the text index. Any set weight will in that case be propagated to the siblings taking the most recent weight information into account, which means that a the weight attribute can be overridden for properties in sub documents.

The setting the index default language can be done via @Document(language) while @Language can be used to define the language_override field.

As text search is disabled by default for mongodb 2.4 we use a jUnit ClassRule to restrict integration tests potentially creating text index (as the entities for testing are found in the classpath) to only be executed in when a 2.6+ mongodb server is present.

For usage hints please see section 6.3.4 (Text Indexes) of reference manual.

Original pull request: #198.
2014-07-07 11:30:08 +02:00
Christoph Strobl
998bb09a92 DATAMONGO-978 - Derived delete query should pass on type information.
We now pass on type information for derived delete queries to the according delete operation. This propagates the information correctly to the according Before and After events.

Before this change the type would have been set to null in case of non collection like method return type.

Original pull request: #199.
2014-07-03 14:16:57 +02:00
Oliver Gierke
cd68a8db54 DATAMONGO-977 - Removed reflective detection of Spring 4 in DBRef proxy creation.
After the Spring 4 upgrade we can now directly use the Objenesis infrastructure of it.
2014-07-02 09:23:19 +02:00
Oliver Gierke
df8477d180 DATAMONGO-955 - Updated changelog. 2014-06-30 10:57:12 +02:00
Christoph Strobl
244fbae0ce DATAMONGO-962 - Cycle guard should respect full path.
We now check on intersections of given path and existing to not only check types and contained property names but also properties full path which must not be present in already traversed paths.

Additionally we’ll now catch any CyclicPropertyReferenceExceptions on the root level to prevent cycle detection interfering with application startup.

Original pull request: #197.
2014-06-27 19:25:26 +02:00
Oliver Gierke
19e08a52c0 DATAMONGO-970 - MongoTemplate.remove(…) now correctly builds query for DBObjects.
If a DBObject was handed into MongoTemplate.remove(…) we previously failed to look up the id value to create a by-id-query. This commit adds explicit handling of DBObjects by looking up their _id field to obtain the id value.
2014-06-27 16:12:50 +02:00
Thomas Darimont
6389b1bb73 DATAMONGO-954 - Add support for system variables in aggregation operations.
Add assumes for appropriate MongoDB version.
2014-06-26 14:19:24 +02:00
Thomas Darimont
cadcbf6106 DATAMONGO-954 - Add support for system variables in aggregation operations.
We now support referring to system variables like for instance $$ROOT or $$CURRENT from within aggregation framework pipeline projection and group expressions.

Original pull request: #190.
2014-06-25 15:53:44 +02:00
Christoph Strobl
118f007ca6 DATAMONGO-963 - @CompoundIndex should treat expireAfterSeconds correctly.
We added an additional check on the fields used as key, so that TTL is ignored for CompoundIndex with more than one field (which effectively renders it useless on @CompoundIndex at all).

Prior to this change potentially invalid index structures would have been created for e.g. @CompoundIndex(def = "{'foo': 1, 'bar': 1}", expireAfterSeconds=10) leading to MongoDB not being able to clean up the indexes (logs: "ERROR: key for ttl index can only have 1 field")

This fix is related to https://jira.mongodb.org/browse/SERVER-10075.

Original pull request: #196.
2014-06-25 15:12:32 +02:00
Christoph Strobl
cbb32bd29d DATAMONGO-950 - Add support for limiting the query result in the query derivation mechanism.
When deriving the query from its method name we check for the limit set on the PartTree to pass this on to the created query. PagedExecution not takes the overall limit into account, skips a query execution entirely (if the Pageable is out of scope completely) or alters the query limits accordingly.

Note, that there has been significant rework of this compared to the pull request to avoid new API in Query and extensive changes in MongoTemplate's QueryCursorPreparer.

Original pull request: #191.
2014-06-25 14:58:59 +02:00
Thomas Darimont
9858dcd740 DATAMONGO-960 - Allow to pass options to the Aggregation Pipeline.
We introduced AggregationOptions abstraction to conveniently construct option objects that can be handed to an Aggregation via the new Aggregation.withOptions(...) factory method. For more details, see the Builder class' JavaDoc.

Note that we exposed the "rawResults" in AggregationResults and put a null guard in MongoTemplate aggregate in order to support the "explain" option.

Original pull request: #195.
2014-06-25 13:25:57 +02:00
Thomas Darimont
1fb76d135b DATAMONGO-953 - Add equals(…)/hashCode()/toString() to Update.
We now use the underlying updateObject to implement appropriate equals(…)/hashCode() and toString() methods.

Original pull request: #192.
2014-06-25 12:40:30 +02:00
Oliver Gierke
bb62c8b2f1 DATAMONGO-958 - Switch to FieldNamingStrategy SPI in Spring Data Commons. 2014-06-20 21:27:24 +02:00
Christoph Strobl
2cbe7bf885 DATAMONGO-952 - Derived queries should consider field specification in @Query.
PartTreeMongoQuery now explicitly check the presence of a manually defined field spec on the query method and creates a new Query if so.

Original pull request: #188.
2014-06-18 12:53:14 +02:00
Christoph Strobl
6043f6b74d DATAMONGO-949 - CycleGuard should only match properties in word boundaries.
We modified the regular expression used for cycle detection to match on the exact property name within the inspected path using word boundaries. This fix prevents sub sequences of an existing property (like ‘sub’ would have matched ‘substr’) from being matched.

Along the way we fixed the (false) assertion in one of the tests, as we create the +1 cycle reference index before actually breaking the operation.
2014-06-18 08:32:30 +02:00
Christoph Strobl
ef1366592a DATAMONGO-948 - Sort should be taken as is when no type information available.
Object type mapping for sort is skipped in the case no type information is present when executing query using mongo template.
2014-06-18 08:27:57 +02:00
Thomas Darimont
01cf9fb8f3 DATAMONGO-938 - Apply QueryMapper in MongoTemplate.mapReduce(…).
Previously MongoTemplate.mapReduce(...) didn't translate nested objects, e.g. GeoCommand, within the given query. That could lead to exceptions during query serialization. We now pass the query and sort object of the given Query through the QueryMapper to avoid such problems.

Original pull request: #184.
2014-06-18 08:27:54 +02:00
Thomas Darimont
285c406d5d DATAMONGO-745 - Added test cases for custom query with $in and pageable parameter.
Added test cases to verify that this works.

Original pull request: #186.
2014-06-18 07:49:06 +02:00
Oliver Gierke
ad29e52a57 DATAMONGO-936 - After release cleanups. 2014-05-20 19:54:03 +02:00
Spring Buildmaster
3cfe207c83 DATAMONGO-936 - Prepare next development iteration. 2014-05-20 09:35:38 -07:00
90 changed files with 5341 additions and 668 deletions

View File

@@ -11,7 +11,7 @@ For a comprehensive treatment of all the Spring Data MongoDB features, please re
* the [User Guide](http://docs.spring.io/spring-data/mongodb/docs/current/reference/html/)
* the [JavaDocs](http://docs.spring.io/spring-data/mongodb/docs/current/api/) have extensive comments in them as well.
* the home page of [Spring Data MongoDB](http://projects.spring.io/spring-data-mongodb) contains links to articles and other resources.
* for more detailed questions, use the [forum](http://forum.spring.io/forum/spring-projects/data/nosql).
* for more detailed questions, use [Spring Data Mongodb on Stackoverflow](http://stackoverflow.com/questions/tagged/spring-data-mongodb).
If you are new to Spring as well as to Spring Data, look for information about [Spring projects](http://projects.spring.io/).
@@ -139,7 +139,7 @@ public class MyService {
Here are some ways for you to get involved in the community:
* Get involved with the Spring community on the Spring Community Forums. Please help out on the [forum](http://forum.spring.io/forum/spring-projects/data/nosql) by responding to questions and joining the debate.
* Get involved with the Spring community on Stackoverflow and help out on the [spring-data-mongodb](http://stackoverflow.com/questions/tagged/spring-data-mongodb) tag by responding to questions and joining the debate.
* Create [JIRA](https://jira.springframework.org/browse/DATADOC) tickets for bugs and new features and comment and vote on the ones that you are interested in.
* Github is for social coding: if you want to write code, we encourage contributions through pull requests from [forks of this repository](http://help.github.com/forking/). If you want to contribute code this way, please reference a JIRA ticket as well covering the specific issue you are addressing.
* Watch for upcoming articles on Spring by [subscribing](http://spring.io/blog) to spring.io.

46
pom.xml
View File

@@ -2,10 +2,10 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>1.5.0.RELEASE</version>
<version>1.6.0.M1</version>
<packaging>pom</packaging>
<name>Spring Data MongoDB</name>
@@ -15,7 +15,7 @@
<parent>
<groupId>org.springframework.data.build</groupId>
<artifactId>spring-data-parent</artifactId>
<version>1.4.0.RELEASE</version>
<version>1.5.0.M1</version>
<relativePath>../spring-data-build/parent/pom.xml</relativePath>
</parent>
@@ -29,10 +29,11 @@
<properties>
<project.type>multi</project.type>
<dist.id>spring-data-mongodb</dist.id>
<springdata.commons>1.8.0.RELEASE</springdata.commons>
<springdata.commons>1.9.0.M1</springdata.commons>
<mongo>2.12.1</mongo>
<mongo.osgi>2.12.1</mongo.osgi>
</properties>
<developers>
<developer>
<id>ogierke</id>
@@ -104,10 +105,35 @@
<profiles>
<profile>
<id>mongo-next</id>
<properties>
<mongo>2.12.0</mongo>
<mongo>2.12.3-SNAPSHOT</mongo>
</properties>
<repositories>
<repository>
<id>mongo-snapshots</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</repository>
</repositories>
</profile>
<profile>
<id>mongo-3-next</id>
<properties>
<mongo>3.0.0-SNAPSHOT</mongo>
</properties>
<repositories>
<repository>
<id>mongo-snapshots</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</repository>
</repositories>
</profile>
</profiles>
@@ -119,14 +145,14 @@
<version>${mongo}</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-libs-release</id>
<url>http://repo.spring.io/libs-release</url>
<id>spring-libs-milestone</id>
<url>http://repo.spring.io/libs-milestone</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-plugins-release</id>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>1.5.0.RELEASE</version>
<version>1.6.0.M1</version>
<relativePath>../pom.xml</relativePath>
</parent>
@@ -48,7 +48,7 @@
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
<version>1.5.0.RELEASE</version>
<version>1.6.0.M1</version>
</dependency>
<dependency>

View File

@@ -7,7 +7,7 @@ Import-Package:
Export-Template:
org.springframework.data.mongodb.crossstore.*;version="${project.version}"
Import-Template:
com.mongodb.*;version="${mongo:[=.=.=,+1.0.0)}",
com.mongodb.*;version="${mongo.osgi:[=.=.=,+1.0.0)}",
javax.persistence.*;version="${jpa:[=.=.=,+1.0.0)}",
org.aspectj.*;version="${aspectj:[1.0.0, 2.0.0)}",
org.bson.*;version="0",

View File

@@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>1.5.0.RELEASE</version>
<version>1.6.0.M1</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>1.5.0.RELEASE</version>
<version>1.6.0.M1</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -5,5 +5,5 @@ Bundle-ManifestVersion: 2
Import-Package:
sun.reflect;version="0";resolution:=optional
Import-Template:
com.mongodb.*;version="${mongo:[=.=.=,+1.0.0)}",
com.mongodb.*;version="${mongo.osgi:[=.=.=,+1.0.0)}",
org.apache.log4j.*;version="${log4j:[=.=.=,+1.0.0)}"

View File

@@ -11,7 +11,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>1.5.0.RELEASE</version>
<version>1.6.0.M1</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -28,6 +28,9 @@ import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.data.annotation.Persistent;
import org.springframework.data.authentication.UserCredentials;
import org.springframework.data.mapping.context.MappingContextIsNewStrategyFactory;
import org.springframework.data.mapping.model.CamelCaseAbbreviatingFieldNamingStrategy;
import org.springframework.data.mapping.model.FieldNamingStrategy;
import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoDbFactory;
@@ -35,11 +38,8 @@ import org.springframework.data.mongodb.core.convert.CustomConversions;
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.mapping.CamelCaseAbbreviatingFieldNamingStrategy;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.FieldNamingStrategy;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.PropertyNameFieldNamingStrategy;
import org.springframework.data.support.CachingIsNewStrategyFactory;
import org.springframework.data.support.IsNewStrategyFactory;
import org.springframework.util.ClassUtils;

View File

@@ -52,10 +52,10 @@ import org.springframework.core.type.filter.TypeFilter;
import org.springframework.data.annotation.Persistent;
import org.springframework.data.config.BeanComponentDefinitionBuilder;
import org.springframework.data.mapping.context.MappingContextIsNewStrategyFactory;
import org.springframework.data.mapping.model.CamelCaseAbbreviatingFieldNamingStrategy;
import org.springframework.data.mongodb.core.convert.CustomConversions;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCreator;
import org.springframework.data.mongodb.core.mapping.CamelCaseAbbreviatingFieldNamingStrategy;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.event.ValidatingMongoEventListener;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011-2013 the original author or authors.
* Copyright 2011-2014 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.
@@ -36,6 +36,7 @@ import com.mongodb.MongoException;
* @author Mark Pollack
* @author Oliver Gierke
* @author Komi Innocent
* @author Christoph Strobl
*/
public class DefaultIndexOperations implements IndexOperations {
@@ -142,6 +143,13 @@ public class DefaultIndexOperations implements IndexOperations {
if ("2d".equals(value)) {
indexFields.add(IndexField.geo(key));
} else if ("text".equals(value)) {
DBObject weights = (DBObject) ix.get("weights");
for (String fieldName : weights.keySet()) {
indexFields.add(IndexField.text(fieldName, Float.valueOf(weights.get(fieldName).toString())));
}
} else {
Double keyValue = new Double(value.toString());
@@ -159,8 +167,8 @@ public class DefaultIndexOperations implements IndexOperations {
boolean unique = ix.containsField("unique") ? (Boolean) ix.get("unique") : false;
boolean dropDuplicates = ix.containsField("dropDups") ? (Boolean) ix.get("dropDups") : false;
boolean sparse = ix.containsField("sparse") ? (Boolean) ix.get("sparse") : false;
indexInfoList.add(new IndexInfo(indexFields, name, unique, dropDuplicates, sparse));
String language = ix.containsField("default_language") ? (String) ix.get("default_language") : "";
indexInfoList.add(new IndexInfo(indexFields, name, unique, dropDuplicates, sparse, language));
}
return indexInfoList;

View File

@@ -23,11 +23,15 @@ import org.springframework.dao.InvalidDataAccessResourceUsageException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.mongodb.UncategorizedMongoDbException;
import com.mongodb.MongoCursorNotFoundException;
import com.mongodb.MongoException;
import com.mongodb.MongoException.CursorNotFound;
import com.mongodb.MongoException.DuplicateKey;
import com.mongodb.MongoException.Network;
import com.mongodb.MongoInternalException;
import com.mongodb.MongoServerSelectionException;
import com.mongodb.MongoSocketException;
import com.mongodb.MongoTimeoutException;
/**
* Simple {@link PersistenceExceptionTranslator} for Mongo. Convert the given runtime exception to an appropriate
@@ -47,21 +51,23 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
// Check for well-known MongoException subclasses.
// All other MongoExceptions
if (ex instanceof DuplicateKey) {
if (ex instanceof DuplicateKey || ex instanceof DuplicateKeyException) {
return new DuplicateKeyException(ex.getMessage(), ex);
}
if (ex instanceof Network) {
if (ex instanceof Network || ex instanceof MongoSocketException) {
return new DataAccessResourceFailureException(ex.getMessage(), ex);
}
if (ex instanceof CursorNotFound) {
if (ex instanceof CursorNotFound || ex instanceof MongoCursorNotFoundException) {
return new DataAccessResourceFailureException(ex.getMessage(), ex);
}
// Driver 2.12 throws this to indicate connection problems. String comparison to avoid hard dependency
if (ex.getClass().getName().equals("com.mongodb.MongoServerSelectionException")) {
if (ex instanceof MongoServerSelectionException) {
return new DataAccessResourceFailureException(ex.getMessage(), ex);
}
if (ex instanceof MongoTimeoutException) {
return new DataAccessResourceFailureException(ex.getMessage(), ex);
}
@@ -69,6 +75,7 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
return new InvalidDataAccessResourceUsageException(ex.getMessage(), ex);
}
// All other MongoExceptions
if (ex instanceof MongoException) {
int code = ((MongoException) ex).getCode();

View File

@@ -1070,17 +1070,22 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
}
/**
* Returns {@link Entry} containing the {@link MongoPersistentProperty} defining the {@literal id} as
* {@link Entry#getKey()} and the {@link Id}s property value as its {@link Entry#getValue()}.
* Returns {@link Entry} containing the field name of the id property as {@link Entry#getKey()} and the {@link Id}s
* property value as its {@link Entry#getValue()}.
*
* @param object
* @return
*/
private Map.Entry<MongoPersistentProperty, Object> extractIdPropertyAndValue(Object object) {
private Entry<String, Object> extractIdPropertyAndValue(Object object) {
Assert.notNull(object, "Id cannot be extracted from 'null'.");
Class<?> objectType = object.getClass();
if (object instanceof DBObject) {
return Collections.singletonMap(ID_FIELD, ((DBObject) object).get(ID_FIELD)).entrySet().iterator().next();
}
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(objectType);
MongoPersistentProperty idProp = entity == null ? null : entity.getIdProperty();
@@ -1090,7 +1095,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
Object idValue = BeanWrapper.create(object, mongoConverter.getConversionService())
.getProperty(idProp, Object.class);
return Collections.singletonMap(idProp, idValue).entrySet().iterator().next();
return Collections.singletonMap(idProp.getFieldName(), idValue).entrySet().iterator().next();
}
/**
@@ -1101,8 +1106,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
*/
private Query getIdQueryFor(Object object) {
Map.Entry<MongoPersistentProperty, Object> id = extractIdPropertyAndValue(object);
return new Query(where(id.getKey().getFieldName()).is(id.getValue()));
Entry<String, Object> id = extractIdPropertyAndValue(object);
return new Query(where(id.getKey()).is(id.getValue()));
}
/**
@@ -1116,7 +1121,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
Assert.notEmpty(objects, "Cannot create Query for empty collection.");
Iterator<?> it = objects.iterator();
Map.Entry<MongoPersistentProperty, Object> firstEntry = extractIdPropertyAndValue(it.next());
Entry<String, Object> firstEntry = extractIdPropertyAndValue(it.next());
ArrayList<Object> ids = new ArrayList<Object>(objects.size());
ids.add(firstEntry.getValue());
@@ -1125,7 +1130,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
ids.add(extractIdPropertyAndValue(it.next()).getValue());
}
return new Query(where(firstEntry.getKey().getFieldName()).in(ids));
return new Query(where(firstEntry.getKey()).in(ids));
}
private void assertUpdateableIdIfNotSet(Object entity) {
@@ -1413,17 +1418,32 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
CommandResult commandResult = executeCommand(command);
handleCommandError(commandResult, command);
// map results
return new AggregationResults<O>(returnPotentiallyMappedResults(outputType, commandResult), commandResult);
}
/**
* Returns the potentially mapped results of the given {@commandResult} contained some.
*
* @param outputType
* @param commandResult
* @return
*/
private <O> List<O> returnPotentiallyMappedResults(Class<O> outputType, CommandResult commandResult) {
@SuppressWarnings("unchecked")
Iterable<DBObject> resultSet = (Iterable<DBObject>) commandResult.get("result");
List<O> mappedResults = new ArrayList<O>();
if (resultSet == null) {
return Collections.emptyList();
}
DbObjectCallback<O> callback = new UnwrapAndReadDbObjectCallback<O>(mongoConverter, outputType);
List<O> mappedResults = new ArrayList<O>();
for (DBObject dbObject : resultSet) {
mappedResults.add(callback.doWith(dbObject));
}
return new AggregationResults<O>(mappedResults, commandResult);
return mappedResults;
}
protected String replaceWithResourceIfNecessary(String function) {
@@ -1455,13 +1475,13 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
"Can not use skip or field specification with map reduce operations");
}
if (query.getQueryObject() != null) {
copyMapReduceOptions.put("query", query.getQueryObject());
copyMapReduceOptions.put("query", queryMapper.getMappedObject(query.getQueryObject(), null));
}
if (query.getLimit() > 0) {
copyMapReduceOptions.put("limit", query.getLimit());
}
if (query.getSortObject() != null) {
copyMapReduceOptions.put("sort", query.getSortObject());
copyMapReduceOptions.put("sort", queryMapper.getMappedObject(query.getSortObject(), null));
}
}
return copyMapReduceOptions;
@@ -2179,7 +2199,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
cursorToUse = cursorToUse.limit(query.getLimit());
}
if (query.getSortObject() != null) {
cursorToUse = cursorToUse.sort(getMappedSortObject(query, type));
DBObject sortDbo = type != null ? getMappedSortObject(query, type) : query.getSortObject();
cursorToUse = cursorToUse.sort(sortDbo);
}
if (StringUtils.hasText(query.getHint())) {
cursorToUse = cursorToUse.hint(query.getHint());

View File

@@ -44,9 +44,23 @@ import com.mongodb.DBObject;
*/
public class Aggregation {
public static final AggregationOperationContext DEFAULT_CONTEXT = new NoOpAggregationOperationContext();
/**
* References the root document, i.e. the top-level document, currently being processed in the aggregation pipeline
* stage.
*/
public static final String ROOT = SystemVariable.ROOT.toString();
private final List<AggregationOperation> operations;
/**
* References the start of the field path being processed in the aggregation pipeline stage. Unless documented
* otherwise, all stages start with CURRENT the same as ROOT.
*/
public static final String CURRENT = SystemVariable.CURRENT.toString();
public static final AggregationOperationContext DEFAULT_CONTEXT = new NoOpAggregationOperationContext();
public static final AggregationOptions DEFAULT_OPTIONS = newAggregationOptions().build();
protected final List<AggregationOperation> operations;
private final AggregationOptions options;
/**
* Creates a new {@link Aggregation} from the given {@link AggregationOperation}s.
@@ -66,6 +80,20 @@ public class Aggregation {
return new Aggregation(operations);
}
/**
* Returns a copy of this {@link Aggregation} with the given {@link AggregationOptions} set. Note that options are
* supported in MongoDB version 2.6+.
*
* @param options must not be {@literal null}.
* @return
* @since 1.6
*/
public Aggregation withOptions(AggregationOptions options) {
Assert.notNull(options, "AggregationOptions must not be null.");
return new Aggregation(this.operations, options);
}
/**
* Creates a new {@link TypedAggregation} for the given type and {@link AggregationOperation}s.
*
@@ -92,11 +120,43 @@ public class Aggregation {
* @param aggregationOperations must not be {@literal null} or empty.
*/
protected Aggregation(AggregationOperation... aggregationOperations) {
this(asAggregationList(aggregationOperations));
}
/**
* @param aggregationOperations must not be {@literal null} or empty.
* @return
*/
protected static List<AggregationOperation> asAggregationList(AggregationOperation... aggregationOperations) {
Assert.notEmpty(aggregationOperations, "AggregationOperations must not be null or empty!");
return Arrays.asList(aggregationOperations);
}
/**
* Creates a new {@link Aggregation} from the given {@link AggregationOperation}s.
*
* @param aggregationOperations must not be {@literal null} or empty.
*/
protected Aggregation(List<AggregationOperation> aggregationOperations) {
this(aggregationOperations, DEFAULT_OPTIONS);
}
/**
* Creates a new {@link Aggregation} from the given {@link AggregationOperation}s.
*
* @param aggregationOperations must not be {@literal null} or empty.
* @param options must not be {@literal null} or empty.
*/
protected Aggregation(List<AggregationOperation> aggregationOperations, AggregationOptions options) {
Assert.notNull(aggregationOperations, "AggregationOperations must not be null!");
Assert.isTrue(aggregationOperations.length > 0, "At least one AggregationOperation has to be provided");
Assert.isTrue(aggregationOperations.size() > 0, "At least one AggregationOperation has to be provided");
Assert.notNull(options, "AggregationOptions must not be null!");
this.operations = Arrays.asList(aggregationOperations);
this.operations = aggregationOperations;
this.options = options;
}
/**
@@ -231,6 +291,16 @@ public class Aggregation {
return Fields.from(field(name, target));
}
/**
* Returns a new {@link AggregationOptions.Builder}.
*
* @return
* @since 1.6
*/
public static AggregationOptions.Builder newAggregationOptions() {
return new AggregationOptions.Builder();
}
/**
* Converts this {@link Aggregation} specification to a {@link DBObject}.
*
@@ -255,6 +325,8 @@ public class Aggregation {
DBObject command = new BasicDBObject("aggregate", inputCollectionName);
command.put("pipeline", operationDocuments);
command = options.applyAndReturnPotentiallyChangedCommand(command);
return command;
}
@@ -302,4 +374,51 @@ public class Aggregation {
return new FieldReference(new ExposedField(new AggregationField(name), true));
}
}
/**
* Describes the system variables available in MongoDB aggregation framework pipeline expressions.
*
* @author Thomas Darimont
* @see http://docs.mongodb.org/manual/reference/aggregation-variables
*/
enum SystemVariable {
ROOT, CURRENT;
private static final String PREFIX = "$$";
/**
* Return {@literal true} if the given {@code fieldRef} denotes a well-known system variable, {@literal false}
* otherwise.
*
* @param fieldRef may be {@literal null}.
* @return
*/
public static boolean isReferingToSystemVariable(String fieldRef) {
if (fieldRef == null || !fieldRef.startsWith(PREFIX) || fieldRef.length() <= 2) {
return false;
}
int indexOfFirstDot = fieldRef.indexOf('.');
String candidate = fieldRef.substring(2, indexOfFirstDot == -1 ? fieldRef.length() : indexOfFirstDot);
for (SystemVariable value : values()) {
if (value.name().equals(candidate)) {
return true;
}
}
return false;
}
/*
* (non-Javadoc)
* @see java.lang.Enum#toString()
*/
@Override
public String toString() {
return PREFIX.concat(name());
}
}
}

View File

@@ -0,0 +1,189 @@
/*
* Copyright 2014 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 com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
/**
* Holds a set of configurable aggregation options that can be used within an aggregation pipeline. A list of support
* aggregation options can be found in the MongoDB reference documentation
* http://docs.mongodb.org/manual/reference/command/aggregate/#aggregate
*
* @author Thomas Darimont
* @author Oliver Gierke
* @see Aggregation#withOptions(AggregationOptions)
* @see TypedAggregation#withOptions(AggregationOptions)
* @since 1.6
*/
public class AggregationOptions {
private static final String CURSOR = "cursor";
private static final String EXPLAIN = "explain";
private static final String ALLOW_DISK_USE = "allowDiskUse";
private final boolean allowDiskUse;
private final boolean explain;
private final DBObject cursor;
/**
* Creates a new {@link AggregationOptions}.
*
* @param allowDiskUse whether to off-load intensive sort-operations to disk.
* @param explain whether to get the execution plan for the aggregation instead of the actual results.
* @param cursor can be {@literal null}, used to pass additional options to the aggregation.
*/
public AggregationOptions(boolean allowDiskUse, boolean explain, DBObject cursor) {
this.allowDiskUse = allowDiskUse;
this.explain = explain;
this.cursor = cursor;
}
/**
* Enables writing to temporary files. When set to true, aggregation stages can write data to the _tmp subdirectory in
* the dbPath directory.
*
* @return
*/
public boolean isAllowDiskUse() {
return allowDiskUse;
}
/**
* Specifies to return the information on the processing of the pipeline.
*
* @return
*/
public boolean isExplain() {
return explain;
}
/**
* Specify a document that contains options that control the creation of the cursor object.
*
* @return
*/
public DBObject getCursor() {
return cursor;
}
/**
* Returns a new potentially adjusted copy for the given {@code aggregationCommandObject} with the configuration
* applied.
*
* @param command the aggregation command.
* @return
*/
DBObject applyAndReturnPotentiallyChangedCommand(DBObject command) {
DBObject result = new BasicDBObject(command.toMap());
if (allowDiskUse && !result.containsField(ALLOW_DISK_USE)) {
result.put(ALLOW_DISK_USE, allowDiskUse);
}
if (explain && !result.containsField(EXPLAIN)) {
result.put(EXPLAIN, explain);
}
if (cursor != null && !result.containsField(CURSOR)) {
result.put("cursor", cursor);
}
return result;
}
/**
* Returns a {@link DBObject} representation of this {@link AggregationOptions}.
*
* @return
*/
public DBObject toDbObject() {
DBObject dbo = new BasicDBObject();
dbo.put(ALLOW_DISK_USE, allowDiskUse);
dbo.put(EXPLAIN, explain);
dbo.put(CURSOR, cursor);
return dbo;
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return toDbObject().toString();
}
/**
* A Builder for {@link AggregationOptions}.
*
* @author Thomas Darimont
*/
public static class Builder {
private boolean allowDiskUse;
private boolean explain;
private DBObject cursor;
/**
* Defines whether to off-load intensive sort-operations to disk.
*
* @param allowDiskUse
* @return
*/
public Builder allowDiskUse(boolean allowDiskUse) {
this.allowDiskUse = allowDiskUse;
return this;
}
/**
* Defines whether to get the execution plan for the aggregation instead of the actual results.
*
* @param explain
* @return
*/
public Builder explain(boolean explain) {
this.explain = explain;
return this;
}
/**
* Additional options to the aggregation.
*
* @param cursor
* @return
*/
public Builder cursor(DBObject cursor) {
this.cursor = cursor;
return this;
}
/**
* Returns a new {@link AggregationOptions} instance with the given configuration.
*
* @return
*/
public AggregationOptions build() {
return new AggregationOptions(allowDiskUse, explain, cursor);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-2014 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.
@@ -28,6 +28,7 @@ import com.mongodb.DBObject;
*
* @author Tobias Trelle
* @author Oliver Gierke
* @author Thomas Darimont
* @param <T> The class in which the results are mapped onto.
* @since 1.3
*/
@@ -90,6 +91,16 @@ public class AggregationResults<T> implements Iterable<T> {
return serverUsed;
}
/**
* Returns the raw result that was returned by the server.
*
* @return
* @since 1.6
*/
public DBObject getRawResults() {
return rawResults;
}
private String parseServerUsed() {
Object object = rawResults.get("serverUsed");

View File

@@ -30,6 +30,7 @@ import org.springframework.util.StringUtils;
* Value object to capture a list of {@link Field} instances.
*
* @author Oliver Gierke
* @author Thomas Darimont
* @since 1.3
*/
public final class Fields implements Iterable<Field> {
@@ -186,7 +187,7 @@ public final class Fields implements Iterable<Field> {
private final String target;
/**
* Creates an aggregation fieldwith the given name. As no target is set explicitly, the name will be used as target
* Creates an aggregation field with the given name. As no target is set explicitly, the name will be used as target
* as well.
*
* @param key
@@ -217,6 +218,10 @@ public final class Fields implements Iterable<Field> {
return source;
}
if (Aggregation.SystemVariable.isReferingToSystemVariable(source)) {
return source;
}
int dollarIndex = source.lastIndexOf('$');
return dollarIndex == -1 ? source : source.substring(dollarIndex + 1);
}

View File

@@ -364,7 +364,16 @@ public class GroupOperation implements FieldsExposingAggregationOperation {
}
public Object getValue(AggregationOperationContext context) {
return reference == null ? value : context.getReference(reference).toString();
if (reference == null) {
return value;
}
if (Aggregation.SystemVariable.isReferingToSystemVariable(reference)) {
return reference;
}
return context.getReference(reference).toString();
}
@Override

View File

@@ -24,7 +24,6 @@ import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedFi
import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder.FieldProjection;
import org.springframework.util.Assert;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
@@ -235,26 +234,53 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
}
/**
* An {@link ProjectionOperationBuilder} that is used for SpEL expression based projections.
*
* @author Thomas Darimont
*/
public static class ExpressionProjectionOperationBuilder extends AbstractProjectionOperationBuilder {
public static class ExpressionProjectionOperationBuilder extends ProjectionOperationBuilder {
private final Object[] params;
private final String expression;
/**
* Creates a new {@link ExpressionProjectionOperationBuilder} for the given value, {@link ProjectionOperation} and
* parameters.
*
* @param value must not be {@literal null}.
* @param expression must not be {@literal null}.
* @param operation must not be {@literal null}.
* @param parameters
*/
public ExpressionProjectionOperationBuilder(Object value, ProjectionOperation operation, Object[] parameters) {
public ExpressionProjectionOperationBuilder(String expression, ProjectionOperation operation, Object[] parameters) {
super(value, operation);
super(expression, operation, null);
this.expression = expression;
this.params = parameters.clone();
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder#project(java.lang.String, java.lang.Object[])
*/
@Override
public ProjectionOperationBuilder project(String operation, final Object... values) {
OperationProjection operationProjection = new OperationProjection(Fields.field(value.toString()), operation,
values) {
@Override
protected List<Object> getOperationArguments(AggregationOperationContext context) {
List<Object> result = new ArrayList<Object>(values.length + 1);
result.add(ExpressionProjection.toMongoExpression(context,
ExpressionProjectionOperationBuilder.this.expression, ExpressionProjectionOperationBuilder.this.params));
result.addAll(Arrays.asList(values));
return result;
}
};
return new ProjectionOperationBuilder(value, this.operation.and(operationProjection), operationProjection);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.AbstractProjectionOperationBuilder#as(java.lang.String)
@@ -303,7 +329,11 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
*/
@Override
public DBObject toDBObject(AggregationOperationContext context) {
return new BasicDBObject(getExposedField().getName(), TRANSFORMER.transform(expression, context, params));
return new BasicDBObject(getExposedField().getName(), toMongoExpression(context, expression, params));
}
protected static Object toMongoExpression(AggregationOperationContext context, String expression, Object[] params) {
return TRANSFORMER.transform(expression, context, params);
}
}
}
@@ -320,7 +350,6 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
private static final String FIELD_REFERENCE_NOT_NULL = "Field reference must not be null!";
private final String name;
private final ProjectionOperation operation;
private final OperationProjection previousProjection;
/**
@@ -335,7 +364,23 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
super(name, operation);
this.name = name;
this.operation = operation;
this.previousProjection = previousProjection;
}
/**
* Creates a new {@link ProjectionOperationBuilder} for the field with the given value on top of the given
* {@link ProjectionOperation}.
*
* @param value
* @param operation
* @param previousProjection
*/
protected ProjectionOperationBuilder(Object value, ProjectionOperation operation,
OperationProjection previousProjection) {
super(value, operation);
this.name = null;
this.previousProjection = previousProjection;
}
@@ -521,8 +566,9 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
* @return
*/
public ProjectionOperationBuilder project(String operation, Object... values) {
OperationProjection projectionOperation = new OperationProjection(Fields.field(name), operation, values);
return new ProjectionOperationBuilder(name, this.operation.and(projectionOperation), projectionOperation);
OperationProjection operationProjection = new OperationProjection(Fields.field(value.toString()), operation,
values);
return new ProjectionOperationBuilder(value, this.operation.and(operationProjection), operationProjection);
}
/**
@@ -627,6 +673,10 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
// implicit reference or explicit include?
if (value == null || Boolean.TRUE.equals(value)) {
if (Aggregation.SystemVariable.isReferingToSystemVariable(field.getTarget())) {
return field.getTarget();
}
// check whether referenced field exists in the context
return context.getReference(field).getReferenceValue();
@@ -649,7 +699,7 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
/**
* Creates a new {@link OperationProjection} for the given field.
*
* @param name the name of the field to add the operation projection for, must not be {@literal null} or empty.
* @param field the name of the field to add the operation projection for, must not be {@literal null} or empty.
* @param operation the actual operation key, must not be {@literal null} or empty.
* @param values the values to pass into the operation, must not be {@literal null}.
*/
@@ -672,18 +722,15 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
@Override
public DBObject toDBObject(AggregationOperationContext context) {
BasicDBList values = new BasicDBList();
values.addAll(buildReferences(context));
DBObject inner = new BasicDBObject("$" + operation, getOperationArguments(context));
DBObject inner = new BasicDBObject("$" + operation, values);
return new BasicDBObject(this.field.getName(), inner);
return new BasicDBObject(getField().getName(), inner);
}
private List<Object> buildReferences(AggregationOperationContext context) {
protected List<Object> getOperationArguments(AggregationOperationContext context) {
List<Object> result = new ArrayList<Object>(values.size());
result.add(context.getReference(field.getTarget()).toString());
result.add(context.getReference(getField().getName()).toString());
for (Object element : values) {
result.add(element instanceof Field ? context.getReference((Field) element).toString() : element);
@@ -692,6 +739,15 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
return result;
}
/**
* Returns the field that holds the {@link OperationProjection}.
*
* @return
*/
protected Field getField() {
return field;
}
/**
* Creates a new instance of this {@link OperationProjection} with the given alias.
*
@@ -699,7 +755,27 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
* @return
*/
public OperationProjection withAlias(String alias) {
return new OperationProjection(Fields.field(alias, this.field.getName()), operation, values.toArray());
final Field aliasedField = Fields.field(alias, this.field.getName());
return new OperationProjection(aliasedField, operation, values.toArray()) {
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder.OperationProjection#getField()
*/
@Override
protected Field getField() {
return aliasedField;
}
@Override
protected List<Object> getOperationArguments(AggregationOperationContext context) {
// We have to make sure that we use the arguments from the "previous" OperationProjection that we replace
// with this new instance.
return OperationProjection.this.getOperationArguments(context);
}
};
}
}
@@ -731,6 +807,96 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
return new BasicDBObject(name, nestedObject);
}
}
/**
* Extracts the minute from a date expression.
*
* @return
*/
public ProjectionOperationBuilder extractMinute() {
return project("minute");
}
/**
* Extracts the hour from a date expression.
*
* @return
*/
public ProjectionOperationBuilder extractHour() {
return project("hour");
}
/**
* Extracts the second from a date expression.
*
* @return
*/
public ProjectionOperationBuilder extractSecond() {
return project("second");
}
/**
* Extracts the millisecond from a date expression.
*
* @return
*/
public ProjectionOperationBuilder extractMillisecond() {
return project("millisecond");
}
/**
* Extracts the year from a date expression.
*
* @return
*/
public ProjectionOperationBuilder extractYear() {
return project("year");
}
/**
* Extracts the month from a date expression.
*
* @return
*/
public ProjectionOperationBuilder extractMonth() {
return project("month");
}
/**
* Extracts the week from a date expression.
*
* @return
*/
public ProjectionOperationBuilder extractWeek() {
return project("week");
}
/**
* Extracts the dayOfYear from a date expression.
*
* @return
*/
public ProjectionOperationBuilder extractDayOfYear() {
return project("dayOfYear");
}
/**
* Extracts the dayOfMonth from a date expression.
*
* @return
*/
public ProjectionOperationBuilder extractDayOfMonth() {
return project("dayOfMonth");
}
/**
* Extracts the dayOfWeek from a date expression.
*
* @return
*/
public ProjectionOperationBuilder extractDayOfWeek() {
return project("dayOfWeek");
}
}
/**
@@ -771,5 +937,4 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
*/
public abstract DBObject toDBObject(AggregationOperationContext context);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-2014 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,6 +15,8 @@
*/
package org.springframework.data.mongodb.core.aggregation;
import java.util.List;
import org.springframework.util.Assert;
/**
@@ -30,11 +32,34 @@ public class TypedAggregation<I> extends Aggregation {
/**
* Creates a new {@link TypedAggregation} from the given {@link AggregationOperation}s.
*
* @param inputType must not be {@literal null}.
* @param operations must not be {@literal null} or empty.
*/
public TypedAggregation(Class<I> inputType, AggregationOperation... operations) {
this(inputType, asAggregationList(operations));
}
super(operations);
/**
* Creates a new {@link TypedAggregation} from the given {@link AggregationOperation}s.
*
* @param inputType must not be {@literal null}.
* @param operations must not be {@literal null} or empty.
*/
public TypedAggregation(Class<I> inputType, List<AggregationOperation> operations) {
this(inputType, operations, DEFAULT_OPTIONS);
}
/**
* Creates a new {@link TypedAggregation} from the given {@link AggregationOperation}s and the given
* {@link AggregationOptions}.
*
* @param inputType must not be {@literal null}.
* @param operations must not be {@literal null} or empty.
* @param options must not be {@literal null}.
*/
public TypedAggregation(Class<I> inputType, List<AggregationOperation> operations, AggregationOptions options) {
super(operations, options);
Assert.notNull(inputType, "Input type must not be null!");
this.inputType = inputType;
@@ -48,4 +73,14 @@ public class TypedAggregation<I> extends Aggregation {
public Class<I> getInputType() {
return inputType;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.Aggregation#withOptions(org.springframework.data.mongodb.core.aggregation.AggregationOptions)
*/
public TypedAggregation<I> withOptions(AggregationOptions options) {
Assert.notNull(options, "AggregationOptions must not be null.");
return new TypedAggregation<I>(inputType, operations, options);
}
}

View File

@@ -25,10 +25,7 @@ import java.lang.reflect.Method;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.objenesis.Objenesis;
import org.objenesis.ObjenesisStd;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.cglib.proxy.Callback;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.Factory;
@@ -39,8 +36,8 @@ import org.springframework.data.mongodb.LazyLoadingException;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.objenesis.ObjenesisStd;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
@@ -57,10 +54,9 @@ import com.mongodb.DBRef;
*/
public class DefaultDbRefResolver implements DbRefResolver {
private static final boolean OBJENESIS_PRESENT = ClassUtils.isPresent("org.objenesis.Objenesis", null);
private final MongoDbFactory mongoDbFactory;
private final PersistenceExceptionTranslator exceptionTranslator;
private final ObjenesisStd objenesis;
/**
* Creates a new {@link DefaultDbRefResolver} with the given {@link MongoDbFactory}.
@@ -73,6 +69,7 @@ public class DefaultDbRefResolver implements DbRefResolver {
this.mongoDbFactory = mongoDbFactory;
this.exceptionTranslator = mongoDbFactory.getExceptionTranslator();
this.objenesis = new ObjenesisStd(true);
}
/*
@@ -117,32 +114,44 @@ public class DefaultDbRefResolver implements DbRefResolver {
*/
private Object createLazyLoadingProxy(MongoPersistentProperty property, DBRef dbref, DbRefResolverCallback callback) {
ProxyFactory proxyFactory = new ProxyFactory();
Class<?> propertyType = property.getType();
LazyLoadingInterceptor interceptor = new LazyLoadingInterceptor(property, dbref, exceptionTranslator, callback);
if (!propertyType.isInterface()) {
Factory factory = (Factory) objenesis.newInstance(getEnhancedTypeFor(propertyType));
factory.setCallbacks(new Callback[] { interceptor });
return factory;
}
ProxyFactory proxyFactory = new ProxyFactory();
for (Class<?> type : propertyType.getInterfaces()) {
proxyFactory.addInterface(type);
}
LazyLoadingInterceptor interceptor = new LazyLoadingInterceptor(property, dbref, exceptionTranslator, callback);
proxyFactory.addInterface(LazyLoadingProxy.class);
proxyFactory.addInterface(propertyType);
proxyFactory.addAdvice(interceptor);
if (propertyType.isInterface()) {
proxyFactory.addInterface(propertyType);
proxyFactory.addAdvice(interceptor);
return proxyFactory.getProxy();
}
return proxyFactory.getProxy();
}
proxyFactory.setProxyTargetClass(true);
proxyFactory.setTargetClass(propertyType);
/**
* Returns the CGLib enhanced type for the given source type.
*
* @param type
* @return
*/
private Class<?> getEnhancedTypeFor(Class<?> type) {
if (!OBJENESIS_PRESENT) {
proxyFactory.addAdvice(interceptor);
return proxyFactory.getProxy();
}
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(type);
enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class);
enhancer.setInterfaces(new Class[] { LazyLoadingProxy.class });
return ObjenesisProxyEnhancer.enhanceAndGet(proxyFactory, propertyType, interceptor);
return enhancer.createClass();
}
/**
@@ -371,109 +380,4 @@ public class DefaultDbRefResolver implements DbRefResolver {
return result;
}
}
/**
* Static class to accommodate optional dependency on Objenesis.
*
* @author Oliver Gierke
* @author Thomas Darimont
* @since 1.4
*/
private static class ObjenesisProxyEnhancer {
private static final boolean IS_SPRING_4_OR_BETTER = ClassUtils.isPresent(
"org.springframework.core.DefaultParameterNameDiscoverer", null);
private static final InstanceCreatorStrategy INSTANCE_CREATOR;
static {
if (IS_SPRING_4_OR_BETTER) {
INSTANCE_CREATOR = new Spring4ObjenesisInstanceCreatorStrategy();
} else {
INSTANCE_CREATOR = new DefaultObjenesisInstanceCreatorStrategy();
}
}
public static Object enhanceAndGet(ProxyFactory proxyFactory, Class<?> type,
org.springframework.cglib.proxy.MethodInterceptor interceptor) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(type);
enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class);
enhancer.setInterfaces(new Class[] { LazyLoadingProxy.class });
Factory factory = (Factory) INSTANCE_CREATOR.newInstance(enhancer.createClass());
factory.setCallbacks(new Callback[] { interceptor });
return factory;
}
/**
* Strategy for constructing new instances of a given {@link Class}.
*
* @author Thomas Darimont
*/
interface InstanceCreatorStrategy {
Object newInstance(Class<?> clazz);
}
/**
* An {@link InstanceCreatorStrategy} that uses Objenesis from the classpath.
*
* @author Thomas Darimont
*/
private static class DefaultObjenesisInstanceCreatorStrategy implements InstanceCreatorStrategy {
private static final Objenesis OBJENESIS = new ObjenesisStd(true);
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.DefaultDbRefResolver.ObjenesisProxyEnhancer.InstanceCreatorStrategy#newInstance(java.lang.Class)
*/
@Override
public Object newInstance(Class<?> clazz) {
return OBJENESIS.newInstance(clazz);
}
}
/**
* An {@link InstanceCreatorStrategy} that uses a repackaged version of Objenesis from Spring 4.
*
* @author Thomas Darimont
*/
private static class Spring4ObjenesisInstanceCreatorStrategy implements InstanceCreatorStrategy {
private static final String SPRING4_OBJENESIS_CLASS_NAME = "org.springframework.objenesis.ObjenesisStd";
private static final Object OBJENESIS;
private static final Method NEW_INSTANCE_METHOD;
static {
try {
Class<?> objenesisClass = ClassUtils.forName(SPRING4_OBJENESIS_CLASS_NAME,
ObjenesisProxyEnhancer.class.getClassLoader());
OBJENESIS = BeanUtils.instantiateClass(objenesisClass.getConstructor(boolean.class), true);
NEW_INSTANCE_METHOD = objenesisClass.getMethod("newInstance", Class.class);
} catch (Exception e) {
throw new RuntimeException("Could not setup Objenesis infrastructure with Spring 4 ", e);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.DefaultDbRefResolver.ObjenesisProxyEnhancer.InstanceCreatorStrategy#newInstance(java.lang.Class)
*/
@Override
public Object newInstance(Class<?> clazz) {
try {
return NEW_INSTANCE_METHOD.invoke(OBJENESIS, clazz);
} catch (Exception e) {
throw new RuntimeException("Could not created instance for " + clazz, e);
}
}
}
}
}

View File

@@ -385,7 +385,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
entity.doWithProperties(new PropertyHandler<MongoPersistentProperty>() {
public void doWithPersistentProperty(MongoPersistentProperty prop) {
if (prop.equals(idProperty)) {
if (prop.equals(idProperty) || !prop.isWritable()) {
return;
}

View File

@@ -107,8 +107,11 @@ public @interface CompoundIndex {
/**
* Configures the number of seconds after which the collection should expire. Defaults to -1 for no expiry.
*
* @deprecated TTL cannot be defined for {@link CompoundIndex} having more than one field as key. Will be removed in
* 1.6.
* @see http://docs.mongodb.org/manual/tutorial/expire-data/
* @return
*/
@Deprecated
int expireAfterSeconds() default -1;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2013 the original author or authors.
* Copyright 2012-2014 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.
@@ -24,22 +24,33 @@ import org.springframework.util.ObjectUtils;
* Value object for an index field.
*
* @author Oliver Gierke
* @author Christoph Strobl
*/
@SuppressWarnings("deprecation")
public final class IndexField {
enum Type {
GEO, TEXT, DEFAULT;
}
private final String key;
private final Direction direction;
private final boolean isGeo;
private final Type type;
private final Float weight;
private IndexField(String key, Direction direction, boolean isGeo) {
private IndexField(String key, Direction direction, Type type) {
this(key, direction, type, Float.NaN);
}
private IndexField(String key, Direction direction, Type type, Float weight) {
Assert.hasText(key);
Assert.isTrue(direction != null ^ isGeo);
Assert.isTrue(direction != null ^ (Type.GEO.equals(type) || Type.TEXT.equals(type)));
this.key = key;
this.direction = direction;
this.isGeo = isGeo;
this.type = type == null ? Type.DEFAULT : type;
this.weight = weight == null ? Float.NaN : weight;
}
/**
@@ -53,12 +64,12 @@ public final class IndexField {
@Deprecated
public static IndexField create(String key, Order order) {
Assert.notNull(order);
return new IndexField(key, order.toDirection(), false);
return new IndexField(key, order.toDirection(), Type.DEFAULT);
}
public static IndexField create(String key, Direction order) {
Assert.notNull(order);
return new IndexField(key, order, false);
return new IndexField(key, order, Type.DEFAULT);
}
/**
@@ -68,7 +79,16 @@ public final class IndexField {
* @return
*/
public static IndexField geo(String key) {
return new IndexField(key, null, true);
return new IndexField(key, null, Type.GEO);
}
/**
* Creates a text {@link IndexField} for the given key.
*
* @since 1.6
*/
public static IndexField text(String key, Float weight) {
return new IndexField(key, null, Type.TEXT, weight);
}
/**
@@ -101,10 +121,20 @@ public final class IndexField {
/**
* Returns whether the {@link IndexField} is a geo index field.
*
* @return the isGeo
* @return true if type is {@link Type#GEO}.
*/
public boolean isGeo() {
return isGeo;
return Type.GEO.equals(type);
}
/**
* Returns wheter the {@link IndexField} is a text index field.
*
* @return true if type is {@link Type#TEXT}
* @since 1.6
*/
public boolean isText() {
return Type.TEXT.equals(type);
}
/*
@@ -125,7 +155,7 @@ public final class IndexField {
IndexField that = (IndexField) obj;
return this.key.equals(that.key) && ObjectUtils.nullSafeEquals(this.direction, that.direction)
&& this.isGeo == that.isGeo;
&& this.type == that.type;
}
/*
@@ -138,7 +168,8 @@ public final class IndexField {
int result = 17;
result += 31 * ObjectUtils.nullSafeHashCode(key);
result += 31 * ObjectUtils.nullSafeHashCode(direction);
result += 31 * ObjectUtils.nullSafeHashCode(isGeo);
result += 31 * ObjectUtils.nullSafeHashCode(type);
result += 31 * ObjectUtils.nullSafeHashCode(weight);
return result;
}
@@ -148,6 +179,7 @@ public final class IndexField {
*/
@Override
public String toString() {
return String.format("IndexField [ key: %s, direction: %s, isGeo: %s]", key, direction, isGeo);
return String.format("IndexField [ key: %s, direction: %s, type: %s, weight: %s]", key, direction, type, weight);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2014 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.
@@ -23,6 +23,11 @@ import java.util.List;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* @author Mark Pollack
* @author Oliver Gierke
* @author Christoph Strobl
*/
public class IndexInfo {
private final List<IndexField> indexFields;
@@ -31,14 +36,30 @@ public class IndexInfo {
private final boolean unique;
private final boolean dropDuplicates;
private final boolean sparse;
private final String language;
/**
* @deprecated Will be removed in 1.7. Please use {@link #IndexInfo(List, String, boolean, boolean, boolean, String)}
* @param indexFields
* @param name
* @param unique
* @param dropDuplicates
* @param sparse
*/
@Deprecated
public IndexInfo(List<IndexField> indexFields, String name, boolean unique, boolean dropDuplicates, boolean sparse) {
this(indexFields, name, unique, dropDuplicates, sparse, "");
}
public IndexInfo(List<IndexField> indexFields, String name, boolean unique, boolean dropDuplicates, boolean sparse,
String language) {
this.indexFields = Collections.unmodifiableList(indexFields);
this.name = name;
this.unique = unique;
this.dropDuplicates = dropDuplicates;
this.sparse = sparse;
this.language = language;
}
/**
@@ -84,14 +105,23 @@ public class IndexInfo {
return sparse;
}
/**
* @return
* @since 1.6
*/
public String getLanguage() {
return language;
}
@Override
public String toString() {
return "IndexInfo [indexFields=" + indexFields + ", name=" + name + ", unique=" + unique + ", dropDuplicates="
+ dropDuplicates + ", sparse=" + sparse + "]";
+ dropDuplicates + ", sparse=" + sparse + ", language=" + language + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (dropDuplicates ? 1231 : 1237);
@@ -99,6 +129,7 @@ public class IndexInfo {
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + (sparse ? 1231 : 1237);
result = prime * result + (unique ? 1231 : 1237);
result = prime * result + ObjectUtils.nullSafeHashCode(language);
return result;
}
@@ -137,6 +168,9 @@ public class IndexInfo {
if (unique != other.unique) {
return false;
}
if (!ObjectUtils.nullSafeEquals(language, other.language)) {
return false;
}
return true;
}
}

View File

@@ -16,13 +16,12 @@
package org.springframework.data.mongodb.core.index;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -31,6 +30,9 @@ import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mongodb.core.index.Index.Duplicates;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolver.TextIndexIncludeOptions.IncludeStrategy;
import org.springframework.data.mongodb.core.index.TextIndexDefinition.TextIndexDefinitionBuilder;
import org.springframework.data.mongodb.core.index.TextIndexDefinition.TextIndexedFieldSpec;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
@@ -95,6 +97,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
final List<IndexDefinitionHolder> indexInformation = new ArrayList<MongoPersistentEntityIndexResolver.IndexDefinitionHolder>();
indexInformation.addAll(potentiallyCreateCompoundIndexDefinitions("", root.getCollection(), root.getType()));
indexInformation.addAll(potentiallyCreateTextIndexDefinition(root));
final CycleGuard guard = new CycleGuard();
@@ -103,15 +106,19 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
@Override
public void doWithPersistentProperty(MongoPersistentProperty persistentProperty) {
if (persistentProperty.isEntity()) {
indexInformation.addAll(resolveIndexForClass(persistentProperty.getActualType(),
persistentProperty.getFieldName(), root.getCollection(), guard));
}
try {
if (persistentProperty.isEntity()) {
indexInformation.addAll(resolveIndexForClass(persistentProperty.getActualType(),
persistentProperty.getFieldName(), root.getCollection(), guard));
}
IndexDefinitionHolder indexDefinitionHolder = createIndexDefinitionHolderForProperty(
persistentProperty.getFieldName(), root.getCollection(), persistentProperty);
if (indexDefinitionHolder != null) {
indexInformation.add(indexDefinitionHolder);
IndexDefinitionHolder indexDefinitionHolder = createIndexDefinitionHolderForProperty(
persistentProperty.getFieldName(), root.getCollection(), persistentProperty);
if (indexDefinitionHolder != null) {
indexInformation.add(indexDefinitionHolder);
}
} catch (CyclicPropertyReferenceException e) {
LOGGER.warn(e.getMessage());
}
}
});
@@ -186,6 +193,82 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
return createCompoundIndexDefinitions(dotPath, collection, type);
}
private Collection<? extends IndexDefinitionHolder> potentiallyCreateTextIndexDefinition(MongoPersistentEntity<?> root) {
TextIndexDefinitionBuilder indexDefinitionBuilder = new TextIndexDefinitionBuilder().named(root.getType()
.getSimpleName() + "_TextIndex");
if (StringUtils.hasText(root.getLanguage())) {
indexDefinitionBuilder.withDefaultLanguage(root.getLanguage());
}
try {
appendTextIndexInformation("", indexDefinitionBuilder, root,
new TextIndexIncludeOptions(IncludeStrategy.DEFAULT), new CycleGuard());
} catch (CyclicPropertyReferenceException e) {
LOGGER.warn(e.getMessage());
}
TextIndexDefinition indexDefinition = indexDefinitionBuilder.build();
if (!indexDefinition.hasFieldSpec()) {
return Collections.emptyList();
}
IndexDefinitionHolder holder = new IndexDefinitionHolder("", indexDefinition, root.getCollection());
return Collections.singletonList(holder);
}
private void appendTextIndexInformation(final String dotPath,
final TextIndexDefinitionBuilder indexDefinitionBuilder, MongoPersistentEntity<?> entity,
final TextIndexIncludeOptions includeOptions, final CycleGuard guard) {
entity.doWithProperties(new PropertyHandler<MongoPersistentProperty>() {
@Override
public void doWithPersistentProperty(MongoPersistentProperty persistentProperty) {
guard.protect(persistentProperty, dotPath);
if (persistentProperty.isLanguageProperty()) {
indexDefinitionBuilder.withLanguageOverride(persistentProperty.getFieldName());
}
TextIndexed indexed = persistentProperty.findAnnotation(TextIndexed.class);
if (includeOptions.isForce() || indexed != null || persistentProperty.isEntity()) {
String propertyDotPath = (StringUtils.hasText(dotPath) ? dotPath + "." : "")
+ persistentProperty.getFieldName();
Float weight = indexed != null ? indexed.weight()
: (includeOptions.getParentFieldSpec() != null ? includeOptions.getParentFieldSpec().getWeight() : 1.0F);
if (persistentProperty.isEntity()) {
TextIndexIncludeOptions optionsForNestedType = includeOptions;
if (!IncludeStrategy.FORCE.equals(includeOptions.getStrategy()) && indexed != null) {
optionsForNestedType = new TextIndexIncludeOptions(IncludeStrategy.FORCE, new TextIndexedFieldSpec(
propertyDotPath, weight));
}
try {
appendTextIndexInformation(propertyDotPath, indexDefinitionBuilder,
mappingContext.getPersistentEntity(persistentProperty.getActualType()), optionsForNestedType, guard);
} catch (CyclicPropertyReferenceException e) {
LOGGER.warn(e.getMessage(), e);
}
} else if (includeOptions.isForce() || indexed != null) {
indexDefinitionBuilder.onField(propertyDotPath, weight);
}
}
}
});
}
/**
* Create {@link IndexDefinition} wrapped in {@link IndexDefinitionHolder} for {@link CompoundIndexes} of given type.
*
@@ -215,6 +298,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
return indexDefinitions;
}
@SuppressWarnings("deprecation")
protected IndexDefinitionHolder createCompoundIndexDefinition(String dotPath, String fallbackCollection,
CompoundIndex index) {
@@ -237,8 +321,14 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
indexDefinition.background();
}
if (index.expireAfterSeconds() >= 0) {
indexDefinition.expire(index.expireAfterSeconds(), TimeUnit.SECONDS);
int ttl = index.expireAfterSeconds();
if (ttl >= 0) {
if (indexDefinition.getIndexKeys().keySet().size() > 1) {
LOGGER.warn("TTL is not supported for compound index with more than one key. TTL={} will be ignored.", ttl);
} else {
indexDefinition.expire(ttl, TimeUnit.SECONDS);
}
}
String collection = StringUtils.hasText(index.collection()) ? index.collection() : fallbackCollection;
@@ -343,7 +433,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
*
* @author Christoph Strobl
*/
private static class CycleGuard {
static class CycleGuard {
private final Map<String, List<Path>> propertyTypeMap;
@@ -355,6 +445,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
* @param property The property to inspect
* @param path The path under which the property can be reached.
* @throws CyclicPropertyReferenceException in case a potential cycle is detected.
* @see Path#cycles(MongoPersistentProperty, String)
*/
void protect(MongoPersistentProperty property, String path) throws CyclicPropertyReferenceException {
@@ -365,7 +456,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
for (Path existingPath : paths) {
if (existingPath.cycles(property)) {
if (existingPath.cycles(property, path)) {
paths.add(new Path(property, path));
throw new CyclicPropertyReferenceException(property.getFieldName(), property.getOwner().getType(),
existingPath.getPath());
@@ -373,7 +464,6 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
}
paths.add(new Path(property, path));
} else {
ArrayList<Path> paths = new ArrayList<Path>();
@@ -386,7 +476,30 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
return property.getOwner().getType().getSimpleName() + ":" + property.getFieldName();
}
private static class Path {
/**
* Path defines the property and its full path from the document root. <br />
* A {@link Path} with {@literal spring.data.mongodb} would be created for the property {@code Three.mongodb}.
*
* <pre>
* <code>
* &#64;Document
* class One {
* Two spring;
* }
*
* class Two {
* Three data;
* }
*
* class Three {
* String mongodb;
* }
* </code>
* </pre>
*
* @author Christoph Strobl
*/
static class Path {
private final MongoPersistentProperty property;
private final String path;
@@ -401,17 +514,23 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
return path;
}
boolean cycles(MongoPersistentProperty property) {
/**
* Checks whether the given property is owned by the same entity and if it has been already visited by a subset of
* the current path. Given {@literal foo.bar.bar} cycles if {@literal foo.bar} has already been visited and
* {@code class Bar} contains a property of type {@code Bar}. The previously mentioned path would not cycle if
* {@code class Bar} contained a property of type {@code SomeEntity} named {@literal bar}.
*
* @param property
* @param path
* @return
*/
boolean cycles(MongoPersistentProperty property, String path) {
Pattern pattern = Pattern.compile("\\p{Punct}?" + Pattern.quote(property.getFieldName()) + "(\\p{Punct}|\\w)?");
Matcher matcher = pattern.matcher(path);
int count = 0;
while (matcher.find()) {
count++;
if (!property.getOwner().equals(this.property.getOwner())) {
return false;
}
return count >= 1 && property.getOwner().getType().equals(this.property.getOwner().getType());
return path.contains(this.path);
}
}
}
@@ -441,8 +560,8 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
*/
@Override
public String getMessage() {
return String.format("Found cycle for field '%s' in type '%s' for path '%s'", propertyName, type.getSimpleName(),
dotPath);
return String.format("Found cycle for field '%s' in type '%s' for path '%s'", propertyName,
type != null ? type.getSimpleName() : "unknown", dotPath);
}
}
@@ -511,4 +630,41 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
return indexDefinition.getIndexOptions();
}
}
/**
* @author Christoph Strobl
* @since 1.6
*/
static class TextIndexIncludeOptions {
enum IncludeStrategy {
FORCE, DEFAULT;
}
private final IncludeStrategy strategy;
private final TextIndexedFieldSpec parentFieldSpec;
public TextIndexIncludeOptions(IncludeStrategy strategy, TextIndexedFieldSpec parentFieldSpec) {
this.strategy = strategy;
this.parentFieldSpec = parentFieldSpec;
}
public TextIndexIncludeOptions(IncludeStrategy strategy) {
this(strategy, null);
}
public IncludeStrategy getStrategy() {
return strategy;
}
public TextIndexedFieldSpec getParentFieldSpec() {
return parentFieldSpec;
}
public boolean isForce() {
return IncludeStrategy.FORCE.equals(strategy);
}
}
}

View File

@@ -0,0 +1,336 @@
/*
* Copyright 2014 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.index;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
/**
* {@link IndexDefinition} to span multiple keys for text search.
*
* @author Christoph Strobl
* @since 1.6
*/
public class TextIndexDefinition implements IndexDefinition {
private String name;
private Set<TextIndexedFieldSpec> fieldSpecs;
private String defaultLanguage;
private String languageOverride;
TextIndexDefinition() {
fieldSpecs = new LinkedHashSet<TextIndexedFieldSpec>();
}
/**
* Creates a {@link TextIndexDefinition} for all fields in the document.
*
* @return
*/
public static TextIndexDefinition forAllFields() {
return new TextIndexDefinitionBuilder().onAllFields().build();
}
/**
* Get {@link TextIndexDefinitionBuilder} to create {@link TextIndexDefinition}.
*
* @return
*/
public static TextIndexDefinitionBuilder builder() {
return new TextIndexDefinitionBuilder();
}
/**
* @param fieldSpec
*/
public void addFieldSpec(TextIndexedFieldSpec fieldSpec) {
this.fieldSpecs.add(fieldSpec);
}
/**
* @param fieldSpecs
*/
public void addFieldSpecs(Collection<TextIndexedFieldSpec> fieldSpecs) {
this.fieldSpecs.addAll(fieldSpecs);
}
/**
* Returns if the {@link TextIndexDefinition} has fields assigned.
*
* @return
*/
public boolean hasFieldSpec() {
return !fieldSpecs.isEmpty();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.index.IndexDefinition#getIndexKeys()
*/
@Override
public DBObject getIndexKeys() {
DBObject keys = new BasicDBObject();
for (TextIndexedFieldSpec fieldSpec : fieldSpecs) {
keys.put(fieldSpec.fieldname, "text");
}
return keys;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.index.IndexDefinition#getIndexOptions()
*/
@Override
public DBObject getIndexOptions() {
DBObject options = new BasicDBObject();
if (StringUtils.hasText(name)) {
options.put("name", name);
}
if (StringUtils.hasText(defaultLanguage)) {
options.put("default_language", defaultLanguage);
}
BasicDBObject weightsDbo = new BasicDBObject();
for (TextIndexedFieldSpec fieldSpec : fieldSpecs) {
if (fieldSpec.isWeighted()) {
weightsDbo.put(fieldSpec.getFieldname(), fieldSpec.getWeight());
}
}
if (!weightsDbo.isEmpty()) {
options.put("weights", weightsDbo);
}
if (StringUtils.hasText(languageOverride)) {
options.put("language_override", languageOverride);
}
return options;
}
/**
* @author Christoph Strobl
* @since 1.6
*/
public static class TextIndexedFieldSpec {
private final String fieldname;
private final Float weight;
/**
* Create new {@link TextIndexedFieldSpec} for given fieldname without any weight.
*
* @param fieldname
*/
public TextIndexedFieldSpec(String fieldname) {
this(fieldname, 1.0F);
}
/**
* Create new {@link TextIndexedFieldSpec} for given fieldname and weight.
*
* @param fieldname
* @param weight
*/
public TextIndexedFieldSpec(String fieldname, Float weight) {
Assert.hasText(fieldname, "Text index field cannot be blank.");
this.fieldname = fieldname;
this.weight = weight != null ? weight : 1.0F;
}
/**
* Get the fieldname associated with the {@link TextIndexedFieldSpec}.
*
* @return
*/
public String getFieldname() {
return fieldname;
}
/**
* Get the weight associated with the {@link TextIndexedFieldSpec}.
*
* @return
*/
public Float getWeight() {
return weight;
}
/**
* @return true if {@link #weight} has a value that is a valid number.
*/
public boolean isWeighted() {
return this.weight != null && this.weight.compareTo(1.0F) != 0;
}
@Override
public int hashCode() {
return ObjectUtils.nullSafeHashCode(fieldname);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof TextIndexedFieldSpec)) {
return false;
}
TextIndexedFieldSpec other = (TextIndexedFieldSpec) obj;
return ObjectUtils.nullSafeEquals(this.fieldname, other.fieldname);
}
}
/**
* {@link TextIndexDefinitionBuilder} helps defining options for creating {@link TextIndexDefinition}.
*
* @author Christoph Strobl
* @since 1.6
*/
public static class TextIndexDefinitionBuilder {
private TextIndexDefinition instance;
private static final TextIndexedFieldSpec ALL_FIELDS = new TextIndexedFieldSpec("$**");
public TextIndexDefinitionBuilder() {
this.instance = new TextIndexDefinition();
}
/**
* Define the name to be used when creating the index in the store.
*
* @param name
* @return
*/
public TextIndexDefinitionBuilder named(String name) {
this.instance.name = name;
return this;
}
/**
* Define the index to span all fields using wilcard. <br/>
* <strong>NOTE</strong> {@link TextIndexDefinition} cannot contain any other fields when defined with wildcard.
*
* @return
*/
public TextIndexDefinitionBuilder onAllFields() {
if (!instance.fieldSpecs.isEmpty()) {
throw new InvalidDataAccessApiUsageException("Cannot add wildcard fieldspect to non empty.");
}
this.instance.fieldSpecs.add(ALL_FIELDS);
return this;
}
/**
* Include given fields with default weight.
*
* @param fieldnames
* @return
*/
public TextIndexDefinitionBuilder onFields(String... fieldnames) {
for (String fieldname : fieldnames) {
onField(fieldname);
}
return this;
}
/**
* Include given field with default weight.
*
* @param fieldname
* @return
*/
public TextIndexDefinitionBuilder onField(String fieldname) {
return onField(fieldname, Float.NaN);
}
/**
* Include given field with weight.
*
* @param fieldname
* @return
*/
public TextIndexDefinitionBuilder onField(String fieldname, Float weight) {
if (this.instance.fieldSpecs.contains(ALL_FIELDS)) {
throw new InvalidDataAccessApiUsageException(String.format("Cannot add %s to field spec for all fields.",
fieldname));
}
this.instance.fieldSpecs.add(new TextIndexedFieldSpec(fieldname, weight));
return this;
}
/**
* Define the default language to be used when indexing documents.
*
* @param language
* @see http://docs.mongodb.org/manual/tutorial/specify-language-for-text-index/#specify-default-language-text-index
* @return
*/
public TextIndexDefinitionBuilder withDefaultLanguage(String language) {
this.instance.defaultLanguage = language;
return this;
}
/**
* Define field for language override.
*
* @param fieldname
* @return
*/
public TextIndexDefinitionBuilder withLanguageOverride(String fieldname) {
if (StringUtils.hasText(this.instance.languageOverride)) {
throw new InvalidDataAccessApiUsageException(String.format(
"Cannot set language override on %s as it is already defined on %s.", fieldname,
this.instance.languageOverride));
}
this.instance.languageOverride = fieldname;
return this;
}
public TextIndexDefinition build() {
return this.instance;
}
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2014 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.index;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* {@link TextIndexed} marks a field to be part of the text index. As there can be only one text index per collection
* all fields marked with {@link TextIndexed} are combined into one single index. <br />
*
* @author Christoph Strobl
* @since 1.6
*/
@Documented
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface TextIndexed {
/**
* Defines the significance of the filed relative to other indexed fields. The value directly influences the documents
* score. <br/>
* Defaulted to {@literal 1.0}.
*
* @return
*/
float weight() default 1.0F;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011-2013 the original author or authors.
* Copyright 2011-2014 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.
@@ -38,6 +38,7 @@ import org.springframework.expression.ParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
@@ -47,12 +48,14 @@ import org.springframework.util.StringUtils;
* @author Jon Brisbin
* @author Oliver Gierke
* @author Thomas Darimont
* @author Christoph Strobl
*/
public class BasicMongoPersistentEntity<T> extends BasicPersistentEntity<T, MongoPersistentProperty> implements
MongoPersistentEntity<T>, ApplicationContextAware {
private static final String AMBIGUOUS_FIELD_MAPPING = "Ambiguous field mapping detected! Both %s and %s map to the same field name %s! Disambiguate using @DocumentField annotation!";
private final String collection;
private final String language;
private final SpelExpressionParser parser;
private final StandardEvaluationContext context;
@@ -75,8 +78,10 @@ public class BasicMongoPersistentEntity<T> extends BasicPersistentEntity<T, Mong
if (rawType.isAnnotationPresent(Document.class)) {
Document d = rawType.getAnnotation(Document.class);
this.collection = StringUtils.hasText(d.collection()) ? d.collection() : fallback;
this.language = StringUtils.hasText(d.language()) ? d.language() : "";
} else {
this.collection = fallback;
this.language = "";
}
}
@@ -100,6 +105,15 @@ public class BasicMongoPersistentEntity<T> extends BasicPersistentEntity<T, Mong
return expression.getValue(context, String.class);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.mapping.MongoPersistentEntity#getLanguage()
*/
@Override
public String getLanguage() {
return this.language;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.model.BasicPersistentEntity#verify()
@@ -107,12 +121,22 @@ public class BasicMongoPersistentEntity<T> extends BasicPersistentEntity<T, Mong
@Override
public void verify() {
verifyFieldUniqueness();
verifyFieldTypes();
}
private void verifyFieldUniqueness() {
AssertFieldNameUniquenessHandler handler = new AssertFieldNameUniquenessHandler();
doWithProperties(handler);
doWithAssociations(handler);
}
private void verifyFieldTypes() {
doWithProperties(new PropertyTypeAssertionHandler());
}
/**
* {@link Comparator} implementation inspecting the {@link MongoPersistentProperty}'s order.
*
@@ -226,4 +250,46 @@ public class BasicMongoPersistentEntity<T> extends BasicPersistentEntity<T, Mong
properties.put(fieldName, property);
}
}
/**
* @author Christoph Strobl
* @since 1.6
*/
private static class PropertyTypeAssertionHandler implements PropertyHandler<MongoPersistentProperty> {
@Override
public void doWithPersistentProperty(MongoPersistentProperty persistentProperty) {
potentiallyAssertTextScoreType(persistentProperty);
potentiallyAssertLanguageType(persistentProperty);
}
private void potentiallyAssertLanguageType(MongoPersistentProperty persistentProperty) {
if (persistentProperty.isLanguageProperty()) {
assertPropertyType(persistentProperty, String.class);
}
}
private void potentiallyAssertTextScoreType(MongoPersistentProperty persistentProperty) {
if (persistentProperty.isTextScoreProperty()) {
assertPropertyType(persistentProperty, Float.class, Double.class);
}
}
private void assertPropertyType(MongoPersistentProperty persistentProperty, Class<?>... validMatches) {
for (Class<?> potentialMatch : validMatches) {
if (ClassUtils.isAssignable(potentialMatch, persistentProperty.getActualType())) {
return;
}
}
throw new MappingException(String.format("Missmatching types for %s. Found %s expected one of %s.",
persistentProperty.getField(), persistentProperty.getActualType(),
StringUtils.arrayToCommaDelimitedString(validMatches)));
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011-2013 the original author or authors.
* Copyright 2011-2014 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.
@@ -27,7 +27,9 @@ import org.slf4j.LoggerFactory;
import org.springframework.data.annotation.Id;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.model.AnnotationBasedPersistentProperty;
import org.springframework.data.mapping.model.FieldNamingStrategy;
import org.springframework.data.mapping.model.MappingException;
import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.util.StringUtils;
@@ -39,6 +41,7 @@ import com.mongodb.DBObject;
* @author Oliver Gierke
* @author Patryk Wasik
* @author Thomas Darimont
* @author Christoph Strobl
*/
public class BasicMongoPersistentProperty extends AnnotationBasedPersistentProperty<MongoPersistentProperty> implements
MongoPersistentProperty {
@@ -46,6 +49,7 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope
private static final Logger LOG = LoggerFactory.getLogger(BasicMongoPersistentProperty.class);
private static final String ID_FIELD_NAME = "_id";
private static final String LANGUAGE_FIELD_NAME = "language";
private static final Set<Class<?>> SUPPORTED_ID_TYPES = new HashSet<Class<?>>();
private static final Set<String> SUPPORTED_ID_PROPERTY_NAMES = new HashSet<String>();
@@ -179,4 +183,22 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope
public DBRef getDBRef() {
return findAnnotation(DBRef.class);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.mapping.MongoPersistentProperty#isLanguageProperty()
*/
@Override
public boolean isLanguageProperty() {
return getFieldName().equals(LANGUAGE_FIELD_NAME) || isAnnotationPresent(Language.class);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.mapping.MongoPersistentProperty#isTextScoreProperty()
*/
@Override
public boolean isTextScoreProperty() {
return isAnnotationPresent(TextScore.class);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011-2013 the original author or authors.
* Copyright 2011-2014 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.
@@ -18,6 +18,7 @@ package org.springframework.data.mongodb.core.mapping;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import org.springframework.data.mapping.model.FieldNamingStrategy;
import org.springframework.data.mapping.model.SimpleTypeHolder;
/**

View File

@@ -1,39 +0,0 @@
/*
* Copyright 2013-2014 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.mapping;
/**
* {@link FieldNamingStrategy} that abbreviates field names by using the very first letter of the camel case parts of
* the {@link MongoPersistentProperty}'s name.
*
* @since 1.3
* @author Oliver Gierke
*/
public class CamelCaseAbbreviatingFieldNamingStrategy extends CamelCaseSplittingFieldNamingStrategy {
public CamelCaseAbbreviatingFieldNamingStrategy() {
super("");
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.mapping.CamelCaseSplittingFieldNamingStrategy#preparePart(java.lang.String)
*/
@Override
protected String preparePart(String part) {
return part.substring(0, 1);
}
}

View File

@@ -1,79 +0,0 @@
/*
* Copyright 2014 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.mapping;
import java.util.ArrayList;
import java.util.List;
import org.springframework.data.util.ParsingUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Configurable {@link FieldNamingStrategy} that splits up camel-case property names and reconcatenates them using a
* configured delimiter. Individual parts of the name can be manipulated using {@link #preparePart(String)}.
*
* @author Oliver Gierke
* @since 1.5
*/
public class CamelCaseSplittingFieldNamingStrategy implements FieldNamingStrategy {
private final String delimiter;
/**
* Creates a new {@link CamelCaseSplittingFieldNamingStrategy}.
*
* @param delimiter must not be {@literal null}.
*/
public CamelCaseSplittingFieldNamingStrategy(String delimiter) {
Assert.notNull(delimiter, "Delimiter must not be null!");
this.delimiter = delimiter;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.mapping.FieldNamingStrategy#getFieldName(org.springframework.data.mongodb.core.mapping.MongoPersistentProperty)
*/
@Override
public String getFieldName(MongoPersistentProperty property) {
List<String> parts = ParsingUtils.splitCamelCaseToLower(property.getName());
List<String> result = new ArrayList<String>();
for (String part : parts) {
String candidate = preparePart(part);
if (StringUtils.hasText(candidate)) {
result.add(candidate);
}
}
return StringUtils.collectionToDelimitedString(result, delimiter);
}
/**
* Callback to prepare the uncapitalized part obtained from the split up of the camel case source. Default
* implementation returns the part as is.
*
* @param part
* @return
*/
protected String preparePart(String part) {
return part;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2011 by the original author(s).
* Copyright (c) 2011-2014 by the original author(s).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -29,6 +29,7 @@ import org.springframework.data.annotation.Persistent;
*
* @author Jon Brisbin <jbrisbin@vmware.com>
* @author Oliver Gierke ogierke@vmware.com
* @author Christoph Strobl
*/
@Persistent
@Inherited
@@ -37,4 +38,13 @@ import org.springframework.data.annotation.Persistent;
public @interface Document {
String collection() default "";
/**
* Defines the default language to be used with this document.
*
* @since 1.6
* @return
*/
String language() default "";
}

View File

@@ -1,37 +0,0 @@
/*
* Copyright 2013 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.mapping;
/**
* SPI interface to determine how to name document fields in cases the field name is not manually defined.
*
* @see DocumentField
* @see PropertyNameFieldNamingStrategy
* @see CamelCaseAbbreviatingFieldNamingStrategy
* @see SnakeCaseFieldNamingStrategy
* @since 1.3
* @author Oliver Gierke
*/
public interface FieldNamingStrategy {
/**
* Returns the field name to be used for the given {@link MongoPersistentProperty}.
*
* @param property must not be {@literal null} or empty;
* @return
*/
String getFieldName(MongoPersistentProperty property);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2014 the original author or authors.
* Copyright 2014 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,20 +15,21 @@
*/
package org.springframework.data.mongodb.core.mapping;
/**
* {@link FieldNamingStrategy} that translates typical camel case Java property names to lower case JSON element names,
* separated by underscores.
*
* @since 1.5
* @author Ryan Tenney
* @author Oliver Gierke
*/
public class SnakeCaseFieldNamingStrategy extends CamelCaseSplittingFieldNamingStrategy {
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Mark property as language field.
*
* @author Christoph Strobl
* @since 1.6
*/
@Documented
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Language {
/**
* Creates a new {@link SnakeCaseFieldNamingStrategy}.
*/
public SnakeCaseFieldNamingStrategy() {
super("_");
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011-2013 the original author or authors.
* Copyright 2011-2014 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.
@@ -24,6 +24,8 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.mapping.context.AbstractMappingContext;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.FieldNamingStrategy;
import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.util.TypeInformation;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011-2012 the original author or authors.
* Copyright 2011-2014 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.
@@ -21,6 +21,7 @@ import org.springframework.data.mapping.PersistentEntity;
* MongoDB specific {@link PersistentEntity} abstraction.
*
* @author Oliver Gierke
* @author Christoph Strobl
*/
public interface MongoPersistentEntity<T> extends PersistentEntity<T, MongoPersistentProperty> {
@@ -30,4 +31,13 @@ public interface MongoPersistentEntity<T> extends PersistentEntity<T, MongoPersi
* @return
*/
String getCollection();
/**
* Returns the default language to be used for this entity.
*
* @since 1.6
* @return
*/
String getLanguage();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011-2013 the original author or authors.
* Copyright 2011-2014 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.
@@ -26,6 +26,7 @@ import org.springframework.data.mapping.PersistentProperty;
* @author Oliver Gierke
* @author Patryk Wasik
* @author Thomas Darimont
* @author Christoph Strobl
*/
public interface MongoPersistentProperty extends PersistentProperty<MongoPersistentProperty> {
@@ -59,6 +60,24 @@ public interface MongoPersistentProperty extends PersistentProperty<MongoPersist
*/
boolean isExplicitIdProperty();
/**
* Returns whether the property indicates the documents language either by having a {@link #getFieldName()} equal to
* {@literal language} or being annotated with {@link Language}.
*
* @return
* @since 1.6
*/
boolean isLanguageProperty();
/**
* Returns whether the property holds the documents score calculated by text search. <br/>
* It's marked with {@link TextScore}.
*
* @return
* @since 1.6
*/
boolean isTextScoreProperty();
/**
* Returns the {@link DBRef} if the property is a reference.
*

View File

@@ -1,35 +0,0 @@
/*
* Copyright 2013 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.mapping;
/**
* {@link FieldNamingStrategy} simply using the {@link MongoPersistentProperty}'s name.
*
* @since 1.3
* @author Oliver Gierke
*/
public enum PropertyNameFieldNamingStrategy implements FieldNamingStrategy {
INSTANCE;
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.mapping.FieldNamingStrategy#getFieldName(org.springframework.data.mongodb.core.mapping.MongoPersistentProperty)
*/
public String getFieldName(MongoPersistentProperty property) {
return property.getName();
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2014 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.mapping;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.data.annotation.ReadOnlyProperty;
/**
* {@link TextScore} marks the property to be considered as the on server calculated {@literal textScore} when doing
* full text search. <br />
* <b>NOTE</b> Property will not be written when saving entity.
*
* @author Christoph Strobl
* @since 1.6
*/
@ReadOnlyProperty
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface TextScore {
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2010-2011 the original author or authors.
* Copyright 2010-2014 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.
@@ -24,11 +24,12 @@ import com.mongodb.util.JSON;
*
* @author Thomas Risberg
* @author Oliver Gierke
* @author Christoph Strobl
*/
public class BasicQuery extends Query {
private final DBObject queryObject;
private final DBObject fieldsObject;
private DBObject fieldsObject;
private DBObject sortObject;
public BasicQuery(String query) {
@@ -84,4 +85,12 @@ public class BasicQuery extends Query {
public void setSortObject(DBObject sortObject) {
this.sortObject = sortObject;
}
/**
* @since 1.6
* @param fieldsObject
*/
protected void setFieldsObject(DBObject fieldsObject) {
this.fieldsObject = fieldsObject;
}
}

View File

@@ -15,10 +15,11 @@
*/
package org.springframework.data.mongodb.core.query;
import static org.springframework.util.ObjectUtils.*;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
@@ -40,6 +41,7 @@ import com.mongodb.DBObject;
* @author Oliver Gierke
* @author Becca Gaspard
* @author Christoph Strobl
* @author Thomas Darimont
*/
public class Update {
@@ -278,7 +280,36 @@ public class Update {
return this;
}
/**
* Update given key to current date using {@literal $currentDate} modifier.
*
* @see http://docs.mongodb.org/manual/reference/operator/update/currentDate/
* @param key
* @return
* @since 1.6
*/
public Update currentDate(String key) {
addMultiFieldOperation("$currentDate", key, true);
return this;
}
/**
* Update given key to current date using {@literal $currentDate : &#123; $type : "timestamp" &#125;} modifier.
*
* @see http://docs.mongodb.org/manual/reference/operator/update/currentDate/
* @param key
* @return
* @since 1.6
*/
public Update currentTimestamp(String key) {
addMultiFieldOperation("$currentDate", key, new BasicDBObject("$type", "timestamp"));
return this;
}
public DBObject getUpdateObject() {
DBObject dbo = new BasicDBObject();
for (String k : modifierOps.keySet()) {
dbo.put(k, modifierOps.get(k));
@@ -335,14 +366,52 @@ public class Update {
return StringUtils.startsWithIgnoreCase(key, "$");
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return getUpdateObject().hashCode();
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Update that = (Update) obj;
return this.getUpdateObject().equals(that.getUpdateObject());
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return getUpdateObject().toString();
}
/**
* Modifiers holds a distinct collection of {@link Modifier}
*
* @author Christoph Strobl
* @author Thomas Darimont
*/
public static class Modifiers {
private HashMap<String, Modifier> modifiers;
private Map<String, Modifier> modifiers;
public Modifiers() {
this.modifiers = new LinkedHashMap<String, Modifier>(1);
@@ -355,6 +424,33 @@ public class Update {
public void addModifier(Modifier modifier) {
this.modifiers.put(modifier.getKey(), modifier);
}
/* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return nullSafeHashCode(modifiers);
}
/* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Modifiers that = (Modifiers) obj;
return this.modifiers.equals(that.modifiers);
}
}
/**
@@ -379,6 +475,7 @@ public class Update {
* Implementation of {@link Modifier} representing {@code $each}.
*
* @author Christoph Strobl
* @author Thomas Darimont
*/
private static class Each implements Modifier {
@@ -399,6 +496,7 @@ public class Update {
}
Object[] convertedValues = new Object[values.length];
for (int i = 0; i < values.length; i++) {
convertedValues[i] = values[i];
}
@@ -406,21 +504,57 @@ public class Update {
return convertedValues;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.query.Update.Modifier#getKey()
*/
@Override
public String getKey() {
return "$each";
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.query.Update.Modifier#getValue()
*/
@Override
public Object getValue() {
return this.values;
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return nullSafeHashCode(values);
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object that) {
if (this == that) {
return true;
}
if (that == null || getClass() != that.getClass()) {
return false;
}
return nullSafeEquals(values, ((Each) that).values);
}
}
/**
* Builder for creating {@code $push} modifiers
*
* @author Christoph Strobl
* @author Thomas Darimont
*/
public class PushOperatorBuilder {
@@ -453,6 +587,50 @@ public class Update {
public Update value(Object value) {
return Update.this.push(key, value);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
int result = 17;
result += 31 * result + getOuterType().hashCode();
result += 31 * result + nullSafeHashCode(key);
result += 31 * result + nullSafeHashCode(modifiers);
return result;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
PushOperatorBuilder that = (PushOperatorBuilder) obj;
if (!getOuterType().equals(that.getOuterType())) {
return false;
}
return nullSafeEquals(this.key, that.key) && nullSafeEquals(this.modifiers, that.modifiers);
}
private Update getOuterType() {
return Update.this;
}
}
/**
@@ -488,7 +666,5 @@ public class Update {
public Update value(Object value) {
return Update.this.addToSet(this.key, value);
}
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright 2014 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.query.text;
/**
* A {@link Term} defines one or multiple words {@link Type#WORD} or phrases {@link Type#PHRASE} to be used in the
* context of full text search.
*
* @author Christoph Strobl
* @since 1.6
*/
public class Term {
enum Type {
WORD, PHRASE;
}
private final Type type;
private final String raw;
private boolean negated;
/**
* Creates a new {@link Term} of {@link Type#WORD}.
*
* @param raw
*/
public Term(String raw) {
this(raw, Type.WORD);
}
/**
* Creates a new {@link Term} of given {@link Type}.
*
* @param raw
* @param type defaulted to {@link Type#WORD} if {@literal null}.
*/
public Term(String raw, Type type) {
this.raw = raw;
this.type = type == null ? Type.WORD : type;
}
/**
* Negates the term.
*
* @return
*/
public Term negate() {
this.negated = true;
return this;
}
/**
* @return return true if term is negated.
*/
public boolean isNegated() {
return negated;
}
/**
* @return type of term. Never {@literal null}.
*/
public Type getType() {
return type;
}
/**
* Get formatted representation of term.
*
* @return
*/
public String getFormatted() {
String formatted = Type.PHRASE.equals(type) ? quotePhrase(raw) : raw;
return negated ? negateRaw(formatted) : formatted;
}
@Override
public String toString() {
return getFormatted();
}
protected String quotePhrase(String raw) {
return "\"" + raw + "\"";
}
protected String negateRaw(String raw) {
return "-" + raw;
}
}

View File

@@ -0,0 +1,223 @@
/*
* Copyright 2014 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.query.text;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.CriteriaDefinition;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import com.mongodb.BasicDBObject;
import com.mongodb.BasicDBObjectBuilder;
import com.mongodb.DBObject;
/**
* Implementation of {@link CriteriaDefinition} to be used for full text search .
*
* @author Christoph Strobl
* @since 1.6
*/
public class TextCriteria extends Criteria {
private String language;
private List<Term> terms;
public TextCriteria() {
this.terms = new ArrayList<Term>();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.query.CriteriaDefinition#getCriteriaObject()
*/
@Override
public DBObject getCriteriaObject() {
BasicDBObjectBuilder builder = new BasicDBObjectBuilder();
if (StringUtils.hasText(language)) {
builder.add("$language", language);
}
if (!CollectionUtils.isEmpty(terms)) {
builder.add("$search", join(terms.iterator()));
}
return new BasicDBObject("$text", builder.get());
}
/**
* @param words
* @return
*/
public TextCriteria matchingAny(String... words) {
for (String word : words) {
matching(word);
}
return this;
}
/**
* Add given {@link Term} to criteria.
*
* @param term must not be null.
*/
public void matching(Term term) {
Assert.notNull(term, "Term to add must not be null.");
this.terms.add(term);
}
private void notMatching(Term term) {
matching(term.negate());
}
/**
* @param term
* @return
*/
public TextCriteria matching(String term) {
if (StringUtils.hasText(term)) {
matching(new Term(term));
}
return this;
}
/**
* @param term
* @return
*/
public TextCriteria notMatching(String term) {
if (StringUtils.hasText(term)) {
notMatching(new Term(term, Term.Type.WORD));
}
return this;
}
/**
* @param words
* @return
*/
public TextCriteria notMatchingAny(String... words) {
for (String word : words) {
notMatching(word);
}
return this;
}
/**
* Given value will treated as a single phrase.
*
* @param phrase
* @return
*/
public TextCriteria notMatchingPhrase(String phrase) {
if (StringUtils.hasText(phrase)) {
notMatching(new Term(phrase, Term.Type.PHRASE));
}
return this;
}
/**
* Given value will treated as a single phrase.
*
* @param phrase
* @return
*/
public TextCriteria matchingPhrase(String phrase) {
if (StringUtils.hasText(phrase)) {
matching(new Term(phrase, Term.Type.PHRASE));
}
return this;
}
/**
* @return
*/
public static TextCriteria forDefaultLanguage() {
return new TextCriteriaBuilder().build();
}
/**
* For a full list of supported languages see the mongdodb reference manual for <a
* href="http://docs.mongodb.org/manual/reference/text-search-languages/">Text Search Languages</a>.
*
* @param language
* @return
*/
public static TextCriteria forLanguage(String language) {
return new TextCriteriaBuilder().withLanguage(language).build();
}
private static String join(Iterator<Term> iterator) {
Term first = iterator.next();
if (!iterator.hasNext()) {
return first.getFormatted();
}
StringBuilder buf = new StringBuilder(256);
if (first != null) {
buf.append(first);
}
while (iterator.hasNext()) {
buf.append(' ');
Term obj = iterator.next();
if (obj != null) {
buf.append(obj.getFormatted());
}
}
return buf.toString();
}
public static class TextCriteriaBuilder {
private TextCriteria instance;
public TextCriteriaBuilder() {
this.instance = new TextCriteria();
}
public TextCriteriaBuilder withLanguage(String language) {
this.instance.language = language;
return this;
}
public TextCriteria build() {
return this.instance;
}
}
@Override
public String getKey() {
return "$text";
}
}

View File

@@ -0,0 +1,178 @@
/*
* Copyright 2014 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.query.text;
import java.util.Locale;
import org.springframework.data.mongodb.core.query.Query;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
/**
* {@link Query} implementation to be used to for performing full text searches.
*
* @author Christoph Strobl
* @since 1.6
*/
public class TextQuery extends Query {
private final String DEFAULT_SCORE_FIELD_FIELDNAME = "score";
private final DBObject META_TEXT_SCORE = new BasicDBObject("$meta", "textScore");
private String scoreFieldName = DEFAULT_SCORE_FIELD_FIELDNAME;
private boolean includeScore = false;
private boolean sortByScore = false;
/**
* Creates new {@link TextQuery} using the the given {@code wordsAndPhrases} with {@link TextCriteria}
*
* @param wordsAndPhrases
* @see TextCriteria#matching(String)
*/
public TextQuery(String wordsAndPhrases) {
super(TextCriteria.forDefaultLanguage().matching(wordsAndPhrases));
}
/**
* Creates new {@link TextQuery} in {@code language}. <br />
* For a full list of supported languages see the mongdodb reference manual for <a
* href="http://docs.mongodb.org/manual/reference/text-search-languages/">Text Search Languages</a>.
*
* @param wordsAndPhrases
* @param language
* @see TextCriteria#forLanguage(String)
* @see TextCriteria#matching(String)
*/
public TextQuery(String wordsAndPhrases, String language) {
super(TextCriteria.forLanguage(language).matching(wordsAndPhrases));
}
/**
* Creates new {@link TextQuery} using the {@code locale}s language.<br />
* For a full list of supported languages see the mongdodb reference manual for <a
* href="http://docs.mongodb.org/manual/reference/text-search-languages/">Text Search Languages</a>.
*
* @param wordsAndPhrases
* @param locale
*/
public TextQuery(String wordsAndPhrases, Locale locale) {
this(wordsAndPhrases, locale != null ? locale.getLanguage() : (String) null);
}
/**
* Creates new {@link TextQuery} for given {@link TextCriteria}.
*
* @param criteria.
*/
public TextQuery(TextCriteria criteria) {
super(criteria);
}
/**
* Creates new {@link TextQuery} searching for given {@link TextCriteria}.
*
* @param criteria
* @return
*/
public static TextQuery queryText(TextCriteria criteria) {
return new TextQuery(criteria);
}
/**
* Add sorting by text score. Will also add text score to returned fields.
*
* @see TextQuery#includeScore()
* @return
*/
public TextQuery sortByScore() {
this.includeScore();
this.sortByScore = true;
return this;
}
/**
* Add field {@literal score} holding the documents textScore to the returned fields.
*
* @return
*/
public TextQuery includeScore() {
this.includeScore = true;
return this;
}
/**
* Include text search document score in returned fields using the given fieldname.
*
* @param fieldname
* @return
*/
public TextQuery includeScore(String fieldname) {
setScoreFieldName(fieldname);
includeScore();
return this;
}
/**
* Set the fieldname used for scoring.
*
* @param fieldName
*/
public void setScoreFieldName(String fieldName) {
this.scoreFieldName = fieldName;
}
/**
* Get the fieldname used for scoring
*
* @return
*/
public String getScoreFieldName() {
return scoreFieldName;
}
@Override
public DBObject getFieldsObject() {
if (!this.includeScore) {
return super.getFieldsObject();
}
DBObject fields = super.getFieldsObject();
if (fields == null) {
fields = new BasicDBObject();
}
fields.put(getScoreFieldName(), META_TEXT_SCORE);
return fields;
}
@Override
public DBObject getSortObject() {
DBObject sort = new BasicDBObject();
if (this.sortByScore) {
sort.put(getScoreFieldName(), META_TEXT_SCORE);
}
if (super.getSortObject() != null) {
sort.putAll(super.getSortObject());
}
return sort;
}
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.mongodb.repository.query;
import java.util.Collections;
import java.util.List;
import org.springframework.core.convert.ConversionService;
@@ -254,10 +255,26 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
Object execute(Query query) {
MongoEntityMetadata<?> metadata = method.getEntityInformation();
int overallLimit = query.getLimit();
long count = operations.count(query, metadata.getCollectionName());
count = overallLimit != 0 ? Math.min(count, query.getLimit()) : count;
List<?> result = operations.find(query.with(pageable), metadata.getJavaType(), metadata.getCollectionName());
boolean pageableOutOfScope = pageable.getOffset() > query.getLimit();
if (pageableOutOfScope) {
return new PageImpl<Object>(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, metadata.getJavaType(), metadata.getCollectionName());
return new PageImpl(result, pageable, count);
}
}
@@ -391,7 +408,7 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
return operations.findAllAndRemove(query, metadata.getJavaType());
}
WriteResult writeResult = operations.remove(query, metadata.getCollectionName());
WriteResult writeResult = operations.remove(query, metadata.getJavaType(), metadata.getCollectionName());
return writeResult != null ? writeResult.getN() : 0L;
}
}

View File

@@ -19,10 +19,14 @@ import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.util.StringUtils;
import com.mongodb.util.JSONParseException;
/**
* {@link RepositoryQuery} implementation for Mongo.
@@ -67,7 +71,28 @@ public class PartTreeMongoQuery extends AbstractMongoQuery {
protected Query createQuery(ConvertingParameterAccessor accessor) {
MongoQueryCreator creator = new MongoQueryCreator(tree, accessor, context, isGeoNearQuery);
return creator.createQuery();
Query query = creator.createQuery();
if (tree.isLimiting()) {
query.limit(tree.getMaxResults());
}
String fieldSpec = this.getQueryMethod().getFieldSpecification();
if (!StringUtils.hasText(fieldSpec)) {
return query;
}
try {
BasicQuery result = new BasicQuery(query.getQueryObject().toString(), fieldSpec);
result.setSortObject(query.getSortObject());
return result;
} catch (JSONParseException o_O) {
throw new IllegalStateException(String.format("Invalid query or field specification in %s!", getQueryMethod(),
o_O));
}
}
/*

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011-2012 the original author or authors.
* Copyright 2011-2014 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.
@@ -25,7 +25,10 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.util.Assert;
import com.mongodb.DBObject;
import com.mongodb.DBRef;
import com.mysema.query.mongodb.MongodbSerializer;
import com.mysema.query.types.Constant;
import com.mysema.query.types.Operation;
import com.mysema.query.types.Path;
import com.mysema.query.types.PathMetadata;
import com.mysema.query.types.PathType;
@@ -34,9 +37,12 @@ import com.mysema.query.types.PathType;
* Custom {@link MongodbSerializer} to take mapping information into account when building keys for constraints.
*
* @author Oliver Gierke
* @author Christoph Strobl
*/
class SpringDataMongodbSerializer extends MongodbSerializer {
private final String ID_KEY = "_id";
private final MongoConverter converter;
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
private final QueryMapper mapper;
@@ -44,7 +50,7 @@ class SpringDataMongodbSerializer extends MongodbSerializer {
/**
* Creates a new {@link SpringDataMongodbSerializer} for the given {@link MappingContext}.
*
* @param mappingContext
* @param mappingContext must not be {@literal null}.
*/
public SpringDataMongodbSerializer(MongoConverter converter) {
@@ -80,10 +86,63 @@ class SpringDataMongodbSerializer extends MongodbSerializer {
@Override
protected DBObject asDBObject(String key, Object value) {
if ("_id".equals(key)) {
return super.asDBObject(key, mapper.convertId(value));
if (ID_KEY.equals(key)) {
return mapper.getMappedObject(super.asDBObject(key, value), null);
}
return super.asDBObject(key, value instanceof Pattern ? value : converter.convertToMongoType(value));
}
/*
* (non-Javadoc)
* @see com.mysema.query.mongodb.MongodbSerializer#isReference(com.mysema.query.types.Path)
*/
@Override
protected boolean isReference(Path<?> path) {
MongoPersistentProperty property = getPropertyFor(path);
return property == null ? false : property.isAssociation();
}
/*
* (non-Javadoc)
* @see com.mysema.query.mongodb.MongodbSerializer#asReference(java.lang.Object)
*/
@Override
protected DBRef asReference(Object constant) {
return converter.toDBRef(constant, null);
}
/*
* (non-Javadoc)
* @see com.mysema.query.mongodb.MongodbSerializer#asReference(com.mysema.query.types.Operation, int)
*/
@Override
protected DBRef asReference(Operation<?> expr, int constIndex) {
for (Object arg : expr.getArgs()) {
if (arg instanceof Path) {
MongoPersistentProperty property = getPropertyFor((Path<?>) arg);
Object constant = ((Constant<?>) expr.getArg(constIndex)).getConstant();
return converter.toDBRef(constant, property);
}
}
return super.asReference(expr, constIndex);
}
private MongoPersistentProperty getPropertyFor(Path<?> path) {
Path<?> parent = path.getMetadata().getParent();
if (parent == null) {
return null;
}
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(parent.getType());
return entity != null ? entity.getPersistentProperty(path.getMetadata().getName()) : null;
}
}

View File

@@ -34,11 +34,11 @@ import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.mapping.model.CamelCaseAbbreviatingFieldNamingStrategy;
import org.springframework.data.mongodb.core.convert.CustomConversions;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoTypeMapper;
import org.springframework.data.mongodb.core.mapping.Account;
import org.springframework.data.mongodb.core.mapping.CamelCaseAbbreviatingFieldNamingStrategy;
import org.springframework.data.mongodb.repository.Person;
import org.springframework.stereotype.Component;

View File

@@ -2693,6 +2693,22 @@ public class MongoTemplateTests {
assertThat(result.getContent().getName(), is(content.getName()));
}
/**
* @see DATAMONGO-970
*/
@Test
public void insertsAndRemovesBasicDbObjectCorrectly() {
BasicDBObject object = new BasicDBObject("key", "value");
template.insert(object, "collection");
assertThat(object.get("_id"), is(notNullValue()));
assertThat(template.findAll(DBObject.class, "collection"), hasSize(1));
template.remove(object, "collection");
assertThat(template.findAll(DBObject.class, "collection"), hasSize(0));
}
static class DoucmentWithNamedIdField {
@Id String someIdKey;

View File

@@ -42,6 +42,7 @@ import org.springframework.dao.DataAccessException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Version;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.convert.CustomConversions;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
@@ -50,11 +51,13 @@ import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCreator;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.test.util.ReflectionTestUtils;
import com.mongodb.BasicDBObject;
import com.mongodb.BasicDBObjectBuilder;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
@@ -329,6 +332,26 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
verify(collection, never()).remove(Mockito.any(DBObject.class));
}
/**
* @see DATAMONGO-948
*/
@Test
public void sortShouldBeTakenAsIsWhenExecutingQueryWithoutSpecificTypeInformation() {
Query query = Query.query(Criteria.where("foo").is("bar")).with(new Sort("foo"));
template.executeQuery(query, "collection1", new DocumentCallbackHandler() {
@Override
public void processDocument(DBObject dbObject) throws MongoException, DataAccessException {
// nothing to do - just a test
}
});
ArgumentCaptor<DBObject> captor = ArgumentCaptor.forClass(DBObject.class);
verify(cursor, times(1)).sort(captor.capture());
assertThat(captor.getValue(), equalTo(new BasicDBObjectBuilder().add("foo", 1).get()));
}
class AutogenerateableId {
@Id BigInteger id;

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2014 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 static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
import org.junit.Before;
import org.junit.Test;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
/**
* Unit tests for {@link AggregationOptions}.
*
* @author Thomas Darimont
* @since 1.6
*/
public class AggregationOptionsTests {
AggregationOptions aggregationOptions;
@Before
public void setup() {
aggregationOptions = newAggregationOptions().explain(true).cursor(new BasicDBObject("foo", 1)).allowDiskUse(true)
.build();
}
/**
* @see DATAMONGO-960
*/
@Test
public void aggregationOptionsBuilderShouldSetOptionsAccordingly() {
assertThat(aggregationOptions.isAllowDiskUse(), is(true));
assertThat(aggregationOptions.isExplain(), is(true));
assertThat(aggregationOptions.getCursor(), is((DBObject) new BasicDBObject("foo", 1)));
}
/**
* @see DATAMONGO-960
*/
@Test
public void aggregationOptionsToString() {
assertThat(aggregationOptions.toString(),
is("{ \"allowDiskUse\" : true , \"explain\" : true , \"cursor\" : { \"foo\" : 1}}"));
}
}

View File

@@ -31,6 +31,8 @@ import java.util.Date;
import java.util.List;
import java.util.Scanner;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDateTime;
import org.junit.After;
import org.junit.Before;
@@ -44,11 +46,14 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.dao.DataAccessException;
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.mapping.model.MappingException;
import org.springframework.data.mongodb.core.CollectionCallback;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.AggregationTests.CarDescriptor.Entry;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.repository.Person;
import org.springframework.data.util.Version;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -75,6 +80,7 @@ public class AggregationTests {
private static final String INPUT_COLLECTION = "aggregation_test_collection";
private static final Logger LOGGER = LoggerFactory.getLogger(AggregationTests.class);
private static final Version TWO_DOT_FOUR = new Version(2, 4);
private static final Version TWO_DOT_SIX = new Version(2, 6);
private static boolean initialized = false;
@@ -112,6 +118,8 @@ public class AggregationTests {
mongoTemplate.dropCollection(Data.class);
mongoTemplate.dropCollection(DATAMONGO788.class);
mongoTemplate.dropCollection(User.class);
mongoTemplate.dropCollection(Person.class);
mongoTemplate.dropCollection(Reservation.class);
}
/**
@@ -414,23 +422,7 @@ public class AggregationTests {
createUserWithLikesDocuments();
/*
...
$group: {
_id:"$like",
number:{ $sum:1}
}
...
*/
TypedAggregation<UserWithLikes> agg = newAggregation(UserWithLikes.class, //
unwind("likes"), //
group("likes").count().as("number"), //
sort(DESC, "number"), //
limit(5), //
sort(ASC, previousOperation()) //
);
TypedAggregation<UserWithLikes> agg = createUsersWithCommonLikesAggregation();
assertThat(agg, is(notNullValue()));
assertThat(agg.toString(), is(notNullValue()));
@@ -447,6 +439,16 @@ public class AggregationTests {
assertLikeStats(result.getMappedResults().get(4), "e", 3);
}
protected TypedAggregation<UserWithLikes> createUsersWithCommonLikesAggregation() {
return newAggregation(UserWithLikes.class, //
unwind("likes"), //
group("likes").count().as("number"), //
sort(DESC, "number"), //
limit(5), //
sort(ASC, previousOperation()) //
);
}
@Test
public void arithmenticOperatorsInProjectionExample() {
@@ -857,6 +859,165 @@ public class AggregationTests {
assertThat(result.getMappedResults(), hasSize(3));
}
/**
* @see DATAMONGO-960
*/
@Test
public void returnFiveMostCommonLikesAggregationFrameworkExampleWithSortOnDiskOptionEnabled() {
assumeTrue(mongoVersion.isGreaterThanOrEqualTo(TWO_DOT_SIX));
createUserWithLikesDocuments();
TypedAggregation<UserWithLikes> agg = createUsersWithCommonLikesAggregation() //
.withOptions(newAggregationOptions().allowDiskUse(true).build());
assertThat(agg, is(notNullValue()));
assertThat(agg.toString(), is(notNullValue()));
AggregationResults<LikeStats> result = mongoTemplate.aggregate(agg, LikeStats.class);
assertThat(result, is(notNullValue()));
assertThat(result.getMappedResults(), is(notNullValue()));
assertThat(result.getMappedResults().size(), is(5));
assertLikeStats(result.getMappedResults().get(0), "a", 4);
assertLikeStats(result.getMappedResults().get(1), "b", 2);
assertLikeStats(result.getMappedResults().get(2), "c", 4);
assertLikeStats(result.getMappedResults().get(3), "d", 2);
assertLikeStats(result.getMappedResults().get(4), "e", 3);
}
/**
* @see DATAMONGO-960
*/
@Test
public void returnFiveMostCommonLikesShouldReturnStageExecutionInformationWithExplainOptionEnabled() {
assumeTrue(mongoVersion.isGreaterThanOrEqualTo(TWO_DOT_SIX));
createUserWithLikesDocuments();
TypedAggregation<UserWithLikes> agg = createUsersWithCommonLikesAggregation() //
.withOptions(newAggregationOptions().explain(true).build());
AggregationResults<LikeStats> result = mongoTemplate.aggregate(agg, LikeStats.class);
assertThat(result.getMappedResults(), is(empty()));
DBObject rawResult = result.getRawResults();
assertThat(rawResult, is(notNullValue()));
assertThat(rawResult.containsField("stages"), is(true));
}
/**
* @see DATAMONGO-954
*/
@Test
public void shouldSupportReturningCurrentAggregationRoot() {
assumeTrue(mongoVersion.isGreaterThanOrEqualTo(TWO_DOT_SIX));
mongoTemplate.save(new Person("p1_first", "p1_last", 25));
mongoTemplate.save(new Person("p2_first", "p2_last", 32));
mongoTemplate.save(new Person("p3_first", "p3_last", 25));
mongoTemplate.save(new Person("p4_first", "p4_last", 15));
List<DBObject> personsWithAge25 = mongoTemplate.find(Query.query(where("age").is(25)), DBObject.class,
mongoTemplate.getCollectionName(Person.class));
Aggregation agg = newAggregation(group("age").push(Aggregation.ROOT).as("users"));
AggregationResults<DBObject> result = mongoTemplate.aggregate(agg, Person.class, DBObject.class);
assertThat(result.getMappedResults(), hasSize(3));
DBObject o = (DBObject) result.getMappedResults().get(2);
assertThat(o.get("_id"), is((Object) 25));
assertThat((List<?>) o.get("users"), hasSize(2));
assertThat((List<?>) o.get("users"), is(contains(personsWithAge25.toArray())));
}
/**
* @see DATAMONGO-954
* @see http
* ://stackoverflow.com/questions/24185987/using-root-inside-spring-data-mongodb-for-retrieving-whole-document
*/
@Test
public void shouldSupportReturningCurrentAggregationRootInReference() {
assumeTrue(mongoVersion.isGreaterThanOrEqualTo(TWO_DOT_SIX));
mongoTemplate.save(new Reservation("0123", "42", 100));
mongoTemplate.save(new Reservation("0360", "43", 200));
mongoTemplate.save(new Reservation("0360", "44", 300));
Aggregation agg = newAggregation( //
match(where("hotelCode").is("0360")), //
sort(Direction.DESC, "confirmationNumber", "timestamp"), //
group("confirmationNumber") //
.first("timestamp").as("timestamp") //
.first(Aggregation.ROOT).as("reservationImage") //
);
AggregationResults<DBObject> result = mongoTemplate.aggregate(agg, Reservation.class, DBObject.class);
assertThat(result.getMappedResults(), hasSize(2));
}
/**
* @see DATAMONGO-975
*/
@Test
public void shouldRetrieveDateTimeFragementsCorrectly() throws Exception {
mongoTemplate.dropCollection(ObjectWithDate.class);
DateTime dateTime = new DateTime() //
.withYear(2014) //
.withMonthOfYear(2) //
.withDayOfMonth(7) //
.withTime(3, 4, 5, 6).toDateTime(DateTimeZone.UTC).toDateTimeISO();
ObjectWithDate owd = new ObjectWithDate(dateTime.toDate());
mongoTemplate.insert(owd);
ProjectionOperation dateProjection = Aggregation.project() //
.and("dateValue").extractHour().as("hour") //
.and("dateValue").extractMinute().as("min") //
.and("dateValue").extractSecond().as("second") //
.and("dateValue").extractMillisecond().as("millis") //
.and("dateValue").extractYear().as("year") //
.and("dateValue").extractMonth().as("month") //
.and("dateValue").extractWeek().as("week") //
.and("dateValue").extractDayOfYear().as("dayOfYear") //
.and("dateValue").extractDayOfMonth().as("dayOfMonth") //
.and("dateValue").extractDayOfWeek().as("dayOfWeek") //
.andExpression("dateValue + 86400000").extractDayOfYear().as("dayOfYearPlus1Day") //
.andExpression("dateValue + 86400000").project("dayOfYear").as("dayOfYearPlus1DayManually") //
;
Aggregation agg = newAggregation(dateProjection);
AggregationResults<DBObject> result = mongoTemplate.aggregate(agg, ObjectWithDate.class, DBObject.class);
assertThat(result.getMappedResults(), hasSize(1));
DBObject dbo = result.getMappedResults().get(0);
assertThat(dbo.get("hour"), is((Object) dateTime.getHourOfDay()));
assertThat(dbo.get("min"), is((Object) dateTime.getMinuteOfHour()));
assertThat(dbo.get("second"), is((Object) dateTime.getSecondOfMinute()));
assertThat(dbo.get("millis"), is((Object) dateTime.getMillisOfSecond()));
assertThat(dbo.get("year"), is((Object) dateTime.getYear()));
assertThat(dbo.get("month"), is((Object) dateTime.getMonthOfYear()));
// dateTime.getWeekOfWeekyear()) returns 6 since for MongoDB the week starts on sunday and not on monday.
assertThat(dbo.get("week"), is((Object) 5));
assertThat(dbo.get("dayOfYear"), is((Object) dateTime.getDayOfYear()));
assertThat(dbo.get("dayOfMonth"), is((Object) dateTime.getDayOfMonth()));
// dateTime.getDayOfWeek()
assertThat(dbo.get("dayOfWeek"), is((Object) 6));
assertThat(dbo.get("dayOfYearPlus1Day"), is((Object) dateTime.plusDays(1).getDayOfYear()));
assertThat(dbo.get("dayOfYearPlus1DayManually"), is((Object) dateTime.plusDays(1).getDayOfYear()));
}
private void assertLikeStats(LikeStats like, String id, long count) {
assertThat(like, is(notNullValue()));
@@ -992,6 +1153,7 @@ public class AggregationTests {
}
}
@SuppressWarnings("unused")
static class Descriptors {
private CarDescriptor carDescriptor;
}
@@ -1007,6 +1169,7 @@ public class AggregationTests {
}
}
@SuppressWarnings("unused")
static class Entry {
private String make;
private String model;
@@ -1021,4 +1184,28 @@ public class AggregationTests {
}
}
}
static class Reservation {
String hotelCode;
String confirmationNumber;
int timestamp;
public Reservation() {}
public Reservation(String hotelCode, String confirmationNumber, int timestamp) {
this.hotelCode = hotelCode;
this.confirmationNumber = confirmationNumber;
this.timestamp = timestamp;
}
}
static class ObjectWithDate {
Date dateValue;
public ObjectWithDate(Date dateValue) {
this.dateValue = dateValue;
}
}
}

View File

@@ -27,6 +27,7 @@ import java.util.List;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.data.domain.Sort.Direction;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
@@ -219,6 +220,69 @@ public class AggregationUnitTests {
assertThat(projection1, is((DBObject) new BasicDBObject("b", "$ba")));
}
/**
* @see DATAMONGO-960
*/
@Test
public void shouldRenderAggregationWithDefaultOptionsCorrectly() {
DBObject agg = newAggregation( //
project().and("a").as("aa") //
).toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
assertThat(agg.toString(),
is("{ \"aggregate\" : \"foo\" , \"pipeline\" : [ { \"$project\" : { \"aa\" : \"$a\"}}]}"));
}
/**
* @see DATAMONGO-960
*/
@Test
public void shouldRenderAggregationWithCustomOptionsCorrectly() {
AggregationOptions aggregationOptions = newAggregationOptions().explain(true).cursor(new BasicDBObject("foo", 1))
.allowDiskUse(true).build();
DBObject agg = newAggregation( //
project().and("a").as("aa") //
) //
.withOptions(aggregationOptions) //
.toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
assertThat(agg.toString(), is("{ \"aggregate\" : \"foo\" , " //
+ "\"pipeline\" : [ { \"$project\" : { \"aa\" : \"$a\"}}] , " //
+ "\"allowDiskUse\" : true , " //
+ "\"explain\" : true , " //
+ "\"cursor\" : { \"foo\" : 1}}" //
));
}
/**
* @see DATAMONGO-954
*/
@Test
public void shouldSupportReferencingSystemVariables() {
DBObject agg = newAggregation( //
project("someKey") //
.and("a").as("a1") //
.and(Aggregation.CURRENT + ".a").as("a2") //
, sort(Direction.DESC, "a") //
, group("someKey").first(Aggregation.ROOT).as("doc") //
).toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
DBObject projection0 = extractPipelineElement(agg, 0, "$project");
assertThat(projection0, is((DBObject) new BasicDBObject("someKey", 1).append("a1", "$a")
.append("a2", "$$CURRENT.a")));
DBObject sort = extractPipelineElement(agg, 1, "$sort");
assertThat(sort, is((DBObject) new BasicDBObject("a", -1)));
DBObject group = extractPipelineElement(agg, 2, "$group");
assertThat(group,
is((DBObject) new BasicDBObject("_id", "$someKey").append("doc", new BasicDBObject("$first", "$$ROOT"))));
}
private DBObject extractPipelineElement(DBObject agg, int index, String operation) {
List<DBObject> pipeline = (List<DBObject>) agg.get("pipeline");

View File

@@ -20,12 +20,12 @@ import static org.junit.Assert.*;
import static org.springframework.data.mongodb.util.DBObjectUtils.*;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;
import org.springframework.data.mongodb.core.DBObjectTestUtils;
import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
@@ -91,7 +91,7 @@ public class ProjectionOperationUnitTests {
DBObject dbObject = operation.and("foo").plus(41).as("bar").toDBObject(Aggregation.DEFAULT_CONTEXT);
DBObject projectClause = DBObjectTestUtils.getAsDBObject(dbObject, PROJECT);
DBObject barClause = DBObjectTestUtils.getAsDBObject(projectClause, "bar");
BasicDBList addClause = DBObjectTestUtils.getAsDBList(barClause, "$add");
List<Object> addClause = (List<Object>) barClause.get("$add");
assertThat(addClause, hasSize(2));
assertThat(addClause.get(0), is((Object) "$foo"));
@@ -276,6 +276,64 @@ public class ProjectionOperationUnitTests {
is("{ \"$project\" : { \"grossSalesPrice\" : { \"$multiply\" : [ { \"$add\" : [ \"$netPrice\" , \"$surCharge\"]} , \"$taxrate\" , 2]} , \"bar\" : \"$foo\"}}"));
}
/**
* @see DATAMONGO-975
*/
@Test
public void shouldRenderDateTimeFragmentExtractionsForSimpleFieldProjectionsCorrectly() {
ProjectionOperation operation = Aggregation.project() //
.and("date").extractHour().as("hour") //
.and("date").extractMinute().as("min") //
.and("date").extractSecond().as("second") //
.and("date").extractMillisecond().as("millis") //
.and("date").extractYear().as("year") //
.and("date").extractMonth().as("month") //
.and("date").extractWeek().as("week") //
.and("date").extractDayOfYear().as("dayOfYear") //
.and("date").extractDayOfMonth().as("dayOfMonth") //
.and("date").extractDayOfWeek().as("dayOfWeek") //
;
DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT);
assertThat(dbObject, is(notNullValue()));
DBObject projected = exctractOperation("$project", dbObject);
assertThat(projected.get("hour"), is((Object) new BasicDBObject("$hour", Arrays.asList("$date"))));
assertThat(projected.get("min"), is((Object) new BasicDBObject("$minute", Arrays.asList("$date"))));
assertThat(projected.get("second"), is((Object) new BasicDBObject("$second", Arrays.asList("$date"))));
assertThat(projected.get("millis"), is((Object) new BasicDBObject("$millisecond", Arrays.asList("$date"))));
assertThat(projected.get("year"), is((Object) new BasicDBObject("$year", Arrays.asList("$date"))));
assertThat(projected.get("month"), is((Object) new BasicDBObject("$month", Arrays.asList("$date"))));
assertThat(projected.get("week"), is((Object) new BasicDBObject("$week", Arrays.asList("$date"))));
assertThat(projected.get("dayOfYear"), is((Object) new BasicDBObject("$dayOfYear", Arrays.asList("$date"))));
assertThat(projected.get("dayOfMonth"), is((Object) new BasicDBObject("$dayOfMonth", Arrays.asList("$date"))));
assertThat(projected.get("dayOfWeek"), is((Object) new BasicDBObject("$dayOfWeek", Arrays.asList("$date"))));
}
/**
* @see DATAMONGO-975
*/
@Test
public void shouldRenderDateTimeFragmentExtractionsForExpressionProjectionsCorrectly() throws Exception {
ProjectionOperation operation = Aggregation.project() //
.andExpression("date + 86400000") //
.extractDayOfYear() //
.as("dayOfYearPlus1Day") //
;
DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT);
assertThat(dbObject, is(notNullValue()));
DBObject projected = exctractOperation("$project", dbObject);
assertThat(
projected.get("dayOfYearPlus1Day"),
is((Object) new BasicDBObject("$dayOfYear", Arrays.asList(new BasicDBObject("$add", Arrays.<Object> asList(
"$date", 86400000))))));
}
private static DBObject exctractOperation(String field, DBObject fromProjectClause) {
return (DBObject) fromProjectClause.get(field);
}

View File

@@ -146,6 +146,29 @@ public class TypeBasedAggregationOperationContextUnitTests {
assertThat(age, is((DBObject) new BasicDBObject("v", 10)));
}
/**
* @see DATAMONGO-960
*/
@Test
public void rendersAggregationOptionsInTypedAggregationContextCorrectly() {
AggregationOperationContext context = getContext(FooPerson.class);
TypedAggregation<FooPerson> agg = newAggregation(FooPerson.class, project("name", "age")) //
.withOptions(
newAggregationOptions().allowDiskUse(true).explain(true).cursor(new BasicDBObject("foo", 1)).build());
DBObject dbo = agg.toDbObject("person", context);
DBObject projection = getPipelineElementFromAggregationAt(dbo, 0);
assertThat(projection.containsField("$project"), is(true));
assertThat(projection.get("$project"), is((Object) new BasicDBObject("name", 1).append("age", 1)));
assertThat(dbo.get("allowDiskUse"), is((Object) true));
assertThat(dbo.get("explain"), is((Object) true));
assertThat(dbo.get("cursor"), is((Object) new BasicDBObject("foo", 1)));
}
@Document(collection = "person")
public static class FooPerson {

View File

@@ -73,6 +73,7 @@ import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.mapping.PersonPojoStringId;
import org.springframework.data.mongodb.core.mapping.TextScore;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.test.util.ReflectionTestUtils;
@@ -1807,6 +1808,32 @@ public class MappingMongoConverterUnitTests {
assertThat(result.shape, is((Shape) sphere));
}
/**
* @see DATAMONGO-976
*/
@Test
public void shouldIgnoreTextScorePropertyWhenWriting() {
ClassWithTextScoreProperty source = new ClassWithTextScoreProperty();
source.score = Float.MAX_VALUE;
BasicDBObject dbo = new BasicDBObject();
converter.write(source, dbo);
assertThat(dbo.get("score"), nullValue());
}
/**
* @see DATAMONGO-976
*/
@Test
public void shouldIncludeTextScorePropertyWhenReading() {
ClassWithTextScoreProperty entity = converter
.read(ClassWithTextScoreProperty.class, new BasicDBObject("score", 5F));
assertThat(entity.score, equalTo(5F));
}
static class GenericType<T> {
T content;
}
@@ -2057,4 +2084,9 @@ public class MappingMongoConverterUnitTests {
Shape shape;
}
class ClassWithTextScoreProperty {
@TextScore Float score;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012 the original author or authors.
* Copyright 2012-2014 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.
@@ -22,11 +22,14 @@ import java.util.List;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.test.util.MongoVersionRule;
import org.springframework.data.util.Version;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -34,18 +37,17 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
* Integration tests for {@link MongoPersistentEntityIndexCreator}.
*
* @author Oliver Gierke
* @author Christoph Strobl
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class MongoPersistentEntityIndexCreatorIntegrationTests {
@Autowired
@Qualifier("mongo1")
MongoOperations templateOne;
public static @ClassRule MongoVersionRule version = MongoVersionRule.atLeast(new Version(2, 6));
@Autowired
@Qualifier("mongo2")
MongoOperations templateTwo;
@Autowired @Qualifier("mongo1") MongoOperations templateOne;
@Autowired @Qualifier("mongo2") MongoOperations templateTwo;
@After
public void cleanUp() {

View File

@@ -20,33 +20,45 @@ import static org.hamcrest.collection.IsEmptyCollection.*;
import static org.hamcrest.core.IsEqual.*;
import static org.hamcrest.core.IsInstanceOf.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.lang.annotation.Annotation;
import java.util.Collections;
import java.util.List;
import org.hamcrest.collection.IsEmptyCollection;
import org.hamcrest.core.IsEqual;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
import org.springframework.data.geo.Point;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mongodb.core.DBObjectTestUtils;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolver.IndexDefinitionHolder;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.CompoundIndexResolutionTests;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.GeoSpatialIndexResolutionTests;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.IndexResolutionTests;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.MixedIndexResolutionTests;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.TextIndexedResolutionTests;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.Language;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntityTestDummy.MongoPersistentEntityDummyBuilder;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import com.mongodb.BasicDBObjectBuilder;
import com.mongodb.DBObject;
/**
* @author Christoph Strobl
*/
@RunWith(Suite.class)
@SuiteClasses({ IndexResolutionTests.class, GeoSpatialIndexResolutionTests.class, CompoundIndexResolutionTests.class,
MixedIndexResolutionTests.class })
TextIndexedResolutionTests.class, MixedIndexResolutionTests.class })
public class MongoPersistentEntityIndexResolverUnitTests {
/**
@@ -289,9 +301,8 @@ public class MongoPersistentEntityIndexResolverUnitTests {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(CompoundIndexOnLevelZero.class);
IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition();
assertThat(indexDefinition.getIndexOptions(),
equalTo(new BasicDBObjectBuilder().add("name", "compound_index").add("unique", true).add("dropDups", true)
.add("sparse", true).add("background", true).add("expireAfterSeconds", 10L).get()));
assertThat(indexDefinition.getIndexOptions(), equalTo(new BasicDBObjectBuilder().add("name", "compound_index")
.add("unique", true).add("dropDups", true).add("sparse", true).add("background", true).get()));
assertThat(indexDefinition.getIndexKeys(), equalTo(new BasicDBObjectBuilder().add("foo", 1).add("bar", -1).get()));
}
@@ -304,9 +315,8 @@ public class MongoPersistentEntityIndexResolverUnitTests {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(IndexDefinedOnSuperClass.class);
IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition();
assertThat(indexDefinition.getIndexOptions(),
equalTo(new BasicDBObjectBuilder().add("name", "compound_index").add("unique", true).add("dropDups", true)
.add("sparse", true).add("background", true).add("expireAfterSeconds", 10L).get()));
assertThat(indexDefinition.getIndexOptions(), equalTo(new BasicDBObjectBuilder().add("name", "compound_index")
.add("unique", true).add("dropDups", true).add("sparse", true).add("background", true).get()));
assertThat(indexDefinition.getIndexKeys(), equalTo(new BasicDBObjectBuilder().add("foo", 1).add("bar", -1).get()));
}
@@ -322,7 +332,7 @@ public class MongoPersistentEntityIndexResolverUnitTests {
assertThat(
indexDefinition.getIndexOptions(),
equalTo(new BasicDBObjectBuilder().add("unique", true).add("dropDups", true).add("sparse", true)
.add("background", true).add("expireAfterSeconds", 10L).get()));
.add("background", true).get()));
assertThat(indexDefinition.getIndexKeys(), equalTo(new BasicDBObjectBuilder().add("foo", 1).add("bar", -1).get()));
}
@@ -364,6 +374,22 @@ public class MongoPersistentEntityIndexResolverUnitTests {
assertIndexPathAndCollection(new String[] { "foo", "bar" }, "CompoundIndexOnLevelZero", indexDefinitions.get(0));
}
/**
* @see DATAMONGO-963
*/
@Test
public void compoundIndexShouldIncludeTTLWhenConsistingOfOnlyOneKey() {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(CompoundIndexWithOnlyOneKeyAndTTL.class);
IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition();
assertThat(
indexDefinition.getIndexOptions(),
equalTo(new BasicDBObjectBuilder().add("unique", true).add("dropDups", true).add("sparse", true)
.add("background", true).add("expireAfterSeconds", 10L).get()));
assertThat(indexDefinition.getIndexKeys(), equalTo(new BasicDBObjectBuilder().add("foo", 1).get()));
}
@Document(collection = "CompoundIndexOnLevelOne")
static class CompoundIndexOnLevelOne {
@@ -400,6 +426,168 @@ public class MongoPersistentEntityIndexResolverUnitTests {
static class ComountIndexWithAutogeneratedName {
}
@Document(collection = "CompoundIndexWithOnlyOneKeyAndTTL")
@CompoundIndex(def = "{'foo': 1}", background = true, dropDups = true, expireAfterSeconds = 10, sparse = true,
unique = true)
static class CompoundIndexWithOnlyOneKeyAndTTL {
}
}
public static class TextIndexedResolutionTests {
/**
* @see DATAMONGO-937
*/
@Test
public void shouldResolveSingleFieldTextIndexCorrectly() {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(TextIndexOnSinglePropertyInRoot.class);
assertThat(indexDefinitions.size(), equalTo(1));
assertIndexPathAndCollection("bar", "textIndexOnSinglePropertyInRoot", indexDefinitions.get(0));
}
/**
* @see DATAMONGO-937
*/
@Test
public void shouldResolveMultiFieldTextIndexCorrectly() {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(TextIndexOnMutiplePropertiesInRoot.class);
assertThat(indexDefinitions.size(), equalTo(1));
assertIndexPathAndCollection(new String[] { "foo", "bar" }, "textIndexOnMutiplePropertiesInRoot",
indexDefinitions.get(0));
}
/**
* @see DATAMONGO-937
*/
@Test
public void shouldResolveTextIndexOnElementCorrectly() {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(TextIndexOnNestedRoot.class);
assertThat(indexDefinitions.size(), equalTo(1));
assertIndexPathAndCollection(new String[] { "nested.foo" }, "textIndexOnNestedRoot", indexDefinitions.get(0));
}
/**
* @see DATAMONGO-937
*/
@Test
public void shouldResolveTextIndexOnElementWithWeightCorrectly() {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(TextIndexOnNestedWithWeightRoot.class);
assertThat(indexDefinitions.size(), equalTo(1));
assertIndexPathAndCollection(new String[] { "nested.foo" }, "textIndexOnNestedWithWeightRoot",
indexDefinitions.get(0));
DBObject weights = DBObjectTestUtils.getAsDBObject(indexDefinitions.get(0).getIndexOptions(), "weights");
assertThat(weights.get("nested.foo"), IsEqual.<Object> equalTo(5F));
}
/**
* @see DATAMONGO-937
*/
@Test
public void shouldResolveTextIndexOnElementWithMostSpecificWeightCorrectly() {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(TextIndexOnNestedWithMostSpecificValueRoot.class);
assertThat(indexDefinitions.size(), equalTo(1));
assertIndexPathAndCollection(new String[] { "nested.foo", "nested.bar" },
"textIndexOnNestedWithMostSpecificValueRoot", indexDefinitions.get(0));
DBObject weights = DBObjectTestUtils.getAsDBObject(indexDefinitions.get(0).getIndexOptions(), "weights");
assertThat(weights.get("nested.foo"), IsEqual.<Object> equalTo(5F));
assertThat(weights.get("nested.bar"), IsEqual.<Object> equalTo(10F));
}
/**
* @see DATAMONGO-937
*/
@Test
public void shouldSetDefaultLanguageCorrectly() {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(DocumentWithDefaultLanguage.class);
assertThat(indexDefinitions.get(0).getIndexOptions().get("default_language"), IsEqual.<Object> equalTo("spanish"));
}
/**
* @see DATAMONGO-937
*/
@Test
public void shouldResolveTextIndexLanguageOverrideCorrectly() {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(DocumentWithLanguageOverrideOnNestedElementRoot.class);
assertThat(indexDefinitions.get(0).getIndexOptions().get("language_override"), IsEqual.<Object> equalTo("lang"));
}
@Document
static class TextIndexOnSinglePropertyInRoot {
String foo;
@TextIndexed String bar;
}
@Document
static class TextIndexOnMutiplePropertiesInRoot {
@TextIndexed String foo;
@TextIndexed(weight = 5) String bar;
}
@Document
static class TextIndexOnNestedRoot {
String bar;
@TextIndexed TextIndexOnNested nested;
}
static class TextIndexOnNested {
String foo;
}
@Document
static class TextIndexOnNestedWithWeightRoot {
@TextIndexed(weight = 5) TextIndexOnNested nested;
}
@Document
static class TextIndexOnNestedWithMostSpecificValueRoot {
@TextIndexed(weight = 5) TextIndexOnNestedWithMostSpecificValue nested;
}
static class TextIndexOnNestedWithMostSpecificValue {
String foo;
@TextIndexed(weight = 10) String bar;
}
@Document(language = "spanish")
static class DocumentWithDefaultLanguage {
@TextIndexed String foo;
}
@Document
static class DocumentWithLanguageOverrideOnNestedElementRoot {
DocumentWithLanguageOverrideOnNestedElement nested;
}
static class DocumentWithLanguageOverrideOnNestedElement {
@TextIndexed String foo;
@Language String lang;
}
}
public static class MixedIndexResolutionTests {
@@ -469,9 +657,7 @@ public class MongoPersistentEntityIndexResolverUnitTests {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(CycleOnLevelOne.class);
assertIndexPathAndCollection("reference.indexedProperty", "cycleOnLevelOne", indexDefinitions.get(0));
assertIndexPathAndCollection("reference.cyclicReference.reference.indexedProperty", "cycleOnLevelOne",
indexDefinitions.get(1));
assertThat(indexDefinitions, hasSize(2));
assertThat(indexDefinitions, hasSize(1));
}
/**
@@ -488,6 +674,76 @@ public class MongoPersistentEntityIndexResolverUnitTests {
assertThat(indexDefinitions, hasSize(3));
}
/**
* @see DATAMONGO-949
*/
@Test
public void shouldNotDetectCycleInSimilarlyNamedProperties() {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(SimilarityHolingBean.class);
assertIndexPathAndCollection("norm", "similarityHolingBean", indexDefinitions.get(0));
assertThat(indexDefinitions, hasSize(1));
}
/**
* @see DATAMONGO-962
*/
@Test
public void shouldDetectSelfCycleViaCollectionTypeCorrectly() {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(SelfCyclingViaCollectionType.class);
assertThat(indexDefinitions, IsEmptyCollection.empty());
}
/**
* @see DATAMONGO-962
*/
@Test
public void shouldNotDetectCycleWhenTypeIsUsedMoreThanOnce() {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(MultipleObjectsOfSameType.class);
assertThat(indexDefinitions, IsEmptyCollection.empty());
}
/**
* @see DATAMONGO-962
*/
@Test
public void shouldCatchCyclicReferenceExceptionOnRoot() {
Document documentDummy = new Document() {
@Override
public Class<? extends Annotation> annotationType() {
return Document.class;
}
@Override
public String collection() {
return null;
}
@Override
public String language() {
return null;
}
};
MongoPersistentProperty propertyMock = mock(MongoPersistentProperty.class);
when(propertyMock.isEntity()).thenReturn(true);
when(propertyMock.getOwner()).thenReturn(
(PersistentEntity) MongoPersistentEntityDummyBuilder.forClass(Object.class).build());
when(propertyMock.getActualType()).thenThrow(
new MongoPersistentEntityIndexResolver.CyclicPropertyReferenceException("foo", Object.class, "bar"));
MongoPersistentEntity<SelfCyclingViaCollectionType> dummy = MongoPersistentEntityDummyBuilder
.forClass(SelfCyclingViaCollectionType.class).withCollection("foo").and(propertyMock).and(documentDummy)
.build();
new MongoPersistentEntityIndexResolver(prepareMappingContext(SelfCyclingViaCollectionType.class))
.resolveIndexForEntity(dummy);
}
@Document
static class MixedIndexRoot {
@@ -554,6 +810,32 @@ public class MongoPersistentEntityIndexResolverUnitTests {
@Indexed String foo;
}
@Document
static class SimilarityHolingBean {
@Indexed @Field("norm") String normalProperty;
@Field("similarityL") private List<SimilaritySibling> listOfSimilarilyNamedEntities = null;
}
static class SimilaritySibling {
@Field("similarity") private String similarThoughNotEqualNamedProperty;
}
@Document
static class MultipleObjectsOfSameType {
SelfCyclingViaCollectionType cycleOne;
SelfCyclingViaCollectionType cycleTwo;
}
@Document
static class SelfCyclingViaCollectionType {
List<SelfCyclingViaCollectionType> cyclic;
}
}
private static List<IndexDefinitionHolder> prepareMappingContextAndResolveIndexForType(Class<?> type) {
@@ -586,4 +868,5 @@ public class MongoPersistentEntityIndexResolverUnitTests {
}
assertThat(holder.getCollection(), equalTo(expectedCollection));
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2014 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.index;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolver.CycleGuard.Path;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
/**
* Unit tests for {@link Path}.
*
* @author Christoph Strobl
*/
@RunWith(MockitoJUnitRunner.class)
public class PathUnitTests {
@Mock MongoPersistentEntity<?> entityMock;
@Before
@SuppressWarnings({ "rawtypes", "unchecked" })
public void setUp() {
when(entityMock.getType()).thenReturn((Class) Object.class);
}
/**
* @see DATAMONGO-962
*/
@Test
public void shouldIdentifyCycleForOwnerOfSameTypeAndMatchingPath() {
MongoPersistentProperty property = createPersistentPropertyMock(entityMock, "foo");
assertThat(new Path(property, "foo.bar").cycles(property, "foo.bar.bar"), is(true));
}
/**
* @see DATAMONGO-962
*/
@Test
@SuppressWarnings("rawtypes")
public void shouldAllowMatchingPathForDifferentOwners() {
MongoPersistentProperty existing = createPersistentPropertyMock(entityMock, "foo");
MongoPersistentEntity entityOfDifferentType = Mockito.mock(MongoPersistentEntity.class);
when(entityOfDifferentType.getType()).thenReturn(String.class);
MongoPersistentProperty toBeVerified = createPersistentPropertyMock(entityOfDifferentType, "foo");
assertThat(new Path(existing, "foo.bar").cycles(toBeVerified, "foo.bar.bar"), is(false));
}
/**
* @see DATAMONGO-962
*/
@Test
public void shouldAllowEqaulPropertiesOnDifferentPaths() {
MongoPersistentProperty property = createPersistentPropertyMock(entityMock, "foo");
assertThat(new Path(property, "foo.bar").cycles(property, "foo2.bar.bar"), is(false));
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private MongoPersistentProperty createPersistentPropertyMock(MongoPersistentEntity owner, String fieldname) {
MongoPersistentProperty property = Mockito.mock(MongoPersistentProperty.class);
when(property.getOwner()).thenReturn(owner);
when(property.getFieldName()).thenReturn(fieldname);
return property;
}
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2014 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.index;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.util.List;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.mongodb.config.AbstractIntegrationTests;
import org.springframework.data.mongodb.core.IndexOperations;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Language;
import org.springframework.data.mongodb.test.util.MongoVersionRule;
import org.springframework.data.util.Version;
import com.mongodb.WriteConcern;
/**
* @author Christoph Strobl
*/
public class TextIndexTests extends AbstractIntegrationTests {
public static @ClassRule MongoVersionRule version = MongoVersionRule.atLeast(new Version(2, 6));
private @Autowired MongoTemplate template;
private IndexOperations indexOps;
@Before
public void setUp() throws Exception {
template.setWriteConcern(WriteConcern.FSYNC_SAFE);
this.indexOps = template.indexOps(TextIndexedDocumentRoot.class);
}
/**
* @see DATAMONGO-937
*/
@Test
public void indexInfoShouldHaveBeenCreatedCorrectly() {
List<IndexInfo> indexInfos = indexOps.getIndexInfo();
assertThat(indexInfos.size(), is(2));
List<IndexField> fields = indexInfos.get(0).getIndexFields();
assertThat(fields.size(), is(1));
assertThat(fields, hasItem(IndexField.create("_id", Direction.ASC)));
IndexInfo textIndexInfo = indexInfos.get(1);
List<IndexField> textIndexFields = textIndexInfo.getIndexFields();
assertThat(textIndexFields.size(), is(4));
assertThat(textIndexFields, hasItem(IndexField.text("textIndexedPropertyWithDefaultWeight", 1F)));
assertThat(textIndexFields, hasItem(IndexField.text("textIndexedPropertyWithWeight", 5F)));
assertThat(textIndexFields, hasItem(IndexField.text("nestedDocument.textIndexedPropertyInNestedDocument", 1F)));
assertThat(textIndexFields, hasItem(IndexField.create("_ftsx", Direction.ASC)));
assertThat(textIndexInfo.getLanguage(), is("spanish"));
}
@Document(language = "spanish")
static class TextIndexedDocumentRoot {
@TextIndexed String textIndexedPropertyWithDefaultWeight;
@TextIndexed(weight = 5) String textIndexedPropertyWithWeight;
TextIndexedDocumentWihtLanguageOverride nestedDocument;
}
static class TextIndexedDocumentWihtLanguageOverride {
@Language String lang;
@TextIndexed String textIndexedPropertyInNestedDocument;
String nonTextIndexedProperty;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011-2013 by the original author(s).
* Copyright 2011-2014 by the original author(s).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -30,12 +30,12 @@ import org.springframework.data.util.ClassTypeInformation;
* Unit tests for {@link BasicMongoPersistentEntity}.
*
* @author Oliver Gierke
* @author Christoph Strobl
*/
@RunWith(MockitoJUnitRunner.class)
public class BasicMongoPersistentEntityUnitTests {
@Mock
ApplicationContext context;
@Mock ApplicationContext context;
@Test
public void subclassInheritsAtDocumentAnnotation() {
@@ -69,6 +69,17 @@ public class BasicMongoPersistentEntityUnitTests {
assertThat(entity.getCollection(), is("reference"));
}
/**
* @see DATAMONGO-937
*/
@Test
public void shouldDetectLanguageCorrectly() {
BasicMongoPersistentEntity<DocumentWithLanguage> entity = new BasicMongoPersistentEntity<DocumentWithLanguage>(
ClassTypeInformation.from(DocumentWithLanguage.class));
assertThat(entity.getLanguage(), is("spanish"));
}
@Document(collection = "contacts")
class Contact {
@@ -95,4 +106,9 @@ public class BasicMongoPersistentEntityUnitTests {
return collectionName;
}
}
@Document(language = "spanish")
static class DocumentWithLanguage {
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011-2013 by the original author(s).
* Copyright 2011-2014 by the original author(s).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,7 +26,10 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.data.annotation.Id;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.model.FieldNamingStrategy;
import org.springframework.data.mapping.model.MappingException;
import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.util.ReflectionUtils;
@@ -35,13 +38,13 @@ import org.springframework.util.ReflectionUtils;
* Unit test for {@link BasicMongoPersistentProperty}.
*
* @author Oliver Gierke
* @author Christoph Strobl
*/
public class BasicMongoPersistentPropertyUnitTests {
MongoPersistentEntity<Person> entity;
@Rule
public ExpectedException exception = ExpectedException.none();
@Rule public ExpectedException exception = ExpectedException.none();
@Before
public void setup() {
@@ -121,29 +124,86 @@ public class BasicMongoPersistentPropertyUnitTests {
property.getFieldName();
}
/**
* @see DATAMONGO-937
*/
@Test
public void shouldDetectAnnotatedLanguagePropertyCorrectly() {
BasicMongoPersistentEntity<DocumentWithLanguageProperty> persistentEntity = new BasicMongoPersistentEntity<DocumentWithLanguageProperty>(
ClassTypeInformation.from(DocumentWithLanguageProperty.class));
MongoPersistentProperty property = getPropertyFor(persistentEntity, "lang");
assertThat(property.isLanguageProperty(), is(true));
}
/**
* @see DATAMONGO-937
*/
@Test
public void shouldDetectIplicitLanguagePropertyCorrectly() {
BasicMongoPersistentEntity<DocumentWithImplicitLanguageProperty> persistentEntity = new BasicMongoPersistentEntity<DocumentWithImplicitLanguageProperty>(
ClassTypeInformation.from(DocumentWithImplicitLanguageProperty.class));
MongoPersistentProperty property = getPropertyFor(persistentEntity, "language");
assertThat(property.isLanguageProperty(), is(true));
}
/**
* @see DATAMONGO-976
*/
@Test
public void shouldDetectTextScorePropertyCorrectly() {
BasicMongoPersistentEntity<DocumentWithTextScoreProperty> persistentEntity = new BasicMongoPersistentEntity<DocumentWithTextScoreProperty>(
ClassTypeInformation.from(DocumentWithTextScoreProperty.class));
MongoPersistentProperty property = getPropertyFor(persistentEntity, "score");
assertThat(property.isTextScoreProperty(), is(true));
}
/**
* @see DATAMONGO-976
*/
@Test
public void shouldDetectTextScoreAsReadOnlyProperty() {
BasicMongoPersistentEntity<DocumentWithTextScoreProperty> persistentEntity = new BasicMongoPersistentEntity<DocumentWithTextScoreProperty>(
ClassTypeInformation.from(DocumentWithTextScoreProperty.class));
MongoPersistentProperty property = getPropertyFor(persistentEntity, "score");
assertThat(property.isWritable(), is(false));
}
private MongoPersistentProperty getPropertyFor(Field field) {
return new BasicMongoPersistentProperty(field, null, entity, new SimpleTypeHolder(),
return getPropertyFor(entity, field);
}
private MongoPersistentProperty getPropertyFor(MongoPersistentEntity<?> persistentEntity, String fieldname) {
return getPropertyFor(persistentEntity, ReflectionUtils.findField(persistentEntity.getType(), fieldname));
}
private MongoPersistentProperty getPropertyFor(MongoPersistentEntity<?> persistentEntity, Field field) {
return new BasicMongoPersistentProperty(field, null, persistentEntity, new SimpleTypeHolder(),
PropertyNameFieldNamingStrategy.INSTANCE);
}
class Person {
@Id
String id;
@Id String id;
@org.springframework.data.mongodb.core.mapping.Field("foo")
String firstname;
@org.springframework.data.mongodb.core.mapping.Field("foo") String firstname;
String lastname;
@org.springframework.data.mongodb.core.mapping.Field(order = -20)
String ssn;
@org.springframework.data.mongodb.core.mapping.Field(order = -20) String ssn;
}
enum UppercaseFieldNamingStrategy implements FieldNamingStrategy {
INSTANCE;
public String getFieldName(MongoPersistentProperty property) {
public String getFieldName(PersistentProperty<?> property) {
return property.getName().toUpperCase(Locale.US);
}
}
@@ -152,8 +212,22 @@ public class BasicMongoPersistentPropertyUnitTests {
INSTANCE;
public String getFieldName(MongoPersistentProperty property) {
public String getFieldName(PersistentProperty<?> property) {
return null;
}
}
static class DocumentWithLanguageProperty {
@Language String lang;
}
static class DocumentWithImplicitLanguageProperty {
String language;
}
static class DocumentWithTextScoreProperty {
@TextScore Float score;
}
}

View File

@@ -1,51 +0,0 @@
/*
* Copyright 2013 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.mapping;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
/**
* Unit tests for {@link CamelCaseAbbreviatingFieldNamingStrategy}.
*
* @author Oliver Gierke
*/
@RunWith(MockitoJUnitRunner.class)
public class CamelCaseAbbreviatingFieldNamingStrategyUnitTests {
FieldNamingStrategy strategy = new CamelCaseAbbreviatingFieldNamingStrategy();
@Mock
MongoPersistentProperty property;
@Test
public void foo() {
assertFieldNameForPropertyName("fooBar", "fb");
assertFieldNameForPropertyName("fooBARFooBar", "fbfb");
}
private void assertFieldNameForPropertyName(String propertyName, String fieldName) {
when(property.getName()).thenReturn(propertyName);
assertThat(strategy.getFieldName(property), is(fieldName));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011-2013 by the original author(s).
* Copyright 2011-2014 by the original author(s).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -31,6 +31,8 @@ import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.data.annotation.Id;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.model.FieldNamingStrategy;
import org.springframework.data.mapping.model.MappingException;
import com.mongodb.DBRef;
@@ -40,6 +42,7 @@ import com.mongodb.DBRef;
*
* @author Oliver Gierke
* @author Thomas Darimont
* @author Christoph Strobl
*/
@RunWith(MockitoJUnitRunner.class)
public class MongoMappingContextUnitTests {
@@ -84,7 +87,7 @@ public class MongoMappingContextUnitTests {
context.setApplicationContext(applicationContext);
context.setFieldNamingStrategy(new FieldNamingStrategy() {
public String getFieldName(MongoPersistentProperty property) {
public String getFieldName(PersistentProperty<?> property) {
return property.getName().toUpperCase(Locale.US);
}
});
@@ -177,6 +180,21 @@ public class MongoMappingContextUnitTests {
context.getPersistentEntity(ClassWithMultipleImplicitIds.class);
}
/**
* @see DATAMONGO-976
*/
@Test
public void shouldRejectClassWithInvalidTextScoreProperty() {
exception.expect(MappingException.class);
exception.expectMessage("score");
exception.expectMessage("Float");
exception.expectMessage("Double");
MongoMappingContext context = new MongoMappingContext();
context.getPersistentEntity(ClassWithInvalidTextScoreProperty.class);
}
public class SampleClass {
Map<String, SampleClass> children;
@@ -238,4 +256,9 @@ public class MongoMappingContextUnitTests {
String _id;
String id;
}
class ClassWithInvalidTextScoreProperty {
@TextScore Locale score;
}
}

View File

@@ -0,0 +1,217 @@
/*
* Copyright 2014 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.mapping;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Version;
import org.springframework.data.mapping.AssociationHandler;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PreferredConstructor;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.SimpleAssociationHandler;
import org.springframework.data.mapping.SimplePropertyHandler;
import org.springframework.data.util.TypeInformation;
/**
* Trivial dummy implementation of {@link MongoPersistentEntity} to be used in tests.
*
* @author Christoph Strobl
* @param <T>
*/
public class MongoPersistentEntityTestDummy<T> implements MongoPersistentEntity<T> {
private Map<Class<?>, Annotation> annotations = new HashMap<Class<?>, Annotation>();
private Collection<MongoPersistentProperty> properties = new ArrayList<MongoPersistentProperty>();
private String collection;
private String name;
private Class<T> type;
@Override
public String getName() {
return name;
}
@Override
public PreferredConstructor<T, MongoPersistentProperty> getPersistenceConstructor() {
return null;
}
@Override
public boolean isConstructorArgument(PersistentProperty<?> property) {
return false;
}
@Override
public boolean isIdProperty(PersistentProperty<?> property) {
return property != null ? property.isIdProperty() : false;
}
@Override
public boolean isVersionProperty(PersistentProperty<?> property) {
return property != null ? property.isIdProperty() : false;
}
@Override
public MongoPersistentProperty getIdProperty() {
return getPersistentProperty(Id.class);
}
@Override
public MongoPersistentProperty getVersionProperty() {
return getPersistentProperty(Version.class);
}
@Override
public MongoPersistentProperty getPersistentProperty(String name) {
for (MongoPersistentProperty p : this.properties) {
if (p.getName().equals(name)) {
return p;
}
}
return null;
}
@Override
public MongoPersistentProperty getPersistentProperty(Class<? extends Annotation> annotationType) {
for (MongoPersistentProperty p : this.properties) {
if (p.isAnnotationPresent(annotationType)) {
return p;
}
}
return null;
}
@Override
public boolean hasIdProperty() {
return false;
}
@Override
public boolean hasVersionProperty() {
return getVersionProperty() != null;
}
@Override
public Class<T> getType() {
return this.type;
}
@Override
public Object getTypeAlias() {
return null;
}
@Override
public TypeInformation<T> getTypeInformation() {
return null;
}
@Override
public void doWithProperties(PropertyHandler<MongoPersistentProperty> handler) {
for (MongoPersistentProperty p : this.properties) {
handler.doWithPersistentProperty(p);
}
}
@Override
public void doWithProperties(SimplePropertyHandler handler) {
for (MongoPersistentProperty p : this.properties) {
handler.doWithPersistentProperty(p);
}
}
@Override
public void doWithAssociations(AssociationHandler<MongoPersistentProperty> handler) {
}
@Override
public void doWithAssociations(SimpleAssociationHandler handler) {
}
@SuppressWarnings("unchecked")
@Override
public <A extends Annotation> A findAnnotation(Class<A> annotationType) {
return (A) this.annotations.get(annotationType);
}
@Override
public String getCollection() {
return this.collection;
}
/**
* Simple builder to create {@link MongoPersistentEntityTestDummy} with defined properties.
*
* @author Christoph Strobl
* @param <T>
*/
public static class MongoPersistentEntityDummyBuilder<T> {
private MongoPersistentEntityTestDummy<T> instance;
private MongoPersistentEntityDummyBuilder(Class<T> type) {
this.instance = new MongoPersistentEntityTestDummy<T>();
this.instance.type = type;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
public static <T> MongoPersistentEntityDummyBuilder<T> forClass(Class<T> type) {
return new MongoPersistentEntityDummyBuilder(type);
}
public MongoPersistentEntityDummyBuilder<T> withName(String name) {
this.instance.name = name;
return this;
}
public MongoPersistentEntityDummyBuilder<T> and(MongoPersistentProperty property) {
this.instance.properties.add(property);
return this;
}
public MongoPersistentEntityDummyBuilder<T> withCollection(String collection) {
this.instance.collection = collection;
return this;
}
public MongoPersistentEntityDummyBuilder<T> and(Annotation annotation) {
this.instance.annotations.put(annotation.annotationType(), annotation);
return this;
}
public MongoPersistentEntityTestDummy<T> build() {
return this.instance;
}
}
@Override
public String getLanguage() {
return null;
}
}

View File

@@ -1,57 +0,0 @@
/*
* Copyright 2013-2014 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.mapping;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
/**
* Unit tests for {@link SnakeCaseFieldNamingStrategy}.
*
* @author Ryan Tenney
* @author Oliver Gierke
*/
@RunWith(MockitoJUnitRunner.class)
public class SnakeCaseFieldNamingStrategyUnitTests {
FieldNamingStrategy strategy = new SnakeCaseFieldNamingStrategy();
@Mock MongoPersistentProperty property;
/**
* @see DATAMONGO-866
*/
@Test
public void rendersSnakeCaseFieldNames() {
assertFieldNameForPropertyName("fooBar", "foo_bar");
assertFieldNameForPropertyName("FooBar", "foo_bar");
assertFieldNameForPropertyName("foo_bar", "foo_bar");
assertFieldNameForPropertyName("FOO_BAR", "foo_bar");
}
private void assertFieldNameForPropertyName(String propertyName, String fieldName) {
when(property.getName()).thenReturn(propertyName);
assertThat(strategy.getFieldName(property), is(fieldName));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012 the original author or authors.
* Copyright 2012-2014 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.
@@ -20,10 +20,13 @@ import static org.junit.Assert.*;
import javax.validation.ConstraintViolationException;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.test.util.MongoVersionRule;
import org.springframework.data.util.Version;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -33,13 +36,15 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
* @see DATAMONGO-36
* @author Maciej Walkowiak
* @author Oliver Gierke
* @author Christoph Strobl
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class ValidatingMongoEventListenerTests {
@Autowired
MongoTemplate mongoTemplate;
public static @ClassRule MongoVersionRule version = MongoVersionRule.atLeast(new Version(2, 6));
@Autowired MongoTemplate mongoTemplate;
@Test
public void shouldThrowConstraintViolationException() {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011 the original author or authors.
* Copyright 2011-2014 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.
@@ -32,6 +32,7 @@ import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.Box;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
@@ -50,6 +51,7 @@ import com.mongodb.Mongo;
* Integration test for {@link MongoTemplate}'s Map-Reduce operations
*
* @author Mark Pollack
* @author Thomas Darimont
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:infrastructure.xml")
@@ -276,6 +278,31 @@ public class MapReduceTests {
}
/**
* @see DATAMONGO-938
*/
@Test
public void mapReduceShouldUseQueryMapper() {
DBCollection c = mongoTemplate.getDb().getCollection("jmrWithGeo");
c.save(new BasicDBObject("x", new String[] { "a", "b" }).append("loc", new double[] { 0, 0 }));
c.save(new BasicDBObject("x", new String[] { "b", "c" }).append("loc", new double[] { 0, 0 }));
c.save(new BasicDBObject("x", new String[] { "c", "d" }).append("loc", new double[] { 0, 0 }));
Query query = new Query(where("x").ne(new String[] { "a", "b" }).and("loc")
.within(new Box(new double[] { 0, 0 }, new double[] { 1, 1 })));
MapReduceResults<ValueObject> results = template.mapReduce(query, "jmrWithGeo", mapFunction, reduceFunction,
ValueObject.class);
Map<String, Float> m = copyToMap(results);
assertEquals(3, m.size());
assertEquals(1, m.get("b").intValue());
assertEquals(2, m.get("c").intValue());
assertEquals(1, m.get("d").intValue());
}
private void performMapReduce(boolean inline, boolean withQuery) {
createMapReduceData();
MapReduceResults<ValueObject> results;

View File

@@ -0,0 +1,143 @@
/*
* Copyright 2014 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.query;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
import org.hamcrest.core.IsEqual;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.util.StringUtils;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
/**
* A {@link TypeSafeMatcher} that tests whether a given {@link Query} matches a query specification.
*
* @author Christoph Strobl
* @param <T>
*/
public class IsQuery<T extends Query> extends TypeSafeMatcher<T> {
protected DBObject query;
protected DBObject sort;
protected DBObject fields;
private int skip;
private int limit;
private String hint;
protected IsQuery() {
query = new BasicDBObject();
sort = new BasicDBObject();
}
public static <T extends BasicQuery> IsQuery<T> isQuery() {
return new IsQuery<T>();
}
public IsQuery<T> limitingTo(int limit) {
this.limit = limit;
return this;
}
public IsQuery<T> skippig(int skip) {
this.skip = skip;
return this;
}
public IsQuery<T> providingHint(String hint) {
this.hint = hint;
return this;
}
public IsQuery<T> includingField(String fieldname) {
if (fields == null) {
fields = new BasicDBObject();
}
fields.put(fieldname, 1);
return this;
}
public IsQuery<T> excludingField(String fieldname) {
if (fields == null) {
fields = new BasicDBObject();
}
fields.put(fieldname, -1);
return this;
}
public IsQuery<T> sortingBy(String fieldname, Direction direction) {
sort.put(fieldname, Direction.ASC.equals(direction) ? 1 : -1);
return this;
}
@Override
public void describeTo(Description description) {
BasicQuery expected = new BasicQuery(this.query, this.fields);
expected.setSortObject(sort);
expected.skip(this.skip);
expected.limit(this.limit);
if (StringUtils.hasText(this.hint)) {
expected.withHint(this.hint);
}
description.appendValue(expected);
}
@Override
protected boolean matchesSafely(T item) {
if (item == null) {
return false;
}
if (!new IsEqual<DBObject>(query).matches(item.getQueryObject())) {
return false;
}
if (!new IsEqual<DBObject>(sort).matches(item.getSortObject())) {
return false;
}
if (!new IsEqual<DBObject>(fields).matches(item.getFieldsObject())) {
return false;
}
if (!new IsEqual<String>(this.hint).matches(item.getHint())) {
return false;
}
if (!new IsEqual<Integer>(this.skip).matches(item.getSkip())) {
return false;
}
if (!new IsEqual<Integer>(this.limit).matches(item.getLimit())) {
return false;
}
return true;
}
}

View File

@@ -23,6 +23,9 @@ import java.util.Map;
import org.junit.Test;
import com.mongodb.BasicDBObject;
import com.mongodb.BasicDBObjectBuilder;
/**
* Test cases for {@link Update}.
*
@@ -30,6 +33,7 @@ import org.junit.Test;
* @author Thomas Risberg
* @author Becca Gaspard
* @author Christoph Strobl
* @author Thomas Darimont
*/
public class UpdateTests {
@@ -284,4 +288,125 @@ public class UpdateTests {
public void testCreatingUpdateWithNullKeyThrowsException() {
Update.update(null, "value");
}
/**
* @see DATAMONGO-953
*/
@Test
public void testEquality() {
Update actualUpdate = new Update() //
.inc("size", 1) //
.set("nl", null) //
.set("directory", "/Users/Test/Desktop") //
.push("authors", Collections.singletonMap("name", "Sven")) //
.pop("authors", Update.Position.FIRST) //
.set("foo", "bar");
Update expectedUpdate = new Update() //
.inc("size", 1) //
.set("nl", null) //
.set("directory", "/Users/Test/Desktop") //
.push("authors", Collections.singletonMap("name", "Sven")) //
.pop("authors", Update.Position.FIRST) //
.set("foo", "bar");
assertThat(actualUpdate, is(equalTo(actualUpdate)));
assertThat(actualUpdate.hashCode(), is(equalTo(actualUpdate.hashCode())));
assertThat(actualUpdate, is(equalTo(expectedUpdate)));
assertThat(actualUpdate.hashCode(), is(equalTo(expectedUpdate.hashCode())));
}
/**
* @see DATAMONGO-953
*/
@Test
public void testToString() {
Update actualUpdate = new Update() //
.inc("size", 1) //
.set("nl", null) //
.set("directory", "/Users/Test/Desktop") //
.push("authors", Collections.singletonMap("name", "Sven")) //
.pop("authors", Update.Position.FIRST) //
.set("foo", "bar");
Update expectedUpdate = new Update() //
.inc("size", 1) //
.set("nl", null) //
.set("directory", "/Users/Test/Desktop") //
.push("authors", Collections.singletonMap("name", "Sven")) //
.pop("authors", Update.Position.FIRST) //
.set("foo", "bar");
assertThat(actualUpdate.toString(), is(equalTo(expectedUpdate.toString())));
assertThat(actualUpdate.toString(), is("{ \"$inc\" : { \"size\" : 1} ," //
+ " \"$set\" : { \"nl\" : null , \"directory\" : \"/Users/Test/Desktop\" , \"foo\" : \"bar\"} , " //
+ "\"$push\" : { \"authors\" : { \"name\" : \"Sven\"}} " //
+ ", \"$pop\" : { \"authors\" : -1}}")); //
}
/**
* @see DATAMONGO-944
*/
@Test
public void getUpdateObjectShouldReturnCurrentDateCorrectlyForSingleFieldWhenUsingDate() {
Update update = new Update().currentDate("foo");
assertThat(update.getUpdateObject(),
equalTo(new BasicDBObjectBuilder().add("$currentDate", new BasicDBObject("foo", true)).get()));
}
/**
* @see DATAMONGO-944
*/
@Test
public void getUpdateObjectShouldReturnCurrentDateCorrectlyForMultipleFieldsWhenUsingDate() {
Update update = new Update().currentDate("foo").currentDate("bar");
assertThat(update.getUpdateObject(),
equalTo(new BasicDBObjectBuilder().add("$currentDate", new BasicDBObject("foo", true).append("bar", true))
.get()));
}
/**
* @see DATAMONGO-944
*/
@Test
public void getUpdateObjectShouldReturnCurrentDateCorrectlyForSingleFieldWhenUsingTimestamp() {
Update update = new Update().currentTimestamp("foo");
assertThat(
update.getUpdateObject(),
equalTo(new BasicDBObjectBuilder().add("$currentDate",
new BasicDBObject("foo", new BasicDBObject("$type", "timestamp"))).get()));
}
/**
* @see DATAMONGO-944
*/
@Test
public void getUpdateObjectShouldReturnCurrentDateCorrectlyForMultipleFieldsWhenUsingTimestamp() {
Update update = new Update().currentTimestamp("foo").currentTimestamp("bar");
assertThat(
update.getUpdateObject(),
equalTo(new BasicDBObjectBuilder().add(
"$currentDate",
new BasicDBObject("foo", new BasicDBObject("$type", "timestamp")).append("bar", new BasicDBObject("$type",
"timestamp"))).get()));
}
/**
* @see DATAMONGO-944
*/
@Test
public void getUpdateObjectShouldReturnCurrentDateCorrectlyWhenUsingMixedDateAndTimestamp() {
Update update = new Update().currentDate("foo").currentTimestamp("bar");
assertThat(
update.getUpdateObject(),
equalTo(new BasicDBObjectBuilder().add("$currentDate",
new BasicDBObject("foo", true).append("bar", new BasicDBObject("$type", "timestamp"))).get()));
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2014 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.query.text;
import org.hamcrest.TypeSafeMatcher;
import org.springframework.data.mongodb.core.query.IsQuery;
import org.springframework.util.StringUtils;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
/**
* A {@link TypeSafeMatcher} that tests whether a given {@link TextQuery} matches a query specification.
*
* @author Christoph Strobl
* @param <T>
*/
public class IsTextQuery<T extends TextQuery> extends IsQuery<T> {
private final String SCORE_DEFAULT_FIELDNAME = "score";
private final DBObject META_TEXT_SCORE = new BasicDBObject("$meta", "textScore");
private String scoreFieldName = SCORE_DEFAULT_FIELDNAME;
private IsTextQuery() {
super();
}
public static <T extends TextQuery> IsTextQuery<T> isTextQuery() {
return new IsTextQuery<T>();
}
public IsTextQuery<T> searchingFor(String term) {
appendTerm(term);
return this;
}
public IsTextQuery<T> inLanguage(String language) {
appendLanguage(language);
return this;
}
public IsTextQuery<T> returningScore() {
if (fields == null) {
fields = new BasicDBObject();
}
fields.put(scoreFieldName, META_TEXT_SCORE);
return this;
}
public IsTextQuery<T> returningScoreAs(String fieldname) {
this.scoreFieldName = fieldname != null ? fieldname : SCORE_DEFAULT_FIELDNAME;
return this.returningScore();
}
public IsTextQuery<T> sortingByScore() {
sort.put(scoreFieldName, META_TEXT_SCORE);
return this;
}
private void appendLanguage(String language) {
DBObject dbo = getOrCreateTextDbo();
dbo.put("$language", language);
}
private DBObject getOrCreateTextDbo() {
DBObject dbo = (DBObject) query.get("$text");
if (dbo == null) {
dbo = new BasicDBObject();
}
return dbo;
}
private void appendTerm(String term) {
DBObject dbo = getOrCreateTextDbo();
String searchString = (String) dbo.get("$search");
if (StringUtils.hasText(searchString)) {
searchString += (" " + term);
} else {
searchString = term;
}
dbo.put("$search", searchString);
query.put("$text", dbo);
}
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2014 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.query.text;
import org.hamcrest.core.IsEqual;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.data.mongodb.core.DBObjectTestUtils;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.mongodb.util.JSON;
/**
* @author Christoph Strobl
*/
public class TextCriteriaUnitTests {
/**
* @see DATAMONGO-850
*/
@Test
public void shouldNotHaveLanguageField() {
TextCriteria criteria = TextCriteria.forDefaultLanguage();
Assert.assertThat(criteria.getCriteriaObject(), IsEqual.equalTo(searchObject("{ }")));
}
/**
* @see DATAMONGO-850
*/
@Test
public void shouldNotHaveLanguageForNonDefaultLanguageField() {
TextCriteria criteria = TextCriteria.forLanguage("spanish");
Assert.assertThat(criteria.getCriteriaObject(), IsEqual.equalTo(searchObject("{ \"$language\" : \"spanish\" }")));
}
/**
* @see DATAMONGO-850
*/
@Test
public void shouldCreateSearchFieldForSingleTermCorrectly() {
TextCriteria criteria = TextCriteria.forDefaultLanguage().matching("cake");
Assert.assertThat(criteria.getCriteriaObject(), IsEqual.equalTo(searchObject("{ \"$search\" : \"cake\" }")));
}
/**
* @see DATAMONGO-850
*/
@Test
public void shouldCreateSearchFieldCorrectlyForMultipleTermsCorrectly() {
TextCriteria criteria = TextCriteria.forDefaultLanguage().matchingAny("bake", "coffee", "cake");
Assert.assertThat(criteria.getCriteriaObject(),
IsEqual.equalTo(searchObject("{ \"$search\" : \"bake coffee cake\" }")));
}
/**
* @see DATAMONGO-850
*/
@Test
public void shouldCreateSearchFieldForPhraseCorrectly() {
TextCriteria criteria = TextCriteria.forDefaultLanguage().matchingPhrase("coffee cake");
Assert.assertThat(DBObjectTestUtils.getAsDBObject(criteria.getCriteriaObject(), "$text"),
IsEqual.<DBObject> equalTo(new BasicDBObject("$search", "\"coffee cake\"")));
}
/**
* @see DATAMONGO-850
*/
@Test
public void shouldCreateNotFieldCorrectly() {
TextCriteria criteria = TextCriteria.forDefaultLanguage().notMatching("cake");
Assert.assertThat(criteria.getCriteriaObject(), IsEqual.equalTo(searchObject("{ \"$search\" : \"-cake\" }")));
}
/**
* @see DATAMONGO-850
*/
@Test
public void shouldCreateSearchFieldCorrectlyForNotMultipleTermsCorrectly() {
TextCriteria criteria = TextCriteria.forDefaultLanguage().notMatchingAny("bake", "coffee", "cake");
Assert.assertThat(criteria.getCriteriaObject(),
IsEqual.equalTo(searchObject("{ \"$search\" : \"-bake -coffee -cake\" }")));
}
/**
* @see DATAMONGO-850
*/
@Test
public void shouldCreateSearchFieldForNotPhraseCorrectly() {
TextCriteria criteria = TextCriteria.forDefaultLanguage().notMatchingPhrase("coffee cake");
Assert.assertThat(DBObjectTestUtils.getAsDBObject(criteria.getCriteriaObject(), "$text"),
IsEqual.<DBObject> equalTo(new BasicDBObject("$search", "-\"coffee cake\"")));
}
private DBObject searchObject(String json) {
return new BasicDBObject("$text", JSON.parse(json));
}
}

View File

@@ -0,0 +1,372 @@
/*
* Copyright 2014 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.query.text;
import static org.hamcrest.collection.IsCollectionWithSize.*;
import static org.hamcrest.collection.IsEmptyCollection.*;
import static org.hamcrest.collection.IsIterableContainingInOrder.*;
import static org.hamcrest.core.AnyOf.*;
import static org.hamcrest.core.IsCollectionContaining.*;
import static org.hamcrest.core.IsEqual.*;
import static org.junit.Assert.*;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import java.util.List;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.mongodb.config.AbstractIntegrationTests;
import org.springframework.data.mongodb.core.IndexOperations;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.index.IndexDefinition;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.Language;
import org.springframework.data.mongodb.core.mapping.TextScore;
import org.springframework.data.mongodb.core.query.text.TextQueryTests.FullTextDoc.FullTextDocBuilder;
import org.springframework.data.mongodb.test.util.MongoVersionRule;
import org.springframework.data.util.Version;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
/**
* @author Christoph Strobl
*/
public class TextQueryTests extends AbstractIntegrationTests {
public static @ClassRule MongoVersionRule version = MongoVersionRule.atLeast(new Version(2, 6));
private static final FullTextDoc BAKE = new FullTextDocBuilder().headline("bake").build();
private static final FullTextDoc COFFEE = new FullTextDocBuilder().subHeadline("coffee").build();
private static final FullTextDoc CAKE = new FullTextDocBuilder().body("cake").build();
private static final FullTextDoc NOT_TO_BE_FOUND = new FullTextDocBuilder().headline("o_O").build();
private static final FullTextDoc SPANISH_MILK = new FullTextDocBuilder().headline("leche").lanugage("spanish")
.build();
private static final FullTextDoc FRENCH_MILK = new FullTextDocBuilder().headline("leche").lanugage("french").build();
private static final FullTextDoc MILK_AND_SUGAR = new FullTextDocBuilder().headline("milk and sugar").build();
private @Autowired MongoOperations template;
@Before
public void setUp() {
IndexOperations indexOps = template.indexOps(FullTextDoc.class);
indexOps.dropAllIndexes();
indexOps.ensureIndex(new IndexDefinition() {
@Override
public DBObject getIndexOptions() {
DBObject options = new BasicDBObject();
options.put("weights", weights());
options.put("name", "TextQueryTests_TextIndex");
options.put("language_override", "lang");
options.put("default_language", "english");
return options;
}
@Override
public DBObject getIndexKeys() {
DBObject keys = new BasicDBObject();
keys.put("headline", "text");
keys.put("subheadline", "text");
keys.put("body", "text");
return keys;
}
private DBObject weights() {
DBObject weights = new BasicDBObject();
weights.put("headline", 10);
weights.put("subheadline", 5);
weights.put("body", 1);
return weights;
}
});
}
/**
* @see DATAMONGO-850
*/
@Test
public void shouldOnlyFindDocumentsMatchingAnyWordOfGivenQuery() {
initWithDefaultDocuments();
List<FullTextDoc> result = template.find(new TextQuery("bake coffee cake"), FullTextDoc.class);
assertThat(result, hasSize(3));
assertThat(result, hasItems(BAKE, COFFEE, CAKE));
}
/**
* @see DATAMONGO-850
*/
@Test
public void shouldNotFindDocumentsWhenQueryDoesNotMatchAnyDocumentInIndex() {
initWithDefaultDocuments();
List<FullTextDoc> result = template.find(new TextQuery("tasmanian devil"), FullTextDoc.class);
assertThat(result, hasSize(0));
}
/**
* @see DATAMONGO-850
*/
@Test
public void shouldApplySortByScoreCorrectly() {
initWithDefaultDocuments();
FullTextDoc coffee2 = new FullTextDocBuilder().headline("coffee").build();
template.insert(coffee2);
List<FullTextDoc> result = template.find(new TextQuery("bake coffee cake").sortByScore(), FullTextDoc.class);
assertThat(result, hasSize(4));
assertThat(result.get(0), anyOf(equalTo(BAKE), equalTo(coffee2)));
assertThat(result.get(1), anyOf(equalTo(BAKE), equalTo(coffee2)));
assertThat(result.get(2), equalTo(COFFEE));
assertThat(result.get(3), equalTo(CAKE));
}
/**
* @see DATAMONGO-850
*/
@Test
public void shouldFindTextInAnyLanguage() {
initWithDefaultDocuments();
List<FullTextDoc> result = template.find(new TextQuery("leche"), FullTextDoc.class);
assertThat(result, hasSize(2));
assertThat(result, hasItems(SPANISH_MILK, FRENCH_MILK));
}
/**
* @see DATAMONGO-850
*/
@Test
public void shouldOnlyFindTextInSpecificLanguage() {
initWithDefaultDocuments();
List<FullTextDoc> result = template.find(new TextQuery("leche").addCriteria(where("language").is("spanish")),
FullTextDoc.class);
assertThat(result, hasSize(1));
assertThat(result.get(0), equalTo(SPANISH_MILK));
}
/**
* @see DATAMONGO-850
*/
@Test
public void shouldNotFindDocumentsWithNegatedTerms() {
initWithDefaultDocuments();
List<FullTextDoc> result = template.find(new TextQuery("bake coffee -cake"), FullTextDoc.class);
assertThat(result, hasSize(2));
assertThat(result, hasItems(BAKE, COFFEE));
}
/**
* @see DATAMONGO-976
*/
@Test
public void shouldInlcudeScoreCorreclty() {
initWithDefaultDocuments();
List<FullTextDoc> result = template.find(new TextQuery("bake coffee -cake").includeScore().sortByScore(),
FullTextDoc.class);
assertThat(result, hasSize(2));
for (FullTextDoc scoredDoc : result) {
assertTrue(scoredDoc.score > 0F);
}
}
/**
* @see DATAMONGO-850
*/
@Test
public void shouldApplyPhraseCorrectly() {
initWithDefaultDocuments();
TextQuery query = TextQuery.queryText(TextCriteria.forDefaultLanguage().matchingPhrase("milk and sugar"));
List<FullTextDoc> result = template.find(query, FullTextDoc.class);
assertThat(result, hasSize(1));
assertThat(result, contains(MILK_AND_SUGAR));
}
/**
* @see DATAMONGO-850
*/
@Test
public void shouldReturnEmptyListWhenNoDocumentsMatchGivenPhrase() {
initWithDefaultDocuments();
TextQuery query = TextQuery.queryText(TextCriteria.forDefaultLanguage().matchingPhrase("milk no sugar"));
List<FullTextDoc> result = template.find(query, FullTextDoc.class);
assertThat(result, empty());
}
/**
* @see DATAMONGO-850
*/
@Test
public void shouldApplyPaginationCorrectly() {
initWithDefaultDocuments();
// page 1
List<FullTextDoc> result = template.find(new TextQuery("bake coffee cake").sortByScore()
.with(new PageRequest(0, 2)), FullTextDoc.class);
assertThat(result, hasSize(2));
assertThat(result, contains(BAKE, COFFEE));
// page 2
result = template.find(new TextQuery("bake coffee cake").sortByScore().with(new PageRequest(1, 2)),
FullTextDoc.class);
assertThat(result, hasSize(1));
assertThat(result, contains(CAKE));
}
private void initWithDefaultDocuments() {
this.template.save(BAKE);
this.template.save(COFFEE);
this.template.save(CAKE);
this.template.save(NOT_TO_BE_FOUND);
this.template.save(SPANISH_MILK);
this.template.save(FRENCH_MILK);
this.template.save(MILK_AND_SUGAR);
}
@Document(collection = "fullTextDoc")
static class FullTextDoc {
@Id String id;
private @Language @Field("lang") String language;
private String headline;
private String subheadline;
private String body;
private @TextScore Float score;
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((body == null) ? 0 : body.hashCode());
result = prime * result + ((headline == null) ? 0 : headline.hashCode());
result = prime * result + ((id == null) ? 0 : id.hashCode());
result = prime * result + ((language == null) ? 0 : language.hashCode());
result = prime * result + ((subheadline == null) ? 0 : subheadline.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof FullTextDoc)) {
return false;
}
FullTextDoc other = (FullTextDoc) obj;
if (body == null) {
if (other.body != null) {
return false;
}
} else if (!body.equals(other.body)) {
return false;
}
if (headline == null) {
if (other.headline != null) {
return false;
}
} else if (!headline.equals(other.headline)) {
return false;
}
if (id == null) {
if (other.id != null) {
return false;
}
} else if (!id.equals(other.id)) {
return false;
}
if (language == null) {
if (other.language != null) {
return false;
}
} else if (!language.equals(other.language)) {
return false;
}
if (subheadline == null) {
if (other.subheadline != null) {
return false;
}
} else if (!subheadline.equals(other.subheadline)) {
return false;
}
return true;
}
static class FullTextDocBuilder {
private FullTextDoc instance;
public FullTextDocBuilder() {
this.instance = new FullTextDoc();
}
public FullTextDocBuilder headline(String headline) {
this.instance.headline = headline;
return this;
}
public FullTextDocBuilder subHeadline(String subHeadline) {
this.instance.subheadline = subHeadline;
return this;
}
public FullTextDocBuilder body(String body) {
this.instance.body = body;
return this;
}
public FullTextDocBuilder lanugage(String language) {
this.instance.language = language;
return this;
}
public FullTextDoc build() {
return this.instance;
}
}
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright 2014 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.query.text;
import static org.junit.Assert.*;
import static org.springframework.data.mongodb.core.query.text.IsTextQuery.*;
import org.junit.Test;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.mongodb.core.query.Query;
/**
* @author Christoph Strobl
*/
public class TextQueryUnitTests {
private static final String QUERY = "bake coffee cake";
private static final String LANGUAGE_SPANISH = "spanish";
/**
* @see DATAMONGO-850
*/
@Test
public void shouldCreateQueryObjectCorrectly() {
assertThat(new TextQuery(QUERY), isTextQuery().searchingFor(QUERY));
}
/**
* @see DATAMONGO-850
*/
@Test
public void shouldIncludeLanguageInQueryObjectWhenNotNull() {
assertThat(new TextQuery(QUERY, LANGUAGE_SPANISH), isTextQuery().searchingFor(QUERY).inLanguage(LANGUAGE_SPANISH));
}
/**
* @see DATAMONGO-850
*/
@Test
public void shouldIncludeScoreFieldCorrectly() {
assertThat(new TextQuery(QUERY).includeScore(), isTextQuery().searchingFor(QUERY).returningScore());
}
/**
* @see DATAMONGO-850
*/
@Test
public void shouldNotOverrideExistingProjections() {
TextQuery query = new TextQuery(TextCriteria.forDefaultLanguage().matching(QUERY)).includeScore();
query.fields().include("foo");
assertThat(query, isTextQuery().searchingFor(QUERY).returningScore().includingField("foo"));
}
/**
* @see DATAMONGO-850
*/
@Test
public void shouldIncludeSortingByScoreCorrectly() {
assertThat(new TextQuery(QUERY).sortByScore(), isTextQuery().searchingFor(QUERY).returningScore().sortingByScore());
}
/**
* @see DATAMONGO-850
*/
@Test
public void shouldNotOverrideExistingSort() {
TextQuery query = new TextQuery(QUERY);
query.with(new Sort(Direction.DESC, "foo"));
query.sortByScore();
assertThat(query,
isTextQuery().searchingFor(QUERY).returningScore().sortingByScore().sortingBy("foo", Direction.DESC));
}
/**
* @see DATAMONGO-850
*/
@Test
public void shouldUseCustomFieldnameForScoring() {
TextQuery query = new TextQuery(QUERY).includeScore("customFieldForScore").sortByScore();
assertThat(query, isTextQuery().searchingFor(QUERY).returningScoreAs("customFieldForScore").sortingByScore());
}
}

View File

@@ -24,6 +24,7 @@ import java.util.HashSet;
import java.util.List;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
@@ -880,4 +881,121 @@ public abstract class AbstractPersonRepositoryIntegrationTests {
assertThat(result.getContent(), hasSize(1));
}
/**
* @see DATAMONGO-745
*/
@Test
public void findByCustomQueryFirstnamesInListAndLastname() {
repository.save(new Person("foo", "bar"));
repository.save(new Person("bar", "bar"));
repository.save(new Person("fuu", "bar"));
repository.save(new Person("notfound", "bar"));
Page<Person> result = repository.findByCustomQueryFirstnamesAndLastname(Arrays.asList("bar", "foo", "fuu"), "bar",
new PageRequest(0, 2));
assertThat(result.getContent(), hasSize(2));
assertThat(result.getTotalPages(), is(2));
assertThat(result.getTotalElements(), is(3L));
}
/**
* @see DATAMONGO-745
*/
@Test
public void findByCustomQueryLastnameAndStreetInList() {
repository.save(new Person("foo", "bar").withAddress(new Address("street1", "1", "SB")));
repository.save(new Person("bar", "bar").withAddress(new Address("street2", "1", "SB")));
repository.save(new Person("fuu", "bar").withAddress(new Address("street1", "2", "RGB")));
repository.save(new Person("notfound", "notfound"));
Page<Person> result = repository.findByCustomQueryLastnameAndAddressStreetInList("bar",
Arrays.asList("street1", "street2"), new PageRequest(0, 2));
assertThat(result.getContent(), hasSize(2));
assertThat(result.getTotalPages(), is(2));
assertThat(result.getTotalElements(), is(3L));
}
/**
* @see DATAMONGO-950
*/
@Test
public void shouldLimitCollectionQueryToMaxResultsWhenPresent() {
repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"), new Person("Bob-3",
"Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan")));
List<Person> result = repository.findTop3ByLastnameStartingWith("Dylan");
assertThat(result.size(), is(3));
}
/**
* @see DATAMONGO-950
*/
@Test
public void shouldNotLimitPagedQueryWhenPageRequestWithinBounds() {
repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"), new Person("Bob-3",
"Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan")));
Page<Person> result = repository.findTop3ByLastnameStartingWith("Dylan", new PageRequest(0, 2));
assertThat(result.getContent().size(), is(2));
}
/**
* @see DATAMONGO-950
*/
@Test
public void shouldLimitPagedQueryWhenPageRequestExceedsUpperBoundary() {
repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"), new Person("Bob-3",
"Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan")));
Page<Person> result = repository.findTop3ByLastnameStartingWith("Dylan", new PageRequest(1, 2));
assertThat(result.getContent().size(), is(1));
}
/**
* @see DATAMONGO-950
*/
@Test
public void shouldReturnEmptyWhenPageRequestedPageIsTotallyOutOfScopeForLimit() {
repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"), new Person("Bob-3",
"Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan")));
Page<Person> result = repository.findTop3ByLastnameStartingWith("Dylan", new PageRequest(2, 2));
assertThat(result.getContent().size(), is(0));
}
/**
* Ignored for now as this requires Querydsl 3.4.1 to succeed.
*
* @see DATAMONGO-972
*/
@Test
@Ignore
public void shouldExecuteFindOnDbRefCorrectly() {
operations.remove(new org.springframework.data.mongodb.core.query.Query(), User.class);
User user = new User();
user.setUsername("Valerie Matthews");
operations.save(user);
dave.setCreator(user);
operations.save(dave);
assertThat(repository.findOne(QPerson.person.creator.eq(user)), is(dave));
}
/**
* @see DATAMONGO-969
*/
@Test
public void shouldFindPersonsWhenUsingQueryDslPerdicatedOnIdProperty() {
assertThat(repository.findAll(person.id.in(Arrays.asList(dave.id, carter.id))), containsInAnyOrder(dave, carter));
}
}

View File

@@ -261,6 +261,16 @@ public class Person extends Contact {
return this.getId().equals(that.getId());
}
public Person withAddress(Address address) {
this.address = address;
return this;
}
public void setCreator(User creator) {
this.creator = creator;
}
/*
* (non-Javadoc)
*

View File

@@ -290,4 +290,27 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
* @see DATAMONGO-893
*/
Page<Person> findByAddressIn(List<Address> address, Pageable page);
/**
* @see DATAMONGO-745
*/
@Query("{firstname:{$in:?0}, lastname:?1}")
Page<Person> findByCustomQueryFirstnamesAndLastname(List<String> firstnames, String lastname, Pageable page);
/**
* @see DATAMONGO-745
*/
@Query("{lastname:?0, address.street:{$in:?1}}")
Page<Person> findByCustomQueryLastnameAndAddressStreetInList(String lastname, List<String> streetNames, Pageable page);
/**
* @see DATAMONGO-950
*/
List<Person> findTop3ByLastnameStartingWith(String lastname);
/**
* @see DATAMONGO-950
*/
Page<Person> findTop3ByLastnameStartingWith(String lastname, Pageable pageRequest);
}

View File

@@ -54,7 +54,7 @@ import com.mongodb.WriteResult;
* @author Oliver Gierke
*/
@RunWith(MockitoJUnitRunner.class)
public class AbstracMongoQueryUnitTests {
public class AbstractMongoQueryUnitTests {
@Mock RepositoryMetadata metadataMock;
@Mock MongoOperations mongoOperationsMock;
@@ -88,7 +88,8 @@ public class AbstracMongoQueryUnitTests {
createQueryForMethod("deletePersonByLastname", String.class).setDeleteQuery(true).execute(new Object[] { "booh" });
verify(this.mongoOperationsMock, times(1)).remove(Matchers.any(Query.class), Matchers.eq("persons"));
verify(this.mongoOperationsMock, times(1)).remove(Matchers.any(Query.class), Matchers.eq(Person.class),
Matchers.eq("persons"));
verify(this.mongoOperationsMock, times(0)).find(Matchers.any(Query.class), Matchers.any(Class.class),
Matchers.anyString());
}
@@ -122,19 +123,21 @@ public class AbstracMongoQueryUnitTests {
/**
* @see DATAMONGO-566
* @see DATAMONGO-978
*/
@Test
public void testDeleteExecutionReturnsNrDocumentsDeletedFromWriteResult() {
when(writeResultMock.getN()).thenReturn(100);
when(this.mongoOperationsMock.remove(Matchers.any(Query.class), Matchers.eq("persons")))
when(this.mongoOperationsMock.remove(Matchers.any(Query.class), Matchers.eq(Person.class), Matchers.eq("persons")))
.thenReturn(writeResultMock);
MongoQueryFake query = createQueryForMethod("deletePersonByLastname", String.class);
query.setDeleteQuery(true);
assertThat(query.execute(new Object[] { "fake" }), is((Object) 100L));
verify(this.mongoOperationsMock, times(1)).remove(Matchers.any(Query.class), Matchers.eq("persons"));
verify(this.mongoOperationsMock, times(1)).remove(Matchers.any(Query.class), Matchers.eq(Person.class),
Matchers.eq("persons"));
}
private MongoQueryFake createQueryForMethod(String methodName, Class<?>... paramTypes) {

View File

@@ -0,0 +1,166 @@
/*
* Copyright 2014 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 static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.lang.reflect.Method;
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.Matchers;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.MongoOperations;
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.mapping.MongoMappingContext;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Person;
import org.springframework.data.mongodb.repository.Query;
import org.springframework.data.repository.core.RepositoryMetadata;
import com.mongodb.BasicDBObjectBuilder;
/**
* Unit tests for {@link PartTreeMongoQuery}.
*
* @author Christoph Strobl
* @author Oliver Gierke
*/
@RunWith(MockitoJUnitRunner.class)
public class PartTreeMongoQueryUnitTests {
@Mock RepositoryMetadata metadataMock;
@Mock MongoOperations mongoOperationsMock;
MongoMappingContext mappingContext;
public @Rule ExpectedException exception = ExpectedException.none();
@Before
@SuppressWarnings({ "unchecked", "rawtypes" })
public void setUp() {
when(metadataMock.getDomainType()).thenReturn((Class) Person.class);
when(metadataMock.getReturnedDomainClass(Matchers.any(Method.class))).thenReturn((Class) Person.class);
mappingContext = new MongoMappingContext();
DbRefResolver dbRefResolver = new DefaultDbRefResolver(mock(MongoDbFactory.class));
MongoConverter converter = new MappingMongoConverter(dbRefResolver, mappingContext);
when(mongoOperationsMock.getConverter()).thenReturn(converter);
}
/**
* @see DATAMOGO-952
*/
@Test
public void rejectsInvalidFieldSpecification() {
exception.expect(IllegalStateException.class);
exception.expectMessage("findByLastname");
deriveQueryFromMethod("findByLastname", new Object[] { "foo" });
}
/**
* @see DATAMOGO-952
*/
@Test
public void singleFieldJsonIncludeRestrictionShouldBeConsidered() {
org.springframework.data.mongodb.core.query.Query query = deriveQueryFromMethod("findByFirstname",
new Object[] { "foo" });
assertThat(query.getFieldsObject(), is(new BasicDBObjectBuilder().add("firstname", 1).get()));
}
/**
* @see DATAMOGO-952
*/
@Test
public void multiFieldJsonIncludeRestrictionShouldBeConsidered() {
org.springframework.data.mongodb.core.query.Query query = deriveQueryFromMethod("findByFirstnameAndLastname",
new Object[] { "foo", "bar" });
assertThat(query.getFieldsObject(), is(new BasicDBObjectBuilder().add("firstname", 1).add("lastname", 1).get()));
}
/**
* @see DATAMOGO-952
*/
@Test
public void multiFieldJsonExcludeRestrictionShouldBeConsidered() {
org.springframework.data.mongodb.core.query.Query query = deriveQueryFromMethod("findPersonByFirstnameAndLastname",
new Object[] { "foo", "bar" });
assertThat(query.getFieldsObject(), is(new BasicDBObjectBuilder().add("firstname", 0).add("lastname", 0).get()));
}
private org.springframework.data.mongodb.core.query.Query deriveQueryFromMethod(String method, Object[] args) {
Class<?>[] types = new Class<?>[args.length];
for (int i = 0; i < args.length; i++) {
types[i] = args[i].getClass();
}
PartTreeMongoQuery partTreeQuery = createQueryForMethod(method, types);
MongoParameterAccessor accessor = new MongoParametersParameterAccessor(partTreeQuery.getQueryMethod(), args);
return partTreeQuery.createQuery(new ConvertingParameterAccessor(mongoOperationsMock.getConverter(), accessor));
}
private PartTreeMongoQuery createQueryForMethod(String methodName, Class<?>... paramTypes) {
try {
Method method = Repo.class.getMethod(methodName, paramTypes);
MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadataMock, mappingContext);
return new PartTreeMongoQuery(queryMethod, mongoOperationsMock);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException(e.getMessage(), e);
} catch (SecurityException e) {
throw new IllegalArgumentException(e.getMessage(), e);
}
}
interface Repo extends MongoRepository<Person, Long> {
@Query(fields = "firstname")
Person findByLastname(String lastname);
@Query(fields = "{ 'firstname' : 1 }")
Person findByFirstname(String lastname);
@Query(fields = "{ 'firstname' : 1, 'lastname' : 1 }")
Person findByFirstnameAndLastname(String firstname, String lastname);
@Query(fields = "{ 'firstname' : 0, 'lastname' : 0 }")
Person findPersonByFirstnameAndLastname(String firstname, String lastname);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011-2013 the original author or authors.
* Copyright 2011-2014 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.
@@ -17,8 +17,10 @@ package org.springframework.data.mongodb.repository.support;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.springframework.data.mongodb.core.DBObjectTestUtils.*;
import org.bson.types.ObjectId;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -32,6 +34,7 @@ import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.repository.QAddress;
import org.springframework.data.mongodb.repository.QPerson;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.mysema.query.types.expr.BooleanOperation;
@@ -43,6 +46,7 @@ import com.mysema.query.types.path.StringPath;
* Unit tests for {@link SpringDataMongodbSerializer}.
*
* @author Oliver Gierke
* @author Christoph Strobl
*/
@RunWith(MockitoJUnitRunner.class)
public class SpringDataMongodbSerializerUnitTests {
@@ -132,6 +136,41 @@ public class SpringDataMongodbSerializerUnitTests {
assertThat(path, is("0"));
}
/**
* @see DATAMONGO-969
*/
@Test
public void shouldConvertObjectIdEvenWhenNestedInOperatorDbObject() {
ObjectId value = new ObjectId("53bb9fd14438765b29c2d56e");
DBObject serialized = serializer.asDBObject("_id", new BasicDBObject("$ne", value.toString()));
DBObject _id = getAsDBObject(serialized, "_id");
ObjectId $ne = getTypedValue(_id, "$ne", ObjectId.class);
assertThat($ne, is(value));
}
/**
* @see DATAMONGO-969
*/
@Test
public void shouldConvertCollectionOfObjectIdEvenWhenNestedInOperatorDbObject() {
ObjectId firstId = new ObjectId("53bb9fd14438765b29c2d56e");
ObjectId secondId = new ObjectId("53bb9fda4438765b29c2d56f");
BasicDBList objectIds = new BasicDBList();
objectIds.add(firstId.toString());
objectIds.add(secondId.toString());
DBObject serialized = serializer.asDBObject("_id", new BasicDBObject("$in", objectIds));
DBObject _id = getAsDBObject(serialized, "_id");
Object[] $in = getTypedValue(_id, "$in", Object[].class);
assertThat($in, Matchers.<Object> arrayContaining(firstId, secondId));
}
class Address {
String id;
String street;

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2014 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.test.util;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.internal.AssumptionViolatedException;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.springframework.data.util.Version;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.mongodb.BasicDBObjectBuilder;
import com.mongodb.CommandResult;
import com.mongodb.DB;
import com.mongodb.MongoClient;
/**
* {@link TestRule} verifying server tests are executed against match a given version. This one can be used as
* {@link ClassRule} eg. in context depending tests run with {@link SpringJUnit4ClassRunner} when the context would fail
* to start in case of invalid version, or as simple {@link Rule} on specific tests.
*
* @author Christoph Strobl
* @since 1.6
*/
public class MongoVersionRule implements TestRule {
private String host = "localhost";
private int port = 27017;
private final Version minVersion;
private final Version maxVersion;
private Version currentVersion;
public MongoVersionRule(Version min, Version max) {
this.minVersion = min;
this.maxVersion = max;
}
public static MongoVersionRule any() {
return new MongoVersionRule(new Version(0, 0, 0), new Version(9999, 9999, 9999));
}
public static MongoVersionRule atLeast(Version minVersion) {
return new MongoVersionRule(minVersion, new Version(9999, 9999, 9999));
}
public static MongoVersionRule atMost(Version maxVersion) {
return new MongoVersionRule(new Version(0, 0, 0), maxVersion);
}
public MongoVersionRule withServerRunningAt(String host, int port) {
this.host = host;
this.port = port;
return this;
}
@Override
public Statement apply(final Statement base, Description description) {
initCurrentVersion();
return new Statement() {
@Override
public void evaluate() throws Throwable {
if (currentVersion != null) {
if (currentVersion.isLessThan(minVersion) || currentVersion.isGreaterThan(maxVersion)) {
throw new AssumptionViolatedException(String.format(
"Expected mongodb server to be in range %s to %s but found %s", minVersion, maxVersion, currentVersion));
}
}
base.evaluate();
}
};
}
private void initCurrentVersion() {
if (currentVersion == null) {
try {
MongoClient client;
client = new MongoClient(host, port);
DB db = client.getDB("test");
CommandResult result = db.command(new BasicDBObjectBuilder().add("buildInfo", 1).get());
this.currentVersion = Version.parse(result.get("version").toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

View File

@@ -8,7 +8,7 @@ Export-Template:
org.springframework.data.mongodb.*;version="${project.version}"
Import-Template:
com.google.common.base.*;version="[11.0.0,14.0.0)";resolution:=optional,
com.mongodb.*;version="${mongo:[=.=.=,+1.0.0)}",
com.mongodb.*;version="${mongo.osgi:[=.=.=,+1.0.0)}",
com.mysema.query.*;version="[2.1.1, 3.0.0)";resolution:=optional,
javax.annotation.processing.*;version="0",
javax.enterprise.*;version="${cdi:[=.=.=,+1.0.0)}";resolution:=optional,

View File

@@ -68,7 +68,7 @@
<xi:include href="introduction/introduction.xml"/>
<xi:include href="introduction/requirements.xml"/>
<xi:include href="introduction/getting-started.xml"/>
<xi:include href="https://raw.github.com/spring-projects/spring-data-commons/1.8.0.RELEASE/src/docbkx/repositories.xml">
<xi:include href="https://raw.github.com/spring-projects/spring-data-commons/1.9.0.M1/src/docbkx/repositories.xml">
<xi:fallback href="../../../spring-data-commons/src/docbkx/repositories.xml" />
</xi:include>
</part>
@@ -88,10 +88,10 @@
<part id="appendix">
<title>Appendix</title>
<xi:include href="https://raw.github.com/spring-projects/spring-data-commons/1.8.0.RELEASE/src/docbkx/repository-namespace-reference.xml">
<xi:include href="https://raw.github.com/spring-projects/spring-data-commons/1.9.0.M1/src/docbkx/repository-namespace-reference.xml">
<xi:fallback href="../../../spring-data-commons/src/docbkx/repository-namespace-reference.xml" />
</xi:include>
<xi:include href="https://raw.github.com/spring-projects/spring-data-commons/1.8.0.RELEASE/src/docbkx/repository-query-keywords-reference.xml">
<xi:include href="https://raw.github.com/spring-projects/spring-data-commons/1.9.0.M1/src/docbkx/repository-query-keywords-reference.xml">
<xi:fallback href="../../../spring-data-commons/src/docbkx/repository-query-keywords-reference.xml" />
</xi:include>
</part>

View File

@@ -18,20 +18,19 @@
<section id="get-started:help:community">
<title>Community Forum</title>
<para>The Spring Data <ulink
url="http://forum.spring.io/forum/spring-projects/data">forum
</ulink> is a message board for all Spring Data (not just Document)
users to share information and help each other. Note that registration
is needed <emphasis>only</emphasis> for posting.</para>
<para>Spring Data on Stackoverflow <ulink
url="http://stackoverflow.com/questions/tagged/spring-data">Stackoverflow
</ulink> is a tag for all Spring Data (not just Document) users to share
information and help each other. Note that registration is needed
<emphasis>only</emphasis> for posting.</para>
</section>
<section id="get-started:help:professional">
<title>Professional Support</title>
<para>Professional, from-the-source support, with guaranteed response
time, is available from <ulink
url="http://gopivotal.com/">Pivotal Sofware, Inc.</ulink>, the company
behind Spring Data and Spring.</para>
time, is available from <ulink url="http://gopivotal.com/">Pivotal
Sofware, Inc.</ulink>, the company behind Spring Data and Spring.</para>
</section>
</section>
@@ -44,22 +43,21 @@
homepage</ulink>.</para>
<para>You can help make Spring Data best serve the needs of the Spring
community by interacting with developers through the Spring Community
<ulink url="http://forum.spring.io/">forums</ulink>. To follow
developer activity look for the mailing list information on the Spring
Data Mongo homepage.</para>
community by interacting with developers through the Community on <ulink
url="http://stackoverflow.com/questions/tagged/spring-data">Stackoverflow</ulink>.
To follow developer activity look for the mailing list information on the
Spring Data Mongo homepage.</para>
<para>If you encounter a bug or want to suggest an improvement, please
create a ticket on the Spring Data issue <ulink
url="https://jira.springframework.org/browse/DATAMONGO">tracker</ulink>.</para>
url="https://jira.spring.io/browse/DATAMONGO">tracker</ulink>.</para>
<para>To stay up to date with the latest news and announcements in the
Spring eco system, subscribe to the Spring Community <ulink
url="https://spring.io">Portal</ulink>.</para>
<para>Lastly, you can follow the SpringSource Data <ulink
url="https://spring.io/blog">blog </ulink>or
the project team on Twitter (<ulink
url="http://twitter.com/SpringData">SpringData</ulink>)</para>
url="https://spring.io/blog">blog </ulink>or the project team on Twitter
(<ulink url="http://twitter.com/SpringData">SpringData</ulink>).</para>
</section>
</chapter>

View File

@@ -357,6 +357,16 @@ public class Person {
level to describe how to geoindex the field.</para>
</listitem>
<listitem>
<para><literal>@TextIndexed</literal> - applied at the field level
to mark the field to be included in the text index.</para>
</listitem>
<listitem>
<para><literal>@Language</literal> - applied at the field level to
set the language override property for text index.</para>
</listitem>
<listitem>
<para><literal>@Transient</literal> - by default all private fields
are mapped to the document, this annotation excludes the field where
@@ -526,7 +536,7 @@ OrderItem item = converter.read(OrderItem.class, input);</programlisting>
test suite.</para>
</section>
<section id="mapping-usage-indexes">
<section id="mapping-usage-indexes.compound-index">
<title>Compound Indexes</title>
<para>Compound indexes are also supported. They are defined at the class
@@ -561,6 +571,51 @@ public class Person {
</example></para>
</section>
<section id="mapping-usage-indexes.text-index">
<title>Text Indexes</title>
<note>
<para>The text index feature is disabled by default for mongodb
v.2.4.</para>
</note>
<para>Creating a text index allows to accumulate several fields into a
searchable full text index. It is only possible to have one text index
per collection so all fields marked with
<interfacename>@TextIndexed</interfacename> are combined into this
index. Properties can be weighted to influence document score for
ranking results. The default language for the text index is english, to
change the default language set
<interfacename>@Document(language="spanish")</interfacename> to any
language you want. Using a property called <literal>language</literal>
or <interfacename>@Language</interfacename> allows to define a language
override on a per document base.</para>
<example>
<title>Example Text Index Usage</title>
<programlisting language="java">@Document(language = "spanish")
class SomeEntity {
@TextIndexed String foo;
@Language String lang;
Nested nested;
}
class Nested {
@TextIndexed(weight=5) String bar;
String roo;
}
</programlisting>
</example>
</section>
<section id="mapping-usage-references">
<title>Using DBRefs</title>

View File

@@ -2016,6 +2016,67 @@ GeoResults&lt;Restaurant&gt; = operations.geoNear(query, Restaurant.class);</pro
origin.</para>
</section>
</section>
<section id="mongo.textsearch">
<title>Full Text Queries</title>
<para>Since mongodb 2.6 full text queries can be excuted using the
<literal>$text</literal> operator. Methods and operations specific for
full text queries are available in <classname>TextQuery</classname> and
<classname>TextCriteria</classname>. When doing full text search please
refer to the <ulink
url="http://docs.mongodb.org/manual/reference/operator/query/text/#behavior">mongodb
reference</ulink> for its behavior and limitations.</para>
<example>
<title>Full Text Search</title>
<para>Bevore we are actually able to use full text search we have to
ensure to set up the search index correctly. Please refer to section
<link linkend="mapping-usage-indexes.text-index">Text Index</link> for
creating index structures.</para>
<programlisting language="javascript">db.foo.ensureIndex(
{
title : "text",
content : "text"
},
{
weights : {
title : 3
}
}
)
</programlisting>
<para>A query searching for <literal>coffee cake</literal>, sorted by
relevance according to the <literal>weights</literal> can be defined
and executed as:</para>
<programlisting language="java">Query query = TextQuery.searching(new TextCriteria().matchingAny("coffee", "cake")).sortByScore();
List&lt;Document&gt; page = template.find(query, Document.class);
</programlisting>
<para>Exclusion of search terms can directly be done by prefixing the
term with <literal>-</literal> or using
<literal>notMatching</literal></para>
<programlisting language="java">// search for 'coffee' and not 'cake'
TextQuery.searching(new TextCriteria().matching("coffee").matching("-cake"));
TextQuery.searching(new TextCriteria().matching("coffee").notMatching("cake"));
</programlisting>
<para>As <classname>TextCriteria.matching</classname> takes the
provided term as is. Therefore phrases can be defined by putting them
between double quotes (eg. <literal>\"coffee cake\")</literal> or
using <classname>TextCriteria.phrase.</classname></para>
<programlisting language="java">// search for phrase 'coffee cake'
TextQuery.searching(new TextCriteria().matching("\"coffee cake\""));
TextQuery.searching(new TextCriteria().phrase("coffee cake"));
</programlisting>
</example>
</section>
</section>
<section id="mongo.mapreduce">
@@ -3046,11 +3107,12 @@ class MyConverter implements Converter&lt;String, Person&gt; { … }</programlis
</listitem>
</itemizedlist></para>
<para>You can create both standard indexes and geospatial indexes using
the classes <classname>IndexDefinition</classname> and
<classname>GeoSpatialIndex</classname> respectfully. For example, given
the Venue class defined in a previous section, you would declare a
geospatial query as shown below</para>
<para>You can create standard, geospatial and text indexes using the
classes <classname>IndexDefinition</classname>,
<classname>GeoSpatialIndex</classname> and
<classname>TextIndexDefinition</classname>. For example, given the Venue
class defined in a previous section, you would declare a geospatial
query as shown below.</para>
<programlisting language="java">mongoTemplate.indexOps(Venue.class).ensureIndex(new GeospatialIndex("location"));</programlisting>
</section>

View File

@@ -1,6 +1,57 @@
Spring Data MongoDB Changelog
=============================
Changes in version 1.6.0.M1 (2014-07-10)
----------------------------------------
* DATAMONGO-983 - Remove links to forum.spring.io.
* DATAMONGO-982 - Assure compatibility with upcoming MongoDB driver versions.
* DATAMONGO-981 - Release 1.6 M1.
* DATAMONGO-980 - Use meta annotations from spring data commons for @Score.
* DATAMONGO-978 - deleteBy/removeBy repository methods don't set type information in Before/AfterDeleteEvent.
* DATAMONGO-977 - Adapt to Spring 4 upgrade.
* DATAMONGO-976 - Add support for reading $meta projection on textScore into document.
* DATAMONGO-975 - Add support for date/time operators in aggregation framework.
* DATAMONGO-973 - Add support for deriving full text queries.
* DATAMONGO-972 - References are not handled properly in Querydsl integration.
* DATAMONGO-970 - Id query cannot be created if object to remove is DBObject.
* DATAMONGO-969 - String @id field is not mapped to ObjectId when using QueryDSL .id.in(Collection<String>).
* DATAMONGO-968 - Add support for $meta projections and sorting for textScore metadata.
* DATAMONGO-963 - Compound index with expireAfterSeconds causes repeating error on mongodb server.
* DATAMONGO-962 - “Cycle found” with Spring Data Mongo 1.5.
* DATAMONGO-960 - Allow to pass options to the Aggregation Pipeline.
* DATAMONGO-958 - Move to FieldNamingStrategy SPI in Spring Data Commons.
* DATAMONGO-954 - Add support for System Variables in Aggregations.
* DATAMONGO-953 - Update object should have a proper equals/hashcode/toString.
* DATAMONGO-952 - @Query annotation does not work with only field restrictions.
* DATAMONGO-950 - Add support for limiting the query result in the query derivation mechanism.
* DATAMONGO-949 - CyclicPropertyReferenceException in versions 1.5.0 + for MongoDB.
* DATAMONGO-948 - Assertion error in MongoTemplate.getMappedSortObject.
* DATAMONGO-944 - Add support $currentDate to Update.
* DATAMONGO-938 - Exception when creating geo within Criteria using MapReduce.
* DATAMONGO-937 - Add support for creating text index.
* DATAMONGO-850 - Add support for text search using $text.
* DATAMONGO-745 - @Query($in) and Pageable in result Page total = 0.
Changes in version 1.4.3.RELEASE (2014-06-18)
---------------------------------------------
* DATAMONGO-955 - Release 1.4.3.
* DATAMONGO-953 - Update object should have a proper equals/hashcode/toString.
* DATAMONGO-952 - @Query annotation does not work with only field restrictions.
* DATAMONGO-948 - Assertion error in MongoTemplate.getMappedSortObject.
* DATAMONGO-938 - Exception when creating geo within Criteria using MapReduce.
* DATAMONGO-924 - Aggregation not working with as() method in project() pipeline operator.
* DATAMONGO-920 - Fix debug messages for delete events in AbstractMongoEventListener.
* DATAMONGO-917 - DefaultDbRefResolver throws NPE when bundled into an uberjar.
* DATAMONGO-914 - Improve resolving of LazyLoading proxies for classes that override equals/hashcode.
* DATAMONGO-913 - Can't query using lazy DBRef objects.
* DATAMONGO-912 - Aggregation#project followed by Aggregation#match with custom converter causes IllegalArgumentException.
* DATAMONGO-898 - MapReduce seems not to work when javascript not being escaped.
* DATAMONGO-847 - Allow usage of Criteria within Update.
* DATAMONGO-745 - @Query($in) and Pageable in result Page total = 0.
* DATAMONGO-647 - Using "OrderBy" in "query by method name" ignores the @Field annotation for field alias.
Changes in version 1.5.0.RELEASE (2014-05-20)
---------------------------------------------
* DATAMONGO-936 - Release 1.5 GA.

View File

@@ -1,4 +1,4 @@
Spring Data MongoDB 1.5 GA
Spring Data MongoDB 1.6 M1
Copyright (c) [2010-2014] Pivotal Software, Inc.
This product is licensed to you under the Apache License, Version 2.0 (the "License").