Compare commits

...

201 Commits

Author SHA1 Message Date
Christoph Strobl
32ae2e9ddc DATAMONGO-2160 - Release version 2.2 M1 (Moore). 2018-12-11 10:53:53 +01:00
Christoph Strobl
6fa0625167 DATAMONGO-2160 - Prepare 2.2 M1 (Moore). 2018-12-11 10:53:11 +01:00
Christoph Strobl
e88b88ef36 DATAMONGO-2160 - Updated changelog. 2018-12-11 10:53:07 +01:00
Mark Paluch
2ca879f534 DATAMONGO-2138 - Polishing.
Rename NestedProperty to KPropertyPath to reflect the underlying concept in alignment with our own PropertyPath type. Rename nestedFieldName(…) method to asString(…) to align with Kotlin method terminology. Reformat.

Slightly reword documentation. Add Type-safe Queries for Kotlin to What's New section.

Original pull request: #622.
2018-12-10 15:49:49 +01:00
Tjeu Kayim
8d969ef41d DATAMONGO-2138 - Add Type-safe Kotlin query extension.
We now support type-safe queries using Kotlin's DSL capabilities by accepting property references. Property references map to property paths and are translated to Criteria objects.

mongoOperations.find<Book>(
  Query(Book::title isEqualTo "Moby-Dick")
)

mongoOperations.find<Book>(
  Query(Book::title exists true)
)

mongoOperations.find<Book>(
 Query(Criteria().andOperator(
  Book::price gt 5,
  Book::price lt 10)
))

Original pull request: #622.
2018-12-10 15:49:32 +01:00
Mark Paluch
bbd02107b2 DATAMONGO-2161 - Simplify reference documentation setup.
Remove Asciidoctor plugin from all module builds and run asciidoctor only in distribution build.
2018-12-10 10:20:35 +01:00
Mark Paluch
4a1a9a7b0c DATAMONGO-2150 - Polishing.
Fix imperative auditing test to use intended persist mechanism. Remove final keywords from method args and local variables in ReactiveMongoTemplate. Rename DBObject to Document.

Original Pull Request: #627
2018-12-07 14:32:32 +01:00
Mark Paluch
adf16bb31f DATAMONGO-2150 - Fixed broken auditing for entities using optimistic locking.
The previous implementation of ReactiveMongoTemplate.doSaveVersioned(…) prematurely initialized the version property so that the entity wasn't considered new by the auditing subsystem. Even worse, for primitive version properties, the initialization kept the property at a value of 0, so that the just persisted entity was still considered new. This mean that via the repository route, inserts are triggered even for subsequent attempts to save an entity which caused duplicate key exceptions.

We now make sure we fire the BeforeConvertEvent before the version property is initialized or updated. Also, the initialization of the property now sets primitive properties to 1 initially.

Added integration tests for the auditing via ReactiveMongoTemplate and repositories.

Related ticket: DATAMONGO-2139.

Original Pull Request: #627
2018-12-07 14:32:05 +01:00
Mark Paluch
a834f29f32 DATAMONGO-2156 - Polishing.
Original pull request: #626.
2018-12-05 14:35:00 +01:00
Mark Paluch
d8721c9930 DATAMONGO-2156 - Remove dependency to javax.xml.bind.
We now no longer use DatatypeConverter to convert byte[] to its Base64-representation but use Spring Framework's Base64 utils.

Original pull request: #626.
2018-12-05 14:35:00 +01:00
Mark Paluch
8fe734deb1 DATAMONGO-2115 - Polishing.
Simplify asTimestampOfType(…) retrieval and move the cast to outer method. Simplify test.

Original pull request: #624.
2018-12-04 15:46:17 +01:00
Christoph Strobl
9246d3311d DATAMONGO-2115 - Add support for BsonTimestamp when resuming Change Streams.
Original pull request: #624.
2018-12-04 15:46:08 +01:00
Mark Paluch
6e3e3e4a01 DATAMONGO-2149 - Polishing.
Add ticket reference to follow-up ticket regarding array matching on partial DBRef expressions.

Related ticket: DATAMONGO-2154

Original pull request: #623.
2018-11-30 14:54:05 +01:00
Christoph Strobl
37b9eb3580 DATAMONGO-2149 - Fix $slice in fields projection when pointing to array of DBRefs.
We now no longer try to convert the actual slice parameters into a DBRef.

Original pull request: #623.
2018-11-30 14:54:05 +01:00
Mark Paluch
da1384b84d DATAMONGO-2148 - Polishing.
Add author tag. Add logging for ReactiveMongoTemplate.count(…) and findDistinct(…) operations. Fix variable names.

Original pull request: #620.
2018-11-28 17:25:14 +01:00
Cimon Lucas (LCM)
6c0dd2c80e DATAMONGO-2148 - Add query logging for MongoTemplate.count(…).
Original pull request: #620.
2018-11-28 17:25:02 +01:00
Mark Paluch
28d96f5fe2 DATAMONGO-2121 - Updated changelog. 2018-11-27 14:54:01 +01:00
Mark Paluch
5372a5c8c1 DATAMONGO-2109 - Updated changelog. 2018-11-27 12:36:46 +01:00
Mark Paluch
8672dff5bc DATAMONGO-2110 - Updated changelog. 2018-11-27 11:27:20 +01:00
Mark Paluch
e010bee286 DATAMONGO-2119 - Polishing.
Convert anonymous JSON callback class into a private static one. Use an expressive Pattern constant.

Original pull request: #621.
2018-11-23 09:48:09 +01:00
Christoph Strobl
07b8e8278e DATAMONGO-2119 - Allow SpEL usage for annotated $regex query.
Original pull request: #621.
2018-11-23 09:47:50 +01:00
Oliver Drotbohm
8f4db5540d DATAMONGO-2108 - Fixed broken auditing for entities using optimistic locking.
The previous implementation of MongoTemplate.doSaveVersioned(…) prematurely initialized the version property so that the entity wasn't considered new by the auditing subsystem. Even worse, for primitive version properties, the initialization kept the property at a value of 0, so that the just persisted entity was still considered new. This mean that via the repository route, inserts are triggered even for subsequent attempts to save an entity which caused duplicate key exceptions.

We now make sure we fire the BeforeConvertEvent before the version property is initialized or updated. Also, the initialization of the property now sets primitive properties to 1 initially.

Added integration tests for the auditing via MongoOperations and repositories.
2018-11-22 15:05:10 +01:00
Oliver Drotbohm
9a0dad1723 DATAMONGO-2135 - Default to intermediate List for properties typed to Collection.
We now defensively create a List rather than a LinkedHashSet (which Spring's CollectionFactory.createCollection(…) defaults to) to make sure we're not accidentally dropping values that are considered equal according to their Java class definition.
2018-11-19 19:36:22 +01:00
Mark Paluch
a4c5682f4d DATAMONGO-2130 - Polishing.
Replace duplicate checks to ClientSession.hasActiveTransaction() with MongoResourceHolder.hasActiveTransaction(). Introduce MongoResourceHolder.getRequiredSession() to avoid nullability warnings.

Original pull request: #618.
2018-11-16 12:58:26 +01:00
Christoph Strobl
b2d2c12347 DATAMONGO-2130 - Polishing.
Set timeout for InetAdress host lookup to reduce test execution time.

Original pull request: #618.
2018-11-16 12:58:23 +01:00
Christoph Strobl
b50b645d1f DATAMONGO-2130 - Fix Repository count & exists inside transaction.
We now make sure invocations on repository count and exists methods delegate to countDocuments when inside a transaction.

Original pull request: #618.
2018-11-16 12:58:16 +01:00
Mark Paluch
4c6f793870 DATAMONGO-1798 - Polishing.
Introduce FieldType to express the desired field type to use for MongoDB Id conversion. Adapt Querydsl Id conversion so Id values are converted in the QueryMapper and no longer in SpringDataMongodbSerializer.

Adapt tests. Move MongoId from o.s.d.mongodb to o.s.d.m.c.core. Javadoc, reference docs.

Original pull request: #617.
2018-11-16 11:37:16 +01:00
Christoph Strobl
5d39191f0b DATAMONGO-1798 - Introduce @MongoId annotation for fine grained id conversion control.
@MongoId allows more fine grained control over id conversion by specifying the intended id target type. This allows to skip the automatic to ObjectId conversion of values that happen to be valid ObjectId hex strings.

public class PlainStringId {
  @MongoId String id; // treated as String no matter what
}

public class PlainObjectId {
  @MongoId ObjectId id; // treated as ObjectId
}

public class StringToObjectId {
  @MongoId(FieldType.OBJECT_ID) String id; // treated as ObjectId if the value is a valid ObjectId hex string
}

Original pull request: #617.
2018-11-16 11:36:36 +01:00
Mark Paluch
75b6dc7a0e DATAMONGO-2107 - Updated changelog. 2018-10-29 14:30:29 +01:00
Mark Paluch
0c8f176e70 DATAMONGO-2118 - Polishing.
Fix typo in reactive repositories reference documentation.

Original pull request: #611.
2018-10-26 10:07:54 +02:00
Mona Mohamadinia
c900ceb0d9 DATAMONGO-2118 - Fix typo in repositories reference documentation.
Original pull request: #611.
2018-10-26 10:07:48 +02:00
Mark Paluch
9072e0a703 DATAMONGO-2098 - Polishing.
Annotate methods and parameters with Nullable. Use diamond syntax where appropriate.

Original pull request: #612.
2018-10-25 15:35:10 +02:00
Zied Yaich
67991568f3 DATAMONGO-2098 - Fix typo in MappingMongoConverterParser method.
Original pull request: #612.
2018-10-25 15:34:49 +02:00
Mark Paluch
bc981be9f3 DATAMONGO-2113 - Polishing.
Increase subscription await timeout to allow for slow system processing such as on TravisCI.

Original pull request: #615.
2018-10-25 14:33:16 +02:00
Christoph Strobl
398eafe615 DATAMONGO-2113 - Polishing.
Use AssertJ in tests.

Original pull request: #615.
2018-10-25 14:33:13 +02:00
Christoph Strobl
4673525462 DATAMONGO-2113 - Fix resumeTimestamp conversion for change streams.
We now use the first 32 bits of the timestamp to create the instant and ignore the ordinal value.

Original pull request: #615.
2018-10-25 14:33:01 +02:00
Mark Paluch
5c009e393f DATAMONGO-2083 - Updated changelog. 2018-10-15 14:28:03 +02:00
Mark Paluch
e8e24632e4 DATAMONGO-2094 - Updated changelog. 2018-10-15 11:37:23 +02:00
Mark Paluch
5c54663adf DATAMONGO-2096 - Polishing.
Migrate assertions to AssertJ.

Original pull request: #613.
2018-10-05 15:02:25 +02:00
Christoph Strobl
f04e67edf0 DATAMONGO-2096 - Fix target field name for GraphLookup aggregation operation.
We now make sure to use the target field name instead of the alias when processing GraphLookupOperation.

Original pull request: #613.
2018-10-05 15:02:10 +02:00
Mark Paluch
74269aa6c0 DATAMONGO-2061 - After release cleanups. 2018-09-21 07:45:28 -04:00
Mark Paluch
bb743f494a DATAMONGO-2061 - Prepare next development iteration. 2018-09-21 07:45:26 -04:00
Mark Paluch
a502ffabc3 DATAMONGO-2061 - Release version 2.1 GA (Lovelace). 2018-09-21 07:08:38 -04:00
Mark Paluch
ffe4e9b914 DATAMONGO-2061 - Prepare 2.1 GA (Lovelace). 2018-09-21 07:07:51 -04:00
Mark Paluch
914bdd9434 DATAMONGO-2061 - Updated changelog. 2018-09-21 07:07:46 -04:00
Christoph Strobl
3cd9542483 DATAMONGO-2091 - Upgrade to MongoDB Java Driver 3.8.2 and Reactive Streams Driver 1.9.2 2018-09-20 10:45:23 +02:00
Khaled Baklouti
586bf858f9 DATAMONGO-2087 - Fix typo in MongoRepository.
Original Pull Request: #610
2018-09-20 08:28:44 +02:00
Mark Paluch
3478fd5ab3 DATAMONGO-2090 - Include documentation about Object Mapping Fundamentals.
Related ticket: DATACMNS-1374.
2018-09-18 13:24:40 +02:00
Christoph Strobl
fa5f523c92 DATAMONGO-2086 - Polishing.
Add fix for bound reified type in fluent MapReduce operations.
Also add missing reified type extension to FindDistinct with projection.

Original Pull Request: #609
2018-09-17 14:02:05 +02:00
Mark Paluch
2191ab3bba DATAMONGO-2086 - Fix Fluent API Kotlin extension generics to allow projections.
We now fixed Kotlin extension generics to properly use projections by ignoring the source type of the Fluent API object. Previously, the source and target type were linked which prevented the use of a different result type.

Original Pull Request: #609
2018-09-17 13:49:17 +02:00
Mark Paluch
a79142931f DATAMONGO-2034 - Updated changelog. 2018-09-10 14:15:49 +02:00
Mark Paluch
1ba210366d DATAMONGO-2035 - Updated changelog. 2018-09-10 10:20:54 +02:00
Christoph Strobl
16aa611007 DATAMONGO-2080 - Polishing.
Remove obsolete classes, update Javadoc and fix tests calling all() instead of tail().

Original Pull Request: #608
2018-09-06 15:10:21 +02:00
Mark Paluch
13e29eb81f DATAMONGO-2080 - Use fluent API for reactive tailable query methods.
Using the fluent API allows using DTO projections with properties that are unknown to the actual domain object. Previously, DTO projections attempted to read the domain type and during property access, missing properties were reported with an IllegalArgumentException. Unknown properties remain now unset.

Original Pull Request: #608
2018-09-06 15:09:53 +02:00
Mark Paluch
fe90950880 DATAMONGO-2080 - Support tailable cursors with the fluent reactive API.
We now support queries to return a tailable cursor using the fluent reactive API.

 query(Human.class)
     .inCollection("star-wars")
     .as(Jedi.class)
     .matching(query(where("firstname").is("luke")))
     .tail();

Original Pull Request: #608
2018-09-06 15:09:14 +02:00
Christoph Strobl
492dec8ecf DATAMONGO-2078 - Update reference documentation.
Move and enhance tailable cursor documetation. Move to separate file, preserve anchor and add imperative way using a MessageListener.

Add additional notes on usage of com.mongodb.client.MongoClient.

Original pull request: #607.
2018-09-03 14:09:15 +02:00
Mark Paluch
a1ac2f7c1d DATAMONGO-2075 - Polishing.
Tweaks to Javadoc and reference docs to align with american-english spelling.

Original pull request: #606.
2018-09-03 11:20:45 +02:00
Christoph Strobl
04e53316c6 DATAMONGO-2075 - Open up MongoTransactionManager to allow transaction commit customization and commit retry.
Original pull request: #606.
2018-09-03 11:20:45 +02:00
Oliver Gierke
a991b96518 DATAMONGO-2076 - Fixed attribute substitution in reactive MongoDB section.
We now redeclare the Asciidoctor Maven plugin to register the store specific attributes. Apparently they must not contain dots, so we replaced them with dashes.
2018-08-30 11:45:01 +02:00
Oliver Gierke
d53c5cf5c4 DATAMONGO-2076 - Fixed attribute substitution in getting started section. 2018-08-30 09:30:38 +02:00
Christoph Strobl
90779bbb27 DATAMONGO-2069 - Replace com.mysema.commons.lang.Assert with o.s.util.Assert. 2018-08-24 08:42:36 +02:00
Oliver Gierke
892cc2e69a DATAMONGO-2065 - Polishing. 2018-08-22 11:16:21 +02:00
Oliver Gierke
a69f1b4d51 DATAMONGO-2065 - Make sure that MongoTemplate.doSave(…) triggers overridable property population.
We now consistently call MongoTemplate.populateIdIfNecessary(…) to allow subclasses to override these calls.
2018-08-22 11:16:21 +02:00
Christoph Strobl
7859ee1013 DATAMONGO-2064 - Upgrade MongoDB Java Driver to 3.8.1. 2018-08-21 10:12:42 +02:00
Oliver Gierke
a58562ba69 DATAMONGO-2033 - After release cleanups. 2018-08-20 10:56:52 +02:00
Oliver Gierke
779b0da358 DATAMONGO-2033 - Prepare next development iteration. 2018-08-20 10:56:51 +02:00
Oliver Gierke
ff1703f7c9 DATAMONGO-2033 - Release version 2.1 RC2 (Lovelace). 2018-08-20 10:40:11 +02:00
Oliver Gierke
7b23f8eee2 DATAMONGO-2033 - Prepare 2.1 RC2 (Lovelace). 2018-08-20 10:39:43 +02:00
Oliver Gierke
cc97c5a961 DATAMONGO-2033 - Updated changelog. 2018-08-20 10:39:34 +02:00
Christoph Strobl
08a57e58fd DATAMONGO-2052 - Add support for $arrayToObject and $objectToArray aggregation operators.
Original pull request: #603.
2018-08-17 17:33:16 +02:00
Mark Paluch
9d27d2ff8e DATAMONGO-2059 - Document count helper restrictions for geo commands inside of transactions. 2018-08-17 17:23:53 +02:00
Mark Paluch
3eba7de073 DATAMONGO-2053 - Polishing.
Tweak Javadoc. Surpress generics warnings. Remove nullable annotation from ObjectOperatorFactory.value as it cannot be null. Extend tests. Reformat.

Original pull request: #601.
2018-08-16 11:40:07 +02:00
Christoph Strobl
3dc6cab132 DATAMONGO-2053 - Add support for $mergeObjects aggregation operator.
Original pull request: #601.
2018-08-16 11:39:57 +02:00
Mark Paluch
799fa6c87e DATAMONGO-2046 - Performance improvements in mapping and conversion subsystem.
In MappingMongoConverter, we now avoid the creation of a ParameterValueProvider for parameter-less constructors. We also skip property population if entity can be constructed entirely through constructor creation. Replaced the lambda in MappingMongoConverter.readAndPopulateIdentifier(…) with direct call to ….readIdValue(…). Objectpath now uses decomposed ObjectPathItems to avoid array copying and creation. It now stores a reference to its parent and ObjectPathItem fields are now merged into ObjectPath, which reduces the number of created objects during reads.

Extended CachingMongoPersistentProperty with DBRef caching. Turned key access in DocumentAccessor into an optimistic lookup. DbRefResolverCallbacks are now created lazily.

Related tickets: DATACMNS-1366.
Original pull request: #602.
2018-08-15 16:11:46 +02:00
Mark Paluch
c58032cf37 DATAMONGO-2055 - Polishing.
Move test to UpdateMapperUnitTests.

Original pull request: #600.
2018-08-15 16:00:12 +02:00
Christoph Strobl
67c3f02dcc DATAMONGO-2055 - Allow position modifier to be negative using push at position on Update.
Original pull request: #600.
2018-08-15 15:53:43 +02:00
Mark Paluch
208bd6ae52 DATAMONGO-2050 - Polishing.
Tweak Javadoc.

Original pull request: #596.
2018-08-15 15:04:30 +02:00
Christoph Strobl
64419751c0 DATAMONGO-2050 - Polishing.
Move to AssertJ.

Original pull request: #596.
2018-08-15 15:04:28 +02:00
Christoph Strobl
cd089d4a54 DATAMONGO-2050 - Allow to specify the index to use for $geoNear aggregation operation.
Original pull request: #596.
2018-08-15 15:04:19 +02:00
Mark Paluch
e484337dcf DATAMONGO-2051 - Polishing.
Introduce MongoClientVersion.isMongo38Driver() for API parity between versions 2.0.x and 2.1.

Original pull request: #598.
2018-08-14 16:40:31 +02:00
Christoph Strobl
e4da45baed DATAMONGO-2051 - Add support for SCRAM-SHA-256 authentication mechanism to MongoCredentialPropertyEditor.
Original pull request: #598.
Related pull request: #597.
2018-08-14 16:26:12 +02:00
Mark Paluch
03246f04b8 DATAMONGO-2040 - Polishing.
Deprecate Index.Duplicates. Remove unused imports. Mention deprecation in What's new.

Original pull request: #599.
2018-08-14 16:04:36 +02:00
Christoph Strobl
50070dfc64 DATAMONGO-2040 - Deprecate Indexed.dropDups and CompoundIndex.dropDups.
Add deprecation warning and remove options no longer in use.

Original pull request: #599.
2018-08-14 16:04:03 +02:00
Mark Paluch
029d50e526 DATAMONGO-2049 - Polishing.
Add static import for assertThat(…).

Original pull request: #594.
2018-08-14 10:50:15 +02:00
Christoph Strobl
9764ce0147 DATAMONGO-2049 - Add support for $ltrim, $rtrim, and $trim.
Original pull request: #594.
2018-08-14 10:50:15 +02:00
Mark Paluch
c00f461d06 DATAMONGO-2048 - Polishing.
Javadoc tweaks.

Original pull request: #595.
2018-08-13 15:58:58 +02:00
Christoph Strobl
4205516446 DATAMONGO-2048 - Add support for MongoDB 4.0 $convert aggregation operator.
We now support the following type conversion aggregation operators:

* $convert
* $toBool
* $toDate
* $toDecimal
* $toDouble
* $toInt
* $toLong
* $toObjectId
* $toString

Original pull request: #595.
2018-08-13 15:58:58 +02:00
Mark Paluch
beced8184f DATAMONGO-2047 - Polishing.
Retain previous options when calling withTimezone(…)/onNull…(…). Add tests. Javadoc.

Original pull request: #593.
2018-08-13 13:25:44 +02:00
Christoph Strobl
64dc3dbb1d DATAMONGO-2047 - Update $dateToString and $dateFromString aggregation operators to match MongoDB 4.0 changes.
We added the format and onNull options to DateFromString and changed format to an optional parameter.

Original pull request: #593.
2018-08-13 13:25:33 +02:00
Mark Paluch
7b67ad4f6c DATAMONGO-2045 - Polishing.
Return false instead of null in isTransactionFailureCode(…)/isClientSessionFailureCode(…) to prevent null-dereference. Add initial size to HashMap instances with known number of elements. Fix typos in private constant names. Fix duplicate error code ids.

Original pull request: #592.
2018-08-13 10:30:06 +02:00
Christoph Strobl
c2373d05fe DATAMONGO-2045 - Add session & transaction specific error codes for exception translation.
Original pull request: #592.
2018-08-13 10:30:02 +02:00
Mark Paluch
da63788a52 DATAMONGO-2041 - Polishing.
Use getRequiredPersistentEntity() instead of getPersistentEntity() for improved null-safety. Use Lombok to for required args constructors. Slightly tweak Javadoc.

Original pull request: #591.
2018-08-13 10:10:22 +02:00
Christoph Strobl
016892085c DATAMONGO-2041 - Apply field restriction to DTO projections.
We now derive field projections for DTO projections if the field projection document is unrestricted.

Original pull request: #591.
2018-08-13 10:10:23 +02:00
Mark Paluch
4f9c0fa6b3 DATAMONGO-2043 - Polishing.
Slightly tweak Javadoc.

Original pull request: #589.
2018-08-08 11:01:30 +02:00
Christoph Strobl
e1393847be DATAMONGO-2043 - Omit type hint when mapping simple types.
Original pull request: #589.
2018-08-08 11:01:27 +02:00
Christoph Strobl
ff6f5d9ef3 DATAMONGO-2027 - Polishing.
Remove duplicate tests and fix assertions on existing ones. Move tests over to AssertJ and fix output database not applied correctly.

Original Pull Request: #588
2018-08-07 13:00:58 +02:00
Mark Paluch
d4f351a37c DATAMONGO-2027 - Consider MapReduce output type.
We now consider the output type (collection output) when rendering the MapReduce command. Previously, all output was returned inline without storing the results in the configured collection.

Original Pull Request: #588
2018-08-07 13:00:30 +02:00
Mark Paluch
67281916c2 DATAMONGO-2006 - Updated changelog. 2018-07-27 11:45:21 +02:00
Mark Paluch
f8b2781ec8 DATAMONGO-2007 - Updated changelog. 2018-07-26 16:23:57 +02:00
Mark Paluch
58116dfd63 DATAMONGO-1982 - After release cleanups. 2018-07-26 12:32:27 +02:00
Mark Paluch
5f2c411501 DATAMONGO-1982 - Prepare next development iteration. 2018-07-26 12:32:24 +02:00
Mark Paluch
ac84c7bf57 DATAMONGO-1982 - Release version 2.1 RC1 (Lovelace). 2018-07-26 12:06:34 +02:00
Mark Paluch
db2c05e8fc DATAMONGO-1982 - Prepare 2.1 RC1 (Lovelace). 2018-07-26 12:04:30 +02:00
Mark Paluch
5a735138fc DATAMONGO-1982 - Updated changelog. 2018-07-26 12:04:19 +02:00
Mark Paluch
7f28aaf60d DATAMONGO-2029 - Encode collections of UUID and byte array query method arguments to their binary form.
We now convert collections that only contain UUID or byte array items to a BSON list that contains the encoded form of these items. Previously, we only converted single UUID and byte arrays into $binary so lists rendered to e.g. $uuid which does not work for queries.

Encoding is now encapsulated in strategy objects that implement the encoding only for their type. This allows to break up the conditional flow and improve organization of responsibilities.
2018-07-25 15:05:51 +02:00
Mark Paluch
8b8eb3cfe5 DATAMONGO-2030 - Reinstantiate existsBy queries for reactive repositories.
We now support existsBy queries for reactive repositories to align with blocking repository support. ExistsBy support got lost during merging and is now back in place.

Extract boolean flag counting into BooleanUtil.
2018-07-23 16:28:51 +02:00
Mark Paluch
7f9352f9b8 DATAMONGO-2028 - Polishing.
Remove trailing whitespaces.
2018-07-17 10:03:00 +02:00
Mark Paluch
9d1471bb28 DATAMONGO-2028 - Reinstantiate Map-like document conversion on save.
We now convert values of Map-like documents (Document, DBObject, Map) before writing these into MongoDB. Conversion got lost as result of a refactoring and missing tests.
2018-07-17 10:03:00 +02:00
Christoph Strobl
088928c64a DATAMONGO-2011 - Relax type check when mapping collections.
Original pull request: #587.
2018-07-13 12:42:08 +02:00
Oliver Gierke
648bfdfc67 DATAMONGO-2026 - Polishing.
Slighly polished the initial identifier population and lookup in MappingMongoConverter.

Original pull request: #586.
2018-07-13 12:28:17 +02:00
Christoph Strobl
390b00d5fe DATAMONGO-2026 - Fix id property resolution for immutable objects.
We now make sure id properties used as persistence constructor arguments are no longer set via the property accessor, but during object instantiation. Previous to this change this caused an UnsupportedOperationException.

Original pull request: #586.
2018-07-13 12:28:15 +02:00
Oliver Gierke
98433250c8 DATAMONGO-1992 - Introduced MappingMongoEvent.mapSource(…).
We're currently using application events to allow users to pre-process both entities and documents persisted via our …Template classes. That approach actually exposes a conceptual mismatch as events should be immutable and the hardly can be if event listeners try to modify the entity instance or even exchange them (in case the entity itself is immutable).

We now introduce an intermediate, package protected MappingMongoEvent.mapSource(…) that allows to exchange the source of the event. This is now used by the refined auditing infrastructure as this now returns the manipulated entity as it supports immutable ones as well. This will be removed as soon as we've come up with an alternative callback API that doesn't suffer from these conceptual mismatches (currently scheduled for release train Moore).
2018-07-12 10:57:13 +02:00
Oliver Gierke
323b0a8479 DATAMONGO-1992 - Extract common entity operations API from (Reactive)MongoTemplate.
Introduced EntityOperations and MappedDocument to allow to share common operations from MongoTemplate and ReactiveMongoTemplate.
2018-07-12 10:57:13 +02:00
Mark Paluch
d1b1dfbae9 DATAMONGO-1992 - Disable test for storing Optional properties.
Reading java.util.Optional is now no longer possible as we do not allow modifying immutable (final) fields.
2018-07-12 10:57:13 +02:00
Mark Paluch
d1dea13c32 DATAMONGO-1992 - Add mutation support for immutable objects through MongoTemplate and SimpleMongoRepository.
Persisting methods of MongoTemplate and SimpleMongoRepository now return potentially new object instances of immutable objects. New instances are created using wither methods/Kotlin copy(…) methods if an immutable object requires association with an Id or the version number needs to be incremented.
2018-07-12 10:57:13 +02:00
Mark Paluch
ba2ab183ed DATAMONGO-1992 - Add mutation support for immutable objects through ReactiveMongoTemplate.
Persisting methods of ReactiveMongoTemplate now return potentially new object instances of immutable objects. New instances are created using wither methods/Kotlin copy(…) methods if an immutable object requires association with an Id or the version number needs to be incremented.
2018-07-12 10:57:13 +02:00
Mark Paluch
1eab66aff4 DATAMONGO-1992 - Add mutation support for immutable types.
We now return new instances that are potentially created by wither/Kotlin copy(…) methods when reading immutable properties.
2018-07-12 10:57:13 +02:00
Mark Paluch
8cc4ef3c3f DATAMONGO-1992 - Adapt existing tests to immutable object.
Turn immutable id properties to a mutable one to adapt with removed mutation support for final fields.
2018-07-12 10:57:13 +02:00
Christoph Strobl
1e49c95e41 DATAMONGO-1848 - Polishing.
Prefix types with Querydsl and update visibility to allow construction of custom queries using SpringDataMongodbQuery. Reintroduce generics for JoinBuilder usage, fix warnings and nullability issues. Also add BsonValue types to simple types and use native BsonRegularExpression for regex conversion.

Add tests for "in" on dbref, exception translation, any embedded, join and lifecycle events related to DATAMONGO-362, DATAMONGO-595, DATAMONGO-700, DATAMONGO-1434, DATAMONGO-1810 and DATAMONGO-2010.

Original Pull Request: #579
2018-07-11 13:13:05 +02:00
Mark Paluch
7d06f2b040 DATAMONGO-1848 - Use imported Querydsl support for Document API MongoDB.
Original Pull Request: #579
2018-07-11 13:12:06 +02:00
Mark Paluch
b7755e71f6 DATAMONGO-1848 - Import Document-based Querydsl support.
Original Pull Request: #579
2018-07-11 13:10:51 +02:00
Mark Paluch
0ec82e1f2e DATAMONGO-2021 - Polishing.
Adapt getResources(…) to use the file id and no longer the file name when opening a download stream. Add author tag. Add test to verify content retrieval by identity.

Original pull request: #581.
2018-07-06 13:12:46 +02:00
Niklas Helge Hanft
e18f506edd DATAMONGO-2021 - Use getObjectId() instead of getFilename() for opening the GridFS download stream.
Using the file name leads to duplicate resource streams as file names are not unique therefore we're using the file's ObjectId to lookup the file content.

Original pull request: #581.
2018-07-06 13:12:46 +02:00
Mark Paluch
46ed58b465 DATAMONGO-2016 - Polishing.
Fail gracefully if query string parameter has no value. Reformat test. Convert assertions to AssertJ.

Original pull request: #578.
2018-07-04 11:25:53 +02:00
Stephen Tyler Conrad
fb8084c9f7 DATAMONGO-2016 - Fix username/password extraction in MongoCredentialPropertyEditor.
MongoCredentialPropertyEditor inspects now the connection URI for the appropriate delimiter tokens. Previously, inspection used the char questionmark for username/password delimiter inspection.

Original pull request: #578.
2018-07-04 11:25:50 +02:00
Mark Paluch
5a0171203d DATAMONGO-2005 - Polishing.
Reformat code.

Original pull request: #574.
2018-07-04 09:28:46 +02:00
Christoph Strobl
c1d840d87d DATAMONGO-2005 - Use Flux.usingWhen for resource management in reactive transactions.
Original pull request: #574.
2018-07-04 09:28:15 +02:00
Christoph Strobl
ed1f2c7833 DATAMONGO-2004 - Polishing.
Make sure to place the LazyLoadingProxy early in the mapping process to avoid eager fetching of documents that might then get replaced by the LazyLoadingProxy.

Original Pull Request: #571
2018-07-03 14:21:23 +02:00
Mark Paluch
c545c855b9 DATAMONGO-2004 - Support lazy DBRef resolution through constructor creation of the enclosing entity.
We now respect eager/lazy loading preferences of the annotated association property when the enclosing entity is created through its constructor and the reference is passed as constructor argument.

Previously, we eagerly resolved DBRefs and passed the resolved value to the constructor.

Original Pull Request: #571
2018-07-03 14:09:38 +02:00
Christoph Strobl
1b7678a6af DATAMONGO-1919 - Polishing.
Remove unused imports, deprecated MongoClientVersion methods for drivers no longer supported and remove their usage throughout the codebase and partially revert changes in MongoSimpleTypes because all org.bson types have been included in the 3.8 release of org.mongodb.bson.

Original Pull Request: #572
2018-07-02 14:23:29 +02:00
Mark Paluch
0d06e141a3 DATAMONGO-1919 - Polishing.
Fix typo, use diamond syntax where possible.

Original Pull Request: #572
2018-07-02 14:17:54 +02:00
Mark Paluch
2d36fc3050 DATAMONGO-1919 - Allow reactive-only usage of ReactiveMongoTemplate.
We now support reactive-only usage of Spring Data MongoDB without the need to use the synchronous driver or even having it on the class path. We conditionally register CodeWScope/CodeWithScope types that are bundled with the particular driver distributions.

NoOpDbRefResolver is now a top-level type to be used with a reactive-only MongoMappingContext.

Original Pull Request: #572
2018-07-02 14:17:28 +02:00
Mark Paluch
78c2ab290d DATAMONGO-2012 - Polishing.
Simplify conditional flow. Replace AtomicReference construction in ChangeStreamEvent with AtomicReferenceFieldUpdater usage to reduce object allocations to streamline lazy body conversion usage. Tweak Javadoc and reference docs.

Original pull request: #576.
2018-07-02 09:59:11 +02:00
Christoph Strobl
88150eca54 DATAMONGO-2012 - Upgrade drivers to 3.8 (sync) and 1.9 (reactive).
We still stick to count for non session operations as countDocuments does not allow geo operators like $near in the filter query. For now we will wait to see if this is resolved within the driver.

Added options to watch an entire database and resume the changestream from a given point in time (UTC).

Original pull request: #576.
2018-07-02 09:58:47 +02:00
Christoph Strobl
30b86e7612 DATAMONGO-1311 - Polishing.
Update Javadoc and add reference documentation.
Alter @Meta batchSize default to zero, as negative values bear a special meaning.
Along the lines remove deprecated driver method usage and add deprecations for options about the be removed in subsequent MongoDB server releases.

Original Pull Request: #575
2018-06-29 10:28:33 +02:00
Mark Paluch
d3976f5199 DATAMONGO-1311 - Add configuration options for query batch size.
We now allow configuration of the find cursor/find publisher batch sizes using Query.cursorBatchSize(…).
Configuring the batch size gives users more fine grained control over the fetch behavior especially in reactive usage scenarios as the batch size defaults in FindPublisher to the remaining demand. This can cause several roundtrips in cases the remaining demand is small and the emitted elements are dropped rapidly (e.g. using filter(…)).

On the repository level @Meta allows now configuration of the cursor batch size for derived finder methods.

interface PersonRepository extends Repository<Person, Long> {

	@Meta(cursorBatchSize = 100)
	Stream<Person> findAllByLastname(String lastname);
}

Original Pull Request: #575
2018-06-29 10:25:06 +02:00
Christoph Strobl
f587e1f42a DATAMONGO-1827 - Add guard to tests for pre MongoDB 3.6.
We need a guard for pre MongoDB 3.6 versions using different error label.

Original Pull Request: #569
2018-06-27 15:54:48 +02:00
Christoph Strobl
bd5815dbcb DATAMONGO-1827 - Polishing.
Allow open/close projection on return type for findAndReplace.
Use default methods for delegation and remove collation from FindAndRemoveOption in favor of the collation set on the query itself.
Update Javadoc and reference documentation.

Original Pull Request: #569
2018-06-27 14:33:19 +02:00
Mark Paluch
ac89ce1b2c DATAMONGO-1827 - Polishing.
Use diamond syntax in imperative and reactive Template API implementations. Rename ReactiveMongoTemplate.toDbObject to toDocument. Move Terminating interfaces in ExecutableUpdateOperation and ExecutableRemoveOperation to the top-most position to align with other fluent interface declarations and to improve discoverability of terminating operations.

Convert ReactiveMongoTemplateTests assertions to AssertJ.

Original Pull Request: #569
2018-06-27 14:32:09 +02:00
Mark Paluch
fa880f1c5c DATAMONGO-1827 - Add support for findAndReplace.
We now support findAndReplace operations through the imperative and reactive Template API to find an object by a query and entirely replace it (except for the _id).

template.findAndReplace(query(where("name").is("Han")), new Person("Luke"))

template.update(Person.class).inCollection(STAR_WARS).matching(query(where("name").is("Han"))).replaceWith(luke).findAndReplace()

Original Pull Request: #569
2018-06-27 14:31:47 +02:00
Mark Paluch
56e61a2965 DATAMONGO-1969 - Updated changelog. 2018-06-13 21:39:49 +02:00
Mark Paluch
fcb8647c59 DATAMONGO-1967 - Updated changelog. 2018-06-13 15:01:56 +02:00
Mark Paluch
07e0e78aec DATAMONGO-2003 - Polishing.
Add nullability annotation to MongoParameterAccessor.getPoint(). Remove superfluous casts.

Convert MongoQueryCreatorUnitTests to user AssertJ assertions.

Original pull request: #570.
2018-06-11 14:18:07 +02:00
Christoph Strobl
0c0f47f76f DATAMONGO-2003 - Fix derived query using regex pattern with options.
We now consider regex pattern options when using the pattern as a derived finder argument.

Original pull request: #570.
2018-06-11 14:17:45 +02:00
Mark Paluch
18313db8fb DATAMONGO-2001 - Polishing.
Extract count aggregation pipeline setup to AggregationUtil. Fix count extraction if aggregation returns no results. Fix nullability of Query argument in ReactiveMongoTemplate.count(…). Improve synchronization of multi-threaded aggregation count test to prevent commit before all threads have issued a count query and to await thread completion.

Upgrade to MongoDB 4.0.0-rc4.

Original pull request: #568.
2018-06-11 11:06:23 +02:00
Christoph Strobl
05f325687c DATAMONGO-2001 - Count within transaction should return only the total count of documents visible to the specific session.
We now delegate count operations within an active transaction to an aggregation.

Once `MongoTemplate` detects an active transaction, all exposed `count()` methods are converted and delegated to the
aggregation framework using `$match` and `$count` operators, preserving `Query` settings, such as `collation`.

The following snippet of `count` inside the session bound closure

session.startTransaction();
template.withSession(session)
    .execute(action -> {
        action.count(query(where("state").is("active")), Step.class)
        ...

runs:

db.collection.aggregate(
   [
      { $match: { state: "active" } },
      { $count: "totalEntityCount" }
   ]
)

instead of:

db.collection.find( { state: "active" } ).count()

Original pull request: #568.
2018-06-11 10:52:59 +02:00
Oliver Gierke
8145b84dbe DATAMONGO-2002 - Fixed Criteria.equals(…) for usage with Pattern instances.
For Criteria instances that use regular expressions we now properly compare the two Pattern instances produced by also including the pattern flags in the comparison.
2018-06-07 19:16:27 +02:00
Mark Paluch
794b026f73 DATAMONGO-1979 - Polishing.
Rename QueryUtils method to decorateSort(…) to reflect the nature of the method. Add missing generics. Convert ReactiveMongoRepositoryTests to AssertJ. Add missing verifyComplete() steps to StepVerifier. Slight tweaks to Javadoc and reference docs.

Original pull request: #566.
2018-06-07 10:07:00 +02:00
Christoph Strobl
8f11916014 DATAMONGO-1979 - Use annotation cache throughout MongoQueryMethod.
Original pull request: #566.
2018-06-07 09:59:23 +02:00
Christoph Strobl
c5129aca45 DATAMONGO-1979 - Add default sorting for repository query methods using @Query(sort = "…").
We now allow to set a default sort for repository query methods via the @Query annotation.

	@Query(sort = "{ age : -1 }")
	List<Person> findByFirstname(String firstname);

Using an explicit Sort parameter along with the annotated one allows to alter the defaults set via the annotation. Method argument sort parameters add to / override the annotated defaults.

	@Query(sort = "{ age : -1 }")
	List<Person> findByFirstname(String firstname, Sort sort);

Original pull request: #566.
2018-06-07 09:58:40 +02:00
Mark Paluch
dfede781fb DATAMONGO-1998 - Polishing.
Switch id field name check to equals or to match the last property path segment.

Original pull request: #567.
2018-06-06 11:35:17 +02:00
Christoph Strobl
fe43ba470b DATAMONGO-1998 - Fix Querydsl id handling for nested property references using ObjectId hex String representation.
We now follow the conversion rules for id properties with a valid ObjectId representation when parsing Querydsl queries.

Original pull request: #567.
2018-06-06 11:35:17 +02:00
Mark Paluch
06622bed35 DATAMONGO-1986 - Polishing.
Refactor duplicated code into AggregationUtil.

Original pull request: #564.
2018-06-06 10:37:29 +02:00
Christoph Strobl
2bac54c70f DATAMONGO-1986 - Always provide a typed AggregationOperationContext for TypedAggregation.
We now initialize a TypeBasedAggregationOperationContext for TypedAggregations if no context is provided. This makes sure that potential Criteria objects are run trough the QueryMapper.
In case the default context is used we now also make sure to at least run the aggregation pipeline through the QueryMapper to avoid passing on non MongoDB simple types to the driver.

Original pull request: #564.
2018-06-06 10:37:29 +02:00
Mark Paluch
daae696c78 DATAMONGO-1988 - Polishing.
Match exactly for either top-level properties of leaf-properties instead of accepting the property/field name suffix.

Original pull request: #565.
2018-06-05 11:15:10 +02:00
Christoph Strobl
9f77aba8bb DATAMONGO-1988 - Fix query creation for id property references using ObjectId hex String representation.
We now follow the conversion rules for id properties with a valid ObjectId representation when creating queries. Prior to this change e.g. String values would have been turned into ObejctIds when saving a document, but not when querying the latter.

Original pull request: #565.
2018-06-05 11:12:28 +02:00
Oliver Gierke
2d9232ca04 DATAMONGO-1990 - Adapt build to changes in is-new-detection.
Removed a couple of Mockito stubbings that became unnecessary after the changes for DATACMNS-1333. Removed PersistableMongoEntityInformation as handling Persistable is now transparently taken care of by PersistentEntityInformation which MappingMongoEntityInformation extends.

Related tickets: DATACMNS-1333.
2018-06-01 13:29:27 +02:00
Christoph Strobl
e90c5bad2c DATAMONGO-1987 - Upgrade test infrastructure to MongoDB 4.0-rc0
Also make sure to use WriteConcern.MAJORITY when (re)creating collections in test setup.
2018-05-25 08:58:55 +02:00
Mark Paluch
bec79e6d7b DATAMONGO-1983 - Include transactions reference documentation page.
We now render the transaction support within our reference docs.
2018-05-17 11:15:29 +02:00
Christoph Strobl
619d344f57 DATAMONGO-1927 - After release cleanups. 2018-05-17 10:09:35 +02:00
Christoph Strobl
6f1b9d3fa4 DATAMONGO-1927 - Prepare next development iteration. 2018-05-17 10:09:34 +02:00
Christoph Strobl
24417c4c99 DATAMONGO-1927 - Release version 2.1 M3 (Lovelace). 2018-05-17 09:51:42 +02:00
Christoph Strobl
eec36b791a DATAMONGO-1927 - Prepare 2.1 M3 (Lovelace). 2018-05-17 09:50:48 +02:00
Christoph Strobl
512fa036bb DATAMONGO-1927 - Updated changelog. 2018-05-17 09:50:42 +02:00
Sébastien Deleuze
ea8df26eee DATAMONGO-1980 - Fix a typo in CriteriaExtensions.kt.
Original pull request: #563.
2018-05-16 09:43:53 +02:00
Oliver Gierke
43d821aab0 DATAMONGO-1874 - Polishing.
Cleanups in test case. Moved assertions to AssertJ.
2018-05-15 14:39:35 +02:00
Oliver Gierke
9bb8211ed7 DATAMONGO-1874 - @Document's collection attribute now supports EvaluationContextExtensions.
We now use the API newly introduced with DATACMNS-1260 to expose EvaluationContextExtensions to the SpEL evaluation in case the collection attribute of @Document uses SpEL.

Related tickets: DATACMNS-1260.
2018-05-15 14:39:35 +02:00
Victor
5a24e04226 DATAMONGO-1978 - Fix minor typo in Field.positionKey field name.
Original pull request: #558.
2018-05-15 12:30:22 +02:00
Mark Paluch
521d28ff3f DATAMONGO-1466 - Polishing.
Switch conditionals to Map-based Function registry to pick the appropriate converter. Fix typos in method names.

Original pull request: #561.
2018-05-15 11:27:04 +02:00
Christoph Strobl
e38e7d89f4 DATAMONGO-1466 - Polishing.
Just some minor code style improvements.

Original pull request: #561.
2018-05-15 11:27:04 +02:00
Christoph Strobl
fff69b9ed7 DATAMONGO-1466 - Add embedded typeinformation-based reading GeoJSON converter.
Original pull request: #561.
2018-05-15 11:27:04 +02:00
Oliver Gierke
e2e9e92563 DATAMONGO-1880 - Improve test execution in SessionBoundMongotTemplateTests.
We now also require a replica set for the test execution.
2018-05-15 10:39:36 +02:00
Oliver Gierke
3e040d283b DATAMONGO-1976 - Adapt to SpEL extension API changes in Spring Data Commons.
Related tickets: DATACMNS-1260.
2018-05-15 10:36:18 +02:00
Mark Paluch
a66b87118e DATAMONGO-1970 - Polishing.
ReactiveMongoOperations.withSession(…) no longer commits transactions if a transaction is active. ReactiveSessionScoped obtained through inTransaction() solely manages transactions and participates in ongoing transactions if a given ClientSession has already an active transaction. Remove ReactiveSessionScoped.executeSingle methods to align with ReactiveMongoOperations.

Add tests. Switch reactive tests to .as(StepVerifier:create) form. Extend documentation.

Original pull request: #560.
2018-05-14 13:18:15 +02:00
Christoph Strobl
f296a499e5 DATAMONGO-1970 - Add support for MongoDB 4.0 transactions (reactive).
We now support Mongo Transactions through the reactive Template API. However, there's no reactive repository transaction support yet.

Mono<DeleteResult> result = template.inTransaction()
                              .execute(action -> action.remove(query(where("id").is("step-1")), Step.class));

Original pull request: #560.
2018-05-14 13:16:22 +02:00
Mark Paluch
1acf00b039 DATAMONGO-1974 - Polishing.
Fix typos, links, and code fences.

Original pull request: #559.
2018-05-11 15:30:05 +02:00
Jay Bryant
e23c861a39 DATAMONGO-1974 - Full editing pass for Spring Data MongoDB.
Full editing pass of the Spring Data MongoDB reference guide. I also adjusted index.adoc to work with the changes I made to the build project, so that we get Epub and PDF as well as HTML.

Original pull request: #559.
2018-05-11 15:30:05 +02:00
Mark Paluch
85aef4836d DATAMONGO-1968 - Polishing.
Rename MongoDbFactoryBase to MongoDbFactorySupport. Add constructor to MongoTemplate accepting the new MongoClient type. Extend Javadoc. Switch tests to use the new MongoTemplate constructor.

Original pull request: #557.
2018-05-11 10:29:58 +02:00
Christoph Strobl
5470486d8d DATAMONGO-1968 - Add configuration support for com.mongodb.client.MongoClient.
We now accept MongoDB's new com.mongodb.client.MongoClient object to setup Spring Data MongoDB infrastructure through AbstractMongoClientConfiguration. The new MongoClient does not support DBObject anymore hence it cannot be used with Querydsl.

@Configuration
public class MongoClientConfiguration extends AbstractMongoClientConfiguration {

	@Override
	protected String getDatabaseName() {
		return "database";
	}

	@Override
	public MongoClient mongoClient() {
		return MongoClients.create("mongodb://localhost:27017/?replicaSet=rs0&w=majority");
	}
}

Original pull request: #557.
2018-05-11 10:27:30 +02:00
Mark Paluch
42c02c9b70 DATAMONGO-1971 - Polishing.
Remove outdated profiles.

Original pull request: #554.
2018-05-09 16:35:21 +02:00
Mark Paluch
ee9bca4856 DATAMONGO-1971 - Install MongoDB 3.7.9 on TravisCI.
We now download and unpack MongoDB directly instead of using TravisCI's outdated MongoDB version.

Original pull request: #554.
2018-05-09 16:35:18 +02:00
Mark Paluch
0d823df7f3 DATAMONGO-1920 - Polishing.
Slightly tweak method names. Document MongoDatabaseUtils usage in the context of MongoTransactionManager. Rename SessionSynchronization constants to align with AbstractPlatformTransactionManager. Slightly tweak Javadoc and reference docs for typos.

Original pull request: #554.
2018-05-09 16:31:58 +02:00
Christoph Strobl
4cd2935087 DATAMONGO-1920 - Add support for MongoDB 4.0 transactions (synchronous driver).
MongoTransactionManager is the gateway to the well known Spring transaction support. It allows applications to use managed transaction features of Spring.
The MongoTransactionManager binds a ClientSession to the thread. MongoTemplate automatically detects those and operates on them accordingly.

static class Config extends AbstractMongoConfiguration {

	// ...

	@Bean
	MongoTransactionManager transactionManager(MongoDbFactory dbFactory) {
		return new MongoTransactionManager(dbFactory);
	}
}

@Component
public class StateService {

	@Transactional
	void someBusinessFunction(Step step) {

		template.insert(step);

		process(step);

		template.update(Step.class).apply(update.set("state", // ...
	};
});

Original pull request: #554.
2018-05-09 16:31:06 +02:00
Mark Paluch
6fbb7cec22 DATAMONGO-1918 - Updated changelog. 2018-05-08 15:27:17 +02:00
Mark Paluch
44ea579b69 DATAMONGO-1917 - Updated changelog. 2018-05-08 12:22:51 +02:00
Mark Paluch
30af34f80a DATAMONGO-1943 - Polishing.
Reduce visibility. Use List interface instead of concrete type.

Original pull request: #556.
2018-05-07 16:20:41 +02:00
Christoph Strobl
247f30143b DATAMONGO-1943 - Fix ClassCastException caused by SpringDataMongodbSerializer.
We now convert List-typed predicates to List to BasicDBList to meet MongodbSerializer's expectations for top-level lists used for the $and operator.

Original pull request: #556.
2018-05-07 16:18:52 +02:00
Mark Paluch
364f266a3a DATAMONGO-1914 - Polishing.
Throw FileNotFoundException on inherited methods throwing IOException if resource is absent. Retain filename for absent resources to provide context through GridFsResource.getFilename(). Switch exists() to determine presence/absence based on GridFSFile presence. Extend tests.

Original pull request: #555.
2018-05-07 14:53:27 +02:00
Christoph Strobl
f92bd20384 DATAMONGO-1914 - Return an empty GridFsResource instead of null when resource does not exist.
Original pull request: #555.
2018-05-07 14:53:21 +02:00
Mark Paluch
304e1c607f DATAMONGO-1929 - Polishing.
Add missing Nullable annotations and missing diamond operators. Slightly reorder null guards.

Use Lombok to generate required constructors.

Original pull request: #551.
2018-04-23 10:34:37 +02:00
Christoph Strobl
449780573e DATAMONGO-1929 - Add Kotlin extensions for Executable and ReactiveMapReduceOperation.
Original pull request: #551.
2018-04-23 10:34:31 +02:00
Christoph Strobl
e424573f0d DATAMONGO-1929 - Add fluent mapReduce template API.
Original pull request: #551.
2018-04-23 10:34:10 +02:00
Christoph Strobl
31630c0dcc DATAMONGO-1928 - Polishing.
Use native driver operations to avoid potential unwanted template index interaction.

Original Pull Request: #550
2018-04-20 13:17:38 +02:00
Mark Paluch
7cdc3d00c1 DATAMONGO-1928 - Polishing.
Migrate test to AssertJ and as-style for StepVerifier.

Original Pull Request: #550
2018-04-20 13:02:00 +02:00
Mark Paluch
a9f5d7bd3d DATAMONGO-1928 - Use non-blocking index creation through ReactiveMongoTemplate.
We now use ReactiveIndexOperationsProvider to inspect and create indexes for MongoDB collections without using blocking methods. Indexes are created for initial entities and whenever a MongoPersistentEntity is registered in MongoMappingContext.

Index creation is now decoupled from the actual ReactiveMongoTemplate call causing indexes to be created asynchronously. Mongo commands no longer depend on the completion of index creation commands. Decoupling also comes with the aspect that ReactiveMongoTemplate creation/command invocation no longer fails if the actual index creation fails. Previous usage of blocking index creation caused the actual ReactiveMongoTemplate call to fail.

ReactiveMongoTemplate objects can be created with a Consumer<Throwable> callback that is notified if an index creation fails.

Original Pull Request: #550
2018-04-20 13:00:54 +02:00
Christoph Strobl
b5f18468db DATAMONGO-1808 - Polishing.
Move individual methods under BitwiseCriteriaOperators, update Javadoc and split tests.

Original Pull Request: #507
2018-04-18 13:15:05 +02:00
Andreas Zink
ef872d2527 DATAMONGO-1808 - Add support for bitwise query operators.
Add support for $bitsAllClear, $bitsAllSet, $bitsAnyClear and $bitsAnySet.

Original Pull Request: #507
2018-04-18 13:14:45 +02:00
Mark Paluch
c2516946e9 DATAMONGO-1890 - Polishing.
Remove mapReduce default methods in favor of adding variants through a fluent API at a later stage. Assert mapReduce arguments and remove subsequent null guards. Adapt tests.

Original pull request: #548.
2018-04-17 10:12:46 +02:00
Christoph Strobl
857add7349 DATAMONGO-1890 - Add support for mapReduce to ReactiveMongoOperations.
We now support mapReduce via ReactiveMongoTemplate returning a Flux<T> as the operations result.

Original pull request: #548.
2018-04-17 10:11:29 +02:00
Mark Paluch
2ec3f219c8 DATAMONGO-1869 - After release cleanups. 2018-04-13 15:08:33 +02:00
Mark Paluch
5c8701f79c DATAMONGO-1869 - Prepare next development iteration. 2018-04-13 15:08:32 +02:00
285 changed files with 21858 additions and 4718 deletions

View File

@@ -3,44 +3,33 @@ language: java
jdk: jdk:
- oraclejdk8 - oraclejdk8
before_script: before_install:
- mongod --version - mkdir -p downloads
- mkdir -p var/db var/log
- if [[ ! -d downloads/mongodb-linux-x86_64-ubuntu1604-${MONGO_VERSION} ]] ; then cd downloads && wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-${MONGO_VERSION}.tgz && tar xzf mongodb-linux-x86_64-ubuntu1604-${MONGO_VERSION}.tgz && cd ..; fi
- downloads/mongodb-linux-x86_64-ubuntu1604-${MONGO_VERSION}/bin/mongod --version
- downloads/mongodb-linux-x86_64-ubuntu1604-${MONGO_VERSION}/bin/mongod --dbpath var/db --replSet rs0 --fork --logpath var/log/mongod.log
- sleep 10
- |- - |-
echo "replication: downloads/mongodb-linux-x86_64-ubuntu1604-${MONGO_VERSION}/bin/mongo --eval "rs.initiate({_id: 'rs0', members:[{_id: 0, host: '127.0.0.1:27017'}]});"
replSetName: rs0" | sudo tee -a /etc/mongod.conf sleep 15
- sudo service mongod restart
- sleep 20
- |-
mongo --eval "rs.initiate({_id: 'rs0', members:[{_id: 0, host: '127.0.0.1:27017'}]});"
- sleep 15
services:
- mongodb
env: env:
matrix: matrix:
- PROFILE=ci - PROFILE=ci
- PROFILE=mongo36-next global:
- MONGO_VERSION=4.0.0
# Current MongoDB version is 2.4.2 as of 2016-04, see https://github.com/travis-ci/travis-ci/issues/3694
# apt-get starts a MongoDB instance so it's not started using before_script
addons: addons:
apt: apt:
sources:
- mongodb-3.4-precise
packages: packages:
- mongodb-org-server - oracle-java8-installer
- mongodb-org-shell
- oracle-java8-installer
sudo: false sudo: false
cache: cache:
directories: directories:
- $HOME/.m2 - $HOME/.m2
- downloads
install:
- |-
mongo admin --eval "db.adminCommand({setFeatureCompatibilityVersion: '3.4'});"
script: "mvn clean dependency:list test -P${PROFILE} -Dsort" script: "mvn clean dependency:list test -P${PROFILE} -Dsort"

View File

@@ -138,6 +138,42 @@ public class MyService {
} }
``` ```
### MongoDB 4.0 Transactions
As of version 4 MongoDB supports [Transactions](https://www.mongodb.com/transactions). Transactions are built on top of
`ClientSessions` and therefore require an active session.
`MongoTransactionManager` is the gateway to the well known Spring transaction support. It allows applications to use
[managed transaction features of Spring](http://docs.spring.io/spring/docs/current/spring-framework-reference/html/transaction.html).
The `MongoTransactionManager` binds a `ClientSession` to the thread. `MongoTemplate` automatically detects those and operates on them accordingly.
```java
@Configuration
static class Config extends AbstractMongoConfiguration {
@Bean
MongoTransactionManager transactionManager(MongoDbFactory dbFactory) {
return new MongoTransactionManager(dbFactory);
}
// ...
}
@Component
public class StateService {
@Transactional
void someBusinessFunction(Step step) {
template.insert(step);
process(step);
template.update(Step.class).apply(Update.set("state", // ...
};
});
```
## Contributing to Spring Data ## Contributing to Spring Data
Here are some ways for you to get involved in the community: Here are some ways for you to get involved in the community:

43
pom.xml
View File

@@ -5,7 +5,7 @@
<groupId>org.springframework.data</groupId> <groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId> <artifactId>spring-data-mongodb-parent</artifactId>
<version>2.1.0.M2</version> <version>2.2.0.M1</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<name>Spring Data MongoDB</name> <name>Spring Data MongoDB</name>
@@ -15,7 +15,7 @@
<parent> <parent>
<groupId>org.springframework.data.build</groupId> <groupId>org.springframework.data.build</groupId>
<artifactId>spring-data-parent</artifactId> <artifactId>spring-data-parent</artifactId>
<version>2.1.0.M2</version> <version>2.2.0.M1</version>
</parent> </parent>
<modules> <modules>
@@ -27,9 +27,9 @@
<properties> <properties>
<project.type>multi</project.type> <project.type>multi</project.type>
<dist.id>spring-data-mongodb</dist.id> <dist.id>spring-data-mongodb</dist.id>
<springdata.commons>2.1.0.M2</springdata.commons> <springdata.commons>2.2.0.M1</springdata.commons>
<mongo>3.6.3</mongo> <mongo>3.8.2</mongo>
<mongo.reactivestreams>1.7.1</mongo.reactivestreams> <mongo.reactivestreams>1.9.2</mongo.reactivestreams>
<jmh.version>1.19</jmh.version> <jmh.version>1.19</jmh.version>
</properties> </properties>
@@ -115,38 +115,6 @@
<profiles> <profiles>
<!-- not-yet available profile>
<id>mongo35-next</id>
<properties>
<mongo>3.5.1-SNAPSHOT</mongo>
</properties>
<repositories>
<repository>
<id>mongo-snapshots</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</repository>
</repositories>
</profile -->
<profile>
<id>mongo36-next</id>
<properties>
<mongo>3.6.0-SNAPSHOT</mongo>
</properties>
<repositories>
<repository>
<id>mongo-snapshots</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</repository>
</repositories>
</profile>
<profile> <profile>
<id>release</id> <id>release</id>
<build> <build>
@@ -169,7 +137,6 @@
<module>spring-data-mongodb-benchmarks</module> <module>spring-data-mongodb-benchmarks</module>
</modules> </modules>
</profile> </profile>
</profiles> </profiles>
<dependencies> <dependencies>

View File

@@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.springframework.data</groupId> <groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId> <artifactId>spring-data-mongodb-parent</artifactId>
<version>2.1.0.M2</version> <version>2.2.0.M1</version>
<relativePath>../pom.xml</relativePath> <relativePath>../pom.xml</relativePath>
</parent> </parent>

View File

@@ -6,7 +6,7 @@
<parent> <parent>
<groupId>org.springframework.data</groupId> <groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId> <artifactId>spring-data-mongodb-parent</artifactId>
<version>2.1.0.M2</version> <version>2.2.0.M1</version>
<relativePath>../pom.xml</relativePath> <relativePath>../pom.xml</relativePath>
</parent> </parent>
@@ -50,7 +50,7 @@
<dependency> <dependency>
<groupId>org.springframework.data</groupId> <groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId> <artifactId>spring-data-mongodb</artifactId>
<version>2.1.0.M2</version> <version>2.2.0.M1</version>
</dependency> </dependency>
<!-- reactive --> <!-- reactive -->

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<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"> <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> <modelVersion>4.0.0</modelVersion>
@@ -13,7 +14,7 @@
<parent> <parent>
<groupId>org.springframework.data</groupId> <groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId> <artifactId>spring-data-mongodb-parent</artifactId>
<version>2.1.0.M2</version> <version>2.2.0.M1</version>
<relativePath>../pom.xml</relativePath> <relativePath>../pom.xml</relativePath>
</parent> </parent>
@@ -35,8 +36,15 @@
<plugin> <plugin>
<groupId>org.asciidoctor</groupId> <groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId> <artifactId>asciidoctor-maven-plugin</artifactId>
<configuration>
<attributes>
<mongo-reactivestreams>${mongo.reactivestreams}</mongo-reactivestreams>
<reactor>${reactor}</reactor>
</attributes>
</configuration>
</plugin> </plugin>
</plugins> </plugins>
</build> </build>
</project> </project>

View File

@@ -11,7 +11,7 @@
<parent> <parent>
<groupId>org.springframework.data</groupId> <groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId> <artifactId>spring-data-mongodb-parent</artifactId>
<version>2.1.0.M2</version> <version>2.2.0.M1</version>
<relativePath>../pom.xml</relativePath> <relativePath>../pom.xml</relativePath>
</parent> </parent>
@@ -253,6 +253,13 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
<version>1.1</version>
<scope>test</scope>
</dependency>
<!-- Kotlin extension --> <!-- Kotlin extension -->
<dependency> <dependency>
<groupId>org.jetbrains.kotlin</groupId> <groupId>org.jetbrains.kotlin</groupId>

View File

@@ -0,0 +1,240 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb;
import org.springframework.lang.Nullable;
import org.springframework.transaction.support.ResourceHolderSynchronization;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.mongodb.ClientSessionOptions;
import com.mongodb.client.ClientSession;
import com.mongodb.client.MongoDatabase;
/**
* Helper class for managing a {@link MongoDatabase} instances via {@link MongoDbFactory}. Used for obtaining
* {@link ClientSession session bound} resources, such as {@link MongoDatabase} and
* {@link com.mongodb.client.MongoCollection} suitable for transactional usage.
* <p />
* <strong>Note:</strong> Intended for internal usage only.
*
* @author Christoph Strobl
* @author Mark Paluch
* @currentRead Shadow's Edge - Brent Weeks
* @since 2.1
*/
public class MongoDatabaseUtils {
/**
* Obtain the default {@link MongoDatabase database} form the given {@link MongoDbFactory factory} using
* {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}.
* <p />
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the current
* {@link Thread} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}.
*
* @param factory the {@link MongoDbFactory} to get the {@link MongoDatabase} from.
* @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}.
*/
public static MongoDatabase getDatabase(MongoDbFactory factory) {
return doGetMongoDatabase(null, factory, SessionSynchronization.ON_ACTUAL_TRANSACTION);
}
/**
* Obtain the default {@link MongoDatabase database} form the given {@link MongoDbFactory factory}.
* <p />
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the current
* {@link Thread} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}.
*
* @param factory the {@link MongoDbFactory} to get the {@link MongoDatabase} from.
* @param sessionSynchronization the synchronization to use. Must not be {@literal null}.
* @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}.
*/
public static MongoDatabase getDatabase(MongoDbFactory factory, SessionSynchronization sessionSynchronization) {
return doGetMongoDatabase(null, factory, sessionSynchronization);
}
/**
* Obtain the {@link MongoDatabase database} with given name form the given {@link MongoDbFactory factory} using
* {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}.
* <p />
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the current
* {@link Thread} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}.
*
* @param dbName the name of the {@link MongoDatabase} to get.
* @param factory the {@link MongoDbFactory} to get the {@link MongoDatabase} from.
* @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}.
*/
public static MongoDatabase getDatabase(String dbName, MongoDbFactory factory) {
return doGetMongoDatabase(dbName, factory, SessionSynchronization.ON_ACTUAL_TRANSACTION);
}
/**
* Obtain the {@link MongoDatabase database} with given name form the given {@link MongoDbFactory factory}.
* <p />
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the current
* {@link Thread} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}.
*
* @param dbName the name of the {@link MongoDatabase} to get.
* @param factory the {@link MongoDbFactory} to get the {@link MongoDatabase} from.
* @param sessionSynchronization the synchronization to use. Must not be {@literal null}.
* @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}.
*/
public static MongoDatabase getDatabase(String dbName, MongoDbFactory factory,
SessionSynchronization sessionSynchronization) {
return doGetMongoDatabase(dbName, factory, sessionSynchronization);
}
private static MongoDatabase doGetMongoDatabase(@Nullable String dbName, MongoDbFactory factory,
SessionSynchronization sessionSynchronization) {
Assert.notNull(factory, "Factory must not be null!");
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
return StringUtils.hasText(dbName) ? factory.getDb(dbName) : factory.getDb();
}
ClientSession session = doGetSession(factory, sessionSynchronization);
if (session == null) {
return StringUtils.hasText(dbName) ? factory.getDb(dbName) : factory.getDb();
}
MongoDbFactory factoryToUse = factory.withSession(session);
return StringUtils.hasText(dbName) ? factoryToUse.getDb(dbName) : factoryToUse.getDb();
}
/**
* Check if the {@link MongoDbFactory} is actually bound to a {@link ClientSession} that has an active transaction, or
* if a {@link TransactionSynchronization} has been registered for the {@link MongoDbFactory resource} and if the
* associated {@link ClientSession} has an {@link ClientSession#hasActiveTransaction() active transaction}.
*
* @param dbFactory the resource to check transactions for. Must not be {@literal null}.
* @return {@literal true} if the factory has an ongoing transaction.
* @since 2.1.3
*/
public static boolean isTransactionActive(MongoDbFactory dbFactory) {
if (dbFactory.isTransactionActive()) {
return true;
}
MongoResourceHolder resourceHolder = (MongoResourceHolder) TransactionSynchronizationManager.getResource(dbFactory);
return resourceHolder != null && resourceHolder.hasActiveTransaction();
}
@Nullable
private static ClientSession doGetSession(MongoDbFactory dbFactory, SessionSynchronization sessionSynchronization) {
MongoResourceHolder resourceHolder = (MongoResourceHolder) TransactionSynchronizationManager.getResource(dbFactory);
// check for native MongoDB transaction
if (resourceHolder != null && (resourceHolder.hasSession() || resourceHolder.isSynchronizedWithTransaction())) {
if (!resourceHolder.hasSession()) {
resourceHolder.setSession(createClientSession(dbFactory));
}
return resourceHolder.getSession();
}
if (SessionSynchronization.ON_ACTUAL_TRANSACTION.equals(sessionSynchronization)) {
return null;
}
// init a non native MongoDB transaction by registering a MongoSessionSynchronization
resourceHolder = new MongoResourceHolder(createClientSession(dbFactory), dbFactory);
resourceHolder.getRequiredSession().startTransaction();
TransactionSynchronizationManager
.registerSynchronization(new MongoSessionSynchronization(resourceHolder, dbFactory));
resourceHolder.setSynchronizedWithTransaction(true);
TransactionSynchronizationManager.bindResource(dbFactory, resourceHolder);
return resourceHolder.getSession();
}
private static ClientSession createClientSession(MongoDbFactory dbFactory) {
return dbFactory.getSession(ClientSessionOptions.builder().causallyConsistent(true).build());
}
/**
* MongoDB specific {@link ResourceHolderSynchronization} for resource cleanup at the end of a transaction when
* participating in a non-native MongoDB transaction, such as a Jta or JDBC transaction.
*
* @author Christoph Strobl
* @since 2.1
*/
private static class MongoSessionSynchronization extends ResourceHolderSynchronization<MongoResourceHolder, Object> {
private final MongoResourceHolder resourceHolder;
MongoSessionSynchronization(MongoResourceHolder resourceHolder, MongoDbFactory dbFactory) {
super(resourceHolder, dbFactory);
this.resourceHolder = resourceHolder;
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.support.ResourceHolderSynchronization#shouldReleaseBeforeCompletion()
*/
@Override
protected boolean shouldReleaseBeforeCompletion() {
return false;
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.support.ResourceHolderSynchronization#processResourceAfterCommit(java.lang.Object)
*/
@Override
protected void processResourceAfterCommit(MongoResourceHolder resourceHolder) {
if (resourceHolder.hasActiveTransaction()) {
resourceHolder.getRequiredSession().commitTransaction();
}
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.support.ResourceHolderSynchronization#afterCompletion(int)
*/
@Override
public void afterCompletion(int status) {
if (status == TransactionSynchronization.STATUS_ROLLED_BACK && this.resourceHolder.hasActiveTransaction()) {
resourceHolder.getRequiredSession().abortTransaction();
}
super.afterCompletion(status);
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.support.ResourceHolderSynchronization#releaseResource(java.lang.Object, java.lang.Object)
*/
@Override
protected void releaseResource(MongoResourceHolder resourceHolder, Object resourceKey) {
if (resourceHolder.hasActiveSession()) {
resourceHolder.getRequiredSession().close();
}
}
}
}

View File

@@ -22,8 +22,8 @@ import org.springframework.data.mongodb.core.MongoExceptionTranslator;
import com.mongodb.ClientSessionOptions; import com.mongodb.ClientSessionOptions;
import com.mongodb.DB; import com.mongodb.DB;
import com.mongodb.client.ClientSession;
import com.mongodb.client.MongoDatabase; import com.mongodb.client.MongoDatabase;
import com.mongodb.session.ClientSession;
/** /**
* Interface for factories creating {@link MongoDatabase} instances. * Interface for factories creating {@link MongoDatabase} instances.
@@ -32,7 +32,7 @@ import com.mongodb.session.ClientSession;
* @author Thomas Darimont * @author Thomas Darimont
* @author Christoph Strobl * @author Christoph Strobl
*/ */
public interface MongoDbFactory extends CodecRegistryProvider { public interface MongoDbFactory extends CodecRegistryProvider, MongoSessionProvider {
/** /**
* Creates a default {@link MongoDatabase} instance. * Creates a default {@link MongoDatabase} instance.
@@ -58,6 +58,14 @@ public interface MongoDbFactory extends CodecRegistryProvider {
*/ */
PersistenceExceptionTranslator getExceptionTranslator(); PersistenceExceptionTranslator getExceptionTranslator();
/**
* Get the legacy database entry point. Please consider {@link #getDb()} instead.
*
* @return
* @deprecated since 2.1, use {@link #getDb()}. This method will be removed with a future version as it works only
* with the legacy MongoDB driver.
*/
@Deprecated
DB getLegacyDb(); DB getLegacyDb();
/** /**
@@ -100,4 +108,15 @@ public interface MongoDbFactory extends CodecRegistryProvider {
* @since 2.1 * @since 2.1
*/ */
MongoDbFactory withSession(ClientSession session); MongoDbFactory withSession(ClientSession session);
/**
* Returns if the given {@link MongoDbFactory} is bound to a {@link ClientSession} that has an
* {@link ClientSession#hasActiveTransaction() active transaction}.
*
* @return {@literal true} if there's an active transaction, {@literal false} otherwise.
* @since 2.1.3
*/
default boolean isTransactionActive() {
return false;
}
} }

View File

@@ -0,0 +1,153 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb;
import org.springframework.lang.Nullable;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.ResourceHolderSupport;
import com.mongodb.client.ClientSession;
/**
* MongoDB specific {@link ResourceHolderSupport resource holder}, wrapping a {@link ClientSession}.
* {@link MongoTransactionManager} binds instances of this class to the thread.
* <p />
* <strong>Note:</strong> Intended for internal usage only.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.1
* @see MongoTransactionManager
* @see org.springframework.data.mongodb.core.MongoTemplate
*/
class MongoResourceHolder extends ResourceHolderSupport {
private @Nullable ClientSession session;
private MongoDbFactory dbFactory;
/**
* Create a new {@link MongoResourceHolder} for a given {@link ClientSession session}.
*
* @param session the associated {@link ClientSession}. Can be {@literal null}.
* @param dbFactory the associated {@link MongoDbFactory}. must not be {@literal null}.
*/
MongoResourceHolder(@Nullable ClientSession session, MongoDbFactory dbFactory) {
this.session = session;
this.dbFactory = dbFactory;
}
/**
* @return the associated {@link ClientSession}. Can be {@literal null}.
*/
@Nullable
ClientSession getSession() {
return session;
}
/**
* @return the required associated {@link ClientSession}.
* @throws IllegalStateException if no {@link ClientSession} is associated with this {@link MongoResourceHolder}.
* @since 2.1.3
*/
ClientSession getRequiredSession() {
ClientSession session = getSession();
if (session == null) {
throw new IllegalStateException("No session available!");
}
return session;
}
/**
* @return the associated {@link MongoDbFactory}.
*/
public MongoDbFactory getDbFactory() {
return dbFactory;
}
/**
* Set the {@link ClientSession} to guard.
*
* @param session can be {@literal null}.
*/
public void setSession(@Nullable ClientSession session) {
this.session = session;
}
/**
* Only set the timeout if it does not match the {@link TransactionDefinition#TIMEOUT_DEFAULT default timeout}.
*
* @param seconds
*/
void setTimeoutIfNotDefaulted(int seconds) {
if (seconds != TransactionDefinition.TIMEOUT_DEFAULT) {
setTimeoutInSeconds(seconds);
}
}
/**
* @return {@literal true} if session is not {@literal null}.
*/
boolean hasSession() {
return session != null;
}
/**
* @return {@literal true} if the session is active and has not been closed.
*/
boolean hasActiveSession() {
if (!hasSession()) {
return false;
}
return hasServerSession() && !getRequiredSession().getServerSession().isClosed();
}
/**
* @return {@literal true} if the session has an active transaction.
* @since 2.1.3
* @see #hasActiveSession()
*/
boolean hasActiveTransaction() {
if (!hasActiveSession()) {
return false;
}
return getRequiredSession().hasActiveTransaction();
}
/**
* @return {@literal true} if the {@link ClientSession} has a {@link com.mongodb.session.ServerSession} associated
* that is accessible via {@link ClientSession#getServerSession()}.
*/
boolean hasServerSession() {
try {
return getRequiredSession().getServerSession() != null;
} catch (IllegalStateException serverSessionClosed) {
// ignore
}
return false;
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb;
import com.mongodb.ClientSessionOptions;
import com.mongodb.client.ClientSession;
/**
* A simple interface for obtaining a {@link ClientSession} to be consumed by
* {@link org.springframework.data.mongodb.core.MongoOperations} and MongoDB native operations that support causal
* consistency and transactions.
*
* @author Christoph Strobl
* @currentRead Shadow's Edge - Brent Weeks
* @since 2.1
*/
@FunctionalInterface
public interface MongoSessionProvider {
/**
* Obtain a {@link ClientSession} with with given options.
*
* @param options must not be {@literal null}.
* @return never {@literal null}.
* @throws org.springframework.dao.DataAccessException
*/
ClientSession getSession(ClientSessionOptions options);
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb;
import org.springframework.lang.Nullable;
/**
* A specific {@link ClientSessionException} related to issues with a transaction such as aborted or non existing
* transactions.
*
* @author Christoph Strobl
* @since 2.1
*/
public class MongoTransactionException extends ClientSessionException {
/**
* Constructor for {@link MongoTransactionException}.
*
* @param msg the detail message. Must not be {@literal null}.
*/
public MongoTransactionException(String msg) {
super(msg);
}
/**
* Constructor for {@link ClientSessionException}.
*
* @param msg the detail message. Can be {@literal null}.
* @param cause the root cause. Can be {@literal null}.
*/
public MongoTransactionException(@Nullable String msg, @Nullable Throwable cause) {
super(msg, cause);
}
}

View File

@@ -0,0 +1,526 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.lang.Nullable;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionSystemException;
import org.springframework.transaction.support.AbstractPlatformTransactionManager;
import org.springframework.transaction.support.DefaultTransactionStatus;
import org.springframework.transaction.support.ResourceTransactionManager;
import org.springframework.transaction.support.SmartTransactionObject;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.transaction.support.TransactionSynchronizationUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import com.mongodb.ClientSessionOptions;
import com.mongodb.MongoException;
import com.mongodb.TransactionOptions;
import com.mongodb.client.ClientSession;
/**
* A {@link org.springframework.transaction.PlatformTransactionManager} implementation that manages
* {@link ClientSession} based transactions for a single {@link MongoDbFactory}.
* <p />
* Binds a {@link ClientSession} from the specified {@link MongoDbFactory} to the thread.
* <p />
* {@link TransactionDefinition#isReadOnly() Readonly} transactions operate on a {@link ClientSession} and enable causal
* consistency, and also {@link ClientSession#startTransaction() start}, {@link ClientSession#commitTransaction()
* commit} or {@link ClientSession#abortTransaction() abort} a transaction.
* <p />
* Application code is required to retrieve the {@link com.mongodb.client.MongoDatabase} via
* {@link MongoDatabaseUtils#getDatabase(MongoDbFactory)} instead of a standard {@link MongoDbFactory#getDb()} call.
* Spring classes such as {@link org.springframework.data.mongodb.core.MongoTemplate} use this strategy implicitly.
* <p />
* By default failure of a {@literal commit} operation raises a {@link TransactionSystemException}. One may override
* {@link #doCommit(MongoTransactionObject)} to implement the
* <a href="https://docs.mongodb.com/manual/core/transactions/#retry-commit-operation">Retry Commit Operation</a>
* behavior as outlined in the MongoDB reference manual.
*
* @author Christoph Strobl
* @author Mark Paluch
* @currentRead Shadow's Edge - Brent Weeks
* @since 2.1
* @see <a href="https://www.mongodb.com/transactions">MongoDB Transaction Documentation</a>
* @see MongoDatabaseUtils#getDatabase(MongoDbFactory, SessionSynchronization)
*/
public class MongoTransactionManager extends AbstractPlatformTransactionManager
implements ResourceTransactionManager, InitializingBean {
private @Nullable MongoDbFactory dbFactory;
private @Nullable TransactionOptions options;
/**
* Create a new {@link MongoTransactionManager} for bean-style usage.
* <p />
* <strong>Note:</strong>The {@link MongoDbFactory db factory} has to be {@link #setDbFactory(MongoDbFactory) set}
* before using the instance. Use this constructor to prepare a {@link MongoTransactionManager} via a
* {@link org.springframework.beans.factory.BeanFactory}.
* <p />
* Optionally it is possible to set default {@link TransactionOptions transaction options} defining
* {@link com.mongodb.ReadConcern} and {@link com.mongodb.WriteConcern}.
*
* @see #setDbFactory(MongoDbFactory)
* @see #setTransactionSynchronization(int)
*/
public MongoTransactionManager() {}
/**
* Create a new {@link MongoTransactionManager} obtaining sessions from the given {@link MongoDbFactory}.
*
* @param dbFactory must not be {@literal null}.
*/
public MongoTransactionManager(MongoDbFactory dbFactory) {
this(dbFactory, null);
}
/**
* Create a new {@link MongoTransactionManager} obtaining sessions from the given {@link MongoDbFactory} applying the
* given {@link TransactionOptions options}, if present, when starting a new transaction.
*
* @param dbFactory must not be {@literal null}.
* @param options can be {@literal null}.
*/
public MongoTransactionManager(MongoDbFactory dbFactory, @Nullable TransactionOptions options) {
Assert.notNull(dbFactory, "DbFactory must not be null!");
this.dbFactory = dbFactory;
this.options = options;
}
/*
* (non-Javadoc)
* org.springframework.transaction.support.AbstractPlatformTransactionManager#doGetTransaction()
*/
@Override
protected Object doGetTransaction() throws TransactionException {
MongoResourceHolder resourceHolder = (MongoResourceHolder) TransactionSynchronizationManager
.getResource(getRequiredDbFactory());
return new MongoTransactionObject(resourceHolder);
}
/*
* (non-Javadoc)
* org.springframework.transaction.support.AbstractPlatformTransactionManager#isExistingTransaction(java.lang.Object)
*/
@Override
protected boolean isExistingTransaction(Object transaction) throws TransactionException {
return extractMongoTransaction(transaction).hasResourceHolder();
}
/*
* (non-Javadoc)
* org.springframework.transaction.support.AbstractPlatformTransactionManager#doBegin(java.lang.Object, org.springframework.transaction.TransactionDefinition)
*/
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException {
MongoTransactionObject mongoTransactionObject = extractMongoTransaction(transaction);
MongoResourceHolder resourceHolder = newResourceHolder(definition,
ClientSessionOptions.builder().causallyConsistent(true).build());
mongoTransactionObject.setResourceHolder(resourceHolder);
if (logger.isDebugEnabled()) {
logger
.debug(String.format("About to start transaction for session %s.", debugString(resourceHolder.getSession())));
}
try {
mongoTransactionObject.startTransaction(options);
} catch (MongoException ex) {
throw new TransactionSystemException(String.format("Could not start Mongo transaction for session %s.",
debugString(mongoTransactionObject.getSession())), ex);
}
if (logger.isDebugEnabled()) {
logger.debug(String.format("Started transaction for session %s.", debugString(resourceHolder.getSession())));
}
resourceHolder.setSynchronizedWithTransaction(true);
TransactionSynchronizationManager.bindResource(getRequiredDbFactory(), resourceHolder);
}
/*
* (non-Javadoc)
* org.springframework.transaction.support.AbstractPlatformTransactionManager#doSuspend(java.lang.Object)
*/
@Override
protected Object doSuspend(Object transaction) throws TransactionException {
MongoTransactionObject mongoTransactionObject = extractMongoTransaction(transaction);
mongoTransactionObject.setResourceHolder(null);
return TransactionSynchronizationManager.unbindResource(getRequiredDbFactory());
}
/*
* (non-Javadoc)
* org.springframework.transaction.support.AbstractPlatformTransactionManager#doResume(java.lang.Object, java.lang.Object)
*/
@Override
protected void doResume(@Nullable Object transaction, Object suspendedResources) {
TransactionSynchronizationManager.bindResource(getRequiredDbFactory(), suspendedResources);
}
/*
* (non-Javadoc)
* org.springframework.transaction.support.AbstractPlatformTransactionManager#doCommit(org.springframework.transaction.support.DefaultTransactionStatus)
*/
@Override
protected final void doCommit(DefaultTransactionStatus status) throws TransactionException {
MongoTransactionObject mongoTransactionObject = extractMongoTransaction(status);
if (logger.isDebugEnabled()) {
logger.debug(String.format("About to commit transaction for session %s.",
debugString(mongoTransactionObject.getSession())));
}
try {
doCommit(mongoTransactionObject);
} catch (Exception ex) {
throw new TransactionSystemException(String.format("Could not commit Mongo transaction for session %s.",
debugString(mongoTransactionObject.getSession())), ex);
}
}
/**
* Customization hook to perform an actual commit of the given transaction.<br />
* If a commit operation encounters an error, the MongoDB driver throws a {@link MongoException} holding
* {@literal error labels}. <br />
* By default those labels are ignored, nevertheless one might check for
* {@link MongoException#UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL transient commit errors labels} and retry the the
* commit. <br />
* <code>
* <pre>
* int retries = 3;
* do {
* try {
* transactionObject.commitTransaction();
* break;
* } catch (MongoException ex) {
* if (!ex.hasErrorLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)) {
* throw ex;
* }
* }
* Thread.sleep(500);
* } while (--retries > 0);
* </pre>
* </code>
*
* @param transactionObject never {@literal null}.
* @throws Exception in case of transaction errors.
*/
protected void doCommit(MongoTransactionObject transactionObject) throws Exception {
transactionObject.commitTransaction();
}
/*
* (non-Javadoc)
* org.springframework.transaction.support.AbstractPlatformTransactionManager#doRollback(org.springframework.transaction.support.DefaultTransactionStatus)
*/
@Override
protected void doRollback(DefaultTransactionStatus status) throws TransactionException {
MongoTransactionObject mongoTransactionObject = extractMongoTransaction(status);
if (logger.isDebugEnabled()) {
logger.debug(String.format("About to abort transaction for session %s.",
debugString(mongoTransactionObject.getSession())));
}
try {
mongoTransactionObject.abortTransaction();
} catch (MongoException ex) {
throw new TransactionSystemException(String.format("Could not abort Mongo transaction for session %s.",
debugString(mongoTransactionObject.getSession())), ex);
}
}
/*
* (non-Javadoc)
* org.springframework.transaction.support.AbstractPlatformTransactionManager#doSetRollbackOnly(org.springframework.transaction.support.DefaultTransactionStatus)
*/
@Override
protected void doSetRollbackOnly(DefaultTransactionStatus status) throws TransactionException {
MongoTransactionObject transactionObject = extractMongoTransaction(status);
transactionObject.getRequiredResourceHolder().setRollbackOnly();
}
/*
* (non-Javadoc)
* org.springframework.transaction.support.AbstractPlatformTransactionManager#doCleanupAfterCompletion(java.lang.Object)
*/
@Override
protected void doCleanupAfterCompletion(Object transaction) {
Assert.isInstanceOf(MongoTransactionObject.class, transaction,
() -> String.format("Expected to find a %s but it turned out to be %s.", MongoTransactionObject.class,
transaction.getClass()));
MongoTransactionObject mongoTransactionObject = (MongoTransactionObject) transaction;
// Remove the connection holder from the thread.
TransactionSynchronizationManager.unbindResource(getRequiredDbFactory());
mongoTransactionObject.getRequiredResourceHolder().clear();
if (logger.isDebugEnabled()) {
logger.debug(String.format("About to release Session %s after transaction.",
debugString(mongoTransactionObject.getSession())));
}
mongoTransactionObject.closeSession();
}
/**
* Set the {@link MongoDbFactory} that this instance should manage transactions for.
*
* @param dbFactory must not be {@literal null}.
*/
public void setDbFactory(MongoDbFactory dbFactory) {
Assert.notNull(dbFactory, "DbFactory must not be null!");
this.dbFactory = dbFactory;
}
/**
* Set the {@link TransactionOptions} to be applied when starting transactions.
*
* @param options can be {@literal null}.
*/
public void setOptions(@Nullable TransactionOptions options) {
this.options = options;
}
/**
* Get the {@link MongoDbFactory} that this instance manages transactions for.
*
* @return can be {@literal null}.
*/
@Nullable
public MongoDbFactory getDbFactory() {
return dbFactory;
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.support.ResourceTransactionManager#getResourceFactory()
*/
@Override
public MongoDbFactory getResourceFactory() {
return getRequiredDbFactory();
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() {
getRequiredDbFactory();
}
private MongoResourceHolder newResourceHolder(TransactionDefinition definition, ClientSessionOptions options) {
MongoDbFactory dbFactory = getResourceFactory();
MongoResourceHolder resourceHolder = new MongoResourceHolder(dbFactory.getSession(options), dbFactory);
resourceHolder.setTimeoutIfNotDefaulted(determineTimeout(definition));
return resourceHolder;
}
/**
* @throws IllegalStateException if {@link #dbFactory} is {@literal null}.
*/
private MongoDbFactory getRequiredDbFactory() {
Assert.state(dbFactory != null,
"MongoTransactionManager operates upon a MongoDbFactory. Did you forget to provide one? It's required.");
return dbFactory;
}
private static MongoTransactionObject extractMongoTransaction(Object transaction) {
Assert.isInstanceOf(MongoTransactionObject.class, transaction,
() -> String.format("Expected to find a %s but it turned out to be %s.", MongoTransactionObject.class,
transaction.getClass()));
return (MongoTransactionObject) transaction;
}
private static MongoTransactionObject extractMongoTransaction(DefaultTransactionStatus status) {
Assert.isInstanceOf(MongoTransactionObject.class, status.getTransaction(),
() -> String.format("Expected to find a %s but it turned out to be %s.", MongoTransactionObject.class,
status.getTransaction().getClass()));
return (MongoTransactionObject) status.getTransaction();
}
private static String debugString(@Nullable ClientSession session) {
if (session == null) {
return "null";
}
String debugString = String.format("[%s@%s ", ClassUtils.getShortName(session.getClass()),
Integer.toHexString(session.hashCode()));
try {
if (session.getServerSession() != null) {
debugString += String.format("id = %s, ", session.getServerSession().getIdentifier());
debugString += String.format("causallyConsistent = %s, ", session.isCausallyConsistent());
debugString += String.format("txActive = %s, ", session.hasActiveTransaction());
debugString += String.format("txNumber = %d, ", session.getServerSession().getTransactionNumber());
debugString += String.format("closed = %d, ", session.getServerSession().isClosed());
debugString += String.format("clusterTime = %s", session.getClusterTime());
} else {
debugString += "id = n/a";
debugString += String.format("causallyConsistent = %s, ", session.isCausallyConsistent());
debugString += String.format("txActive = %s, ", session.hasActiveTransaction());
debugString += String.format("clusterTime = %s", session.getClusterTime());
}
} catch (RuntimeException e) {
debugString += String.format("error = %s", e.getMessage());
}
debugString += "]";
return debugString;
}
/**
* MongoDB specific transaction object, representing a {@link MongoResourceHolder}. Used as transaction object by
* {@link MongoTransactionManager}.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.1
* @see MongoResourceHolder
*/
protected static class MongoTransactionObject implements SmartTransactionObject {
private @Nullable MongoResourceHolder resourceHolder;
MongoTransactionObject(@Nullable MongoResourceHolder resourceHolder) {
this.resourceHolder = resourceHolder;
}
/**
* Set the {@link MongoResourceHolder}.
*
* @param resourceHolder can be {@literal null}.
*/
void setResourceHolder(@Nullable MongoResourceHolder resourceHolder) {
this.resourceHolder = resourceHolder;
}
/**
* @return {@literal true} if a {@link MongoResourceHolder} is set.
*/
final boolean hasResourceHolder() {
return resourceHolder != null;
}
/**
* Start a MongoDB transaction optionally given {@link TransactionOptions}.
*
* @param options can be {@literal null}
*/
void startTransaction(@Nullable TransactionOptions options) {
ClientSession session = getRequiredSession();
if (options != null) {
session.startTransaction(options);
} else {
session.startTransaction();
}
}
/**
* Commit the transaction.
*/
public void commitTransaction() {
getRequiredSession().commitTransaction();
}
/**
* Rollback (abort) the transaction.
*/
public void abortTransaction() {
getRequiredSession().abortTransaction();
}
/**
* Close a {@link ClientSession} without regard to its transactional state.
*/
void closeSession() {
ClientSession session = getRequiredSession();
if (session.getServerSession() != null && !session.getServerSession().isClosed()) {
session.close();
}
}
@Nullable
public ClientSession getSession() {
return resourceHolder != null ? resourceHolder.getSession() : null;
}
private MongoResourceHolder getRequiredResourceHolder() {
Assert.state(resourceHolder != null, "MongoResourceHolder is required but not present. o_O");
return resourceHolder;
}
private ClientSession getRequiredSession() {
ClientSession session = getSession();
Assert.state(session != null, "A Session is required but it turned out to be null.");
return session;
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.support.SmartTransactionObject#isRollbackOnly()
*/
@Override
public boolean isRollbackOnly() {
return this.resourceHolder != null && this.resourceHolder.isRollbackOnly();
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.support.SmartTransactionObject#flush()
*/
@Override
public void flush() {
TransactionSynchronizationUtils.triggerFlush();
}
}
}

View File

@@ -24,8 +24,8 @@ import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.mongodb.core.MongoExceptionTranslator; import org.springframework.data.mongodb.core.MongoExceptionTranslator;
import com.mongodb.ClientSessionOptions; import com.mongodb.ClientSessionOptions;
import com.mongodb.reactivestreams.client.ClientSession;
import com.mongodb.reactivestreams.client.MongoDatabase; import com.mongodb.reactivestreams.client.MongoDatabase;
import com.mongodb.session.ClientSession;
/** /**
* Interface for factories creating reactive {@link MongoDatabase} instances. * Interface for factories creating reactive {@link MongoDatabase} instances.

View File

@@ -57,6 +57,7 @@ public class SessionAwareMethodInterceptor<D, C> implements MethodInterceptor {
private final Class<?> targetType; private final Class<?> targetType;
private final Class<?> collectionType; private final Class<?> collectionType;
private final Class<?> databaseType; private final Class<?> databaseType;
private final Class<? extends ClientSession> sessionType;
/** /**
* Create a new SessionAwareMethodInterceptor for given target. * Create a new SessionAwareMethodInterceptor for given target.
@@ -71,12 +72,13 @@ public class SessionAwareMethodInterceptor<D, C> implements MethodInterceptor {
* {@code MongoCollection}. * {@code MongoCollection}.
* @param <T> target object type. * @param <T> target object type.
*/ */
public <T> SessionAwareMethodInterceptor(ClientSession session, T target, Class<D> databaseType, public <T> SessionAwareMethodInterceptor(ClientSession session, T target, Class<? extends ClientSession> sessionType,
ClientSessionOperator<D> databaseDecorator, Class<C> collectionType, Class<D> databaseType, ClientSessionOperator<D> databaseDecorator, Class<C> collectionType,
ClientSessionOperator<C> collectionDecorator) { ClientSessionOperator<C> collectionDecorator) {
Assert.notNull(session, "ClientSession must not be null!"); Assert.notNull(session, "ClientSession must not be null!");
Assert.notNull(target, "Target must not be null!"); Assert.notNull(target, "Target must not be null!");
Assert.notNull(sessionType, "SessionType must not be null!");
Assert.notNull(databaseType, "Database type must not be null!"); Assert.notNull(databaseType, "Database type must not be null!");
Assert.notNull(databaseDecorator, "Database ClientSessionOperator must not be null!"); Assert.notNull(databaseDecorator, "Database ClientSessionOperator must not be null!");
Assert.notNull(collectionType, "Collection type must not be null!"); Assert.notNull(collectionType, "Collection type must not be null!");
@@ -90,6 +92,7 @@ public class SessionAwareMethodInterceptor<D, C> implements MethodInterceptor {
this.databaseDecorator = databaseDecorator; this.databaseDecorator = databaseDecorator;
this.targetType = ClassUtils.isAssignable(databaseType, target.getClass()) ? databaseType : collectionType; this.targetType = ClassUtils.isAssignable(databaseType, target.getClass()) ? databaseType : collectionType;
this.sessionType = sessionType;
} }
/* /*
@@ -114,7 +117,7 @@ public class SessionAwareMethodInterceptor<D, C> implements MethodInterceptor {
return methodInvocation.proceed(); return methodInvocation.proceed();
} }
Optional<Method> targetMethod = METHOD_CACHE.lookup(methodInvocation.getMethod(), targetType); Optional<Method> targetMethod = METHOD_CACHE.lookup(methodInvocation.getMethod(), targetType, sessionType);
return !targetMethod.isPresent() ? methodInvocation.proceed() return !targetMethod.isPresent() ? methodInvocation.proceed()
: ReflectionUtils.invokeMethod(targetMethod.get(), target, : ReflectionUtils.invokeMethod(targetMethod.get(), target,
@@ -171,18 +174,19 @@ public class SessionAwareMethodInterceptor<D, C> implements MethodInterceptor {
* @param targetClass * @param targetClass
* @return * @return
*/ */
Optional<Method> lookup(Method method, Class<?> targetClass) { Optional<Method> lookup(Method method, Class<?> targetClass, Class<? extends ClientSession> sessionType) {
return cache.computeIfAbsent(new MethodClassKey(method, targetClass), return cache.computeIfAbsent(new MethodClassKey(method, targetClass),
val -> Optional.ofNullable(findTargetWithSession(method, targetClass))); val -> Optional.ofNullable(findTargetWithSession(method, targetClass, sessionType)));
} }
@Nullable @Nullable
private Method findTargetWithSession(Method sourceMethod, Class<?> targetType) { private Method findTargetWithSession(Method sourceMethod, Class<?> targetType,
Class<? extends ClientSession> sessionType) {
Class<?>[] argTypes = sourceMethod.getParameterTypes(); Class<?>[] argTypes = sourceMethod.getParameterTypes();
Class<?>[] args = new Class<?>[argTypes.length + 1]; Class<?>[] args = new Class<?>[argTypes.length + 1];
args[0] = ClientSession.class; args[0] = sessionType;
System.arraycopy(argTypes, 0, args, 1, argTypes.length); System.arraycopy(argTypes, 0, args, 1, argTypes.length);
return ReflectionUtils.findMethod(targetType, sourceMethod.getName(), args); return ReflectionUtils.findMethod(targetType, sourceMethod.getName(), args);

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb;
/**
* {@link SessionSynchronization} is used along with {@link org.springframework.data.mongodb.core.MongoTemplate} to
* define in which type of transactions to participate if any.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.1
*/
public enum SessionSynchronization {
/**
* Synchronize with any transaction even with empty transactions and initiate a MongoDB transaction when doing so by
* registering a MongoDB specific {@link org.springframework.transaction.support.ResourceHolderSynchronization}.
*/
ALWAYS,
/**
* Synchronize with native MongoDB transactions initiated via {@link MongoTransactionManager}.
*/
ON_ACTUAL_TRANSACTION;
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoClientDbFactory;
import org.springframework.data.mongodb.core.SimpleMongoDbFactory;
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.Document;
import org.springframework.lang.Nullable;
import com.mongodb.client.MongoClient;
/**
* Base class for Spring Data MongoDB configuration using JavaConfig with {@link com.mongodb.client.MongoClient}.
*
* @author Christoph Strobl
* @since 2.1
* @see MongoConfigurationSupport
* @see AbstractMongoConfiguration
*/
@Configuration
public abstract class AbstractMongoClientConfiguration extends MongoConfigurationSupport {
/**
* Return the {@link MongoClient} instance to connect to. Annotate with {@link Bean} in case you want to expose a
* {@link MongoClient} instance to the {@link org.springframework.context.ApplicationContext}.
*
* @return
*/
public abstract MongoClient mongoClient();
/**
* Creates a {@link MongoTemplate}.
*
* @return
*/
@Bean
public MongoTemplate mongoTemplate() throws Exception {
return new MongoTemplate(mongoDbFactory(), mappingMongoConverter());
}
/**
* Creates a {@link SimpleMongoDbFactory} to be used by the {@link MongoTemplate}. Will use the {@link MongoClient}
* instance configured in {@link #mongoClient()}.
*
* @see #mongoClient()
* @see #mongoTemplate()
* @return
*/
@Bean
public MongoDbFactory mongoDbFactory() {
return new SimpleMongoClientDbFactory(mongoClient(), getDatabaseName());
}
/**
* Return the base package to scan for mapped {@link Document}s. Will return the package name of the configuration
* class' (the concrete class, not this one here) by default. So if you have a {@code com.acme.AppConfig} extending
* {@link AbstractMongoClientConfiguration} the base package will be considered {@code com.acme} unless the method is
* overridden to implement alternate behavior.
*
* @return the base package to scan for mapped {@link Document} classes or {@literal null} to not enable scanning for
* entities.
* @deprecated use {@link #getMappingBasePackages()} instead.
*/
@Deprecated
@Nullable
protected String getMappingBasePackage() {
Package mappingBasePackage = getClass().getPackage();
return mappingBasePackage == null ? null : mappingBasePackage.getName();
}
/**
* Creates a {@link MappingMongoConverter} using the configured {@link #mongoDbFactory()} and
* {@link #mongoMappingContext()}. Will get {@link #customConversions()} applied.
*
* @see #customConversions()
* @see #mongoMappingContext()
* @see #mongoDbFactory()
* @return
* @throws Exception
*/
@Bean
public MappingMongoConverter mappingMongoConverter() throws Exception {
DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory());
MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mongoMappingContext());
converter.setCustomConversions(customConversions());
return converter;
}
}

View File

@@ -29,7 +29,10 @@ import org.springframework.lang.Nullable;
import com.mongodb.MongoClient; import com.mongodb.MongoClient;
/** /**
* Base class for Spring Data MongoDB configuration using JavaConfig. * Base class for Spring Data MongoDB configuration using JavaConfig with {@link com.mongodb.MongoClient}.
* <p />
* <strong>INFO:</strong>In case you want to use {@link com.mongodb.client.MongoClients} for configuration please refer
* to {@link AbstractMongoClientConfiguration}.
* *
* @author Mark Pollack * @author Mark Pollack
* @author Oliver Gierke * @author Oliver Gierke
@@ -38,10 +41,10 @@ import com.mongodb.MongoClient;
* @author Christoph Strobl * @author Christoph Strobl
* @author Mark Paluch * @author Mark Paluch
* @see MongoConfigurationSupport * @see MongoConfigurationSupport
* @see AbstractMongoClientConfiguration
*/ */
@Configuration @Configuration
public abstract class public abstract class AbstractMongoConfiguration extends MongoConfigurationSupport {
AbstractMongoConfiguration extends MongoConfigurationSupport {
/** /**
* Return the {@link MongoClient} instance to connect to. Annotate with {@link Bean} in case you want to expose a * Return the {@link MongoClient} instance to connect to. Annotate with {@link Bean} in case you want to expose a
@@ -63,7 +66,7 @@ AbstractMongoConfiguration extends MongoConfigurationSupport {
/** /**
* Creates a {@link SimpleMongoDbFactory} to be used by the {@link MongoTemplate}. Will use the {@link MongoClient} * Creates a {@link SimpleMongoDbFactory} to be used by the {@link MongoTemplate}. Will use the {@link MongoClient}
* instance configured in {@link #mongo()}. * instance configured in {@link #mongoClient()}.
* *
* @see #mongoClient() * @see #mongoClient()
* @see #mongoTemplate() * @see #mongoTemplate()
@@ -111,4 +114,5 @@ AbstractMongoConfiguration extends MongoConfigurationSupport {
return converter; return converter;
} }
} }

View File

@@ -22,6 +22,7 @@ import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate; import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.data.mongodb.core.SimpleReactiveMongoDatabaseFactory; import org.springframework.data.mongodb.core.SimpleReactiveMongoDatabaseFactory;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
import com.mongodb.reactivestreams.client.MongoClient; import com.mongodb.reactivestreams.client.MongoClient;
@@ -80,8 +81,7 @@ public abstract class AbstractReactiveMongoConfiguration extends MongoConfigurat
@Bean @Bean
public MappingMongoConverter mappingMongoConverter() throws Exception { public MappingMongoConverter mappingMongoConverter() throws Exception {
MappingMongoConverter converter = new MappingMongoConverter(ReactiveMongoTemplate.NO_OP_REF_RESOLVER, MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mongoMappingContext());
mongoMappingContext());
converter.setCustomConversions(customConversions()); converter.setCustomConversions(customConversions());
return converter; return converter;

View File

@@ -60,6 +60,7 @@ import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCre
import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.event.ValidatingMongoEventListener; import org.springframework.data.mongodb.core.mapping.event.ValidatingMongoEventListener;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@@ -75,6 +76,7 @@ import org.w3c.dom.Element;
* @author Thomas Darimont * @author Thomas Darimont
* @author Christoph Strobl * @author Christoph Strobl
* @author Mark Paluch * @author Mark Paluch
* @author Zied Yaich
*/ */
public class MappingMongoConverterParser implements BeanDefinitionParser { public class MappingMongoConverterParser implements BeanDefinitionParser {
@@ -159,6 +161,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
return null; return null;
} }
@Nullable
private BeanDefinition potentiallyCreateValidatingMongoEventListener(Element element, ParserContext parserContext) { private BeanDefinition potentiallyCreateValidatingMongoEventListener(Element element, ParserContext parserContext) {
String disableValidation = element.getAttribute("disable-validation"); String disableValidation = element.getAttribute("disable-validation");
@@ -180,6 +183,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
return null; return null;
} }
@Nullable
private RuntimeBeanReference getValidator(Object source, ParserContext parserContext) { private RuntimeBeanReference getValidator(Object source, ParserContext parserContext) {
if (!JSR_303_PRESENT) { if (!JSR_303_PRESENT) {
@@ -197,7 +201,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
} }
public static String potentiallyCreateMappingContext(Element element, ParserContext parserContext, public static String potentiallyCreateMappingContext(Element element, ParserContext parserContext,
BeanDefinition conversionsDefinition, String converterId) { @Nullable BeanDefinition conversionsDefinition, @Nullable String converterId) {
String ctxRef = element.getAttribute("mapping-context-ref"); String ctxRef = element.getAttribute("mapping-context-ref");
@@ -211,7 +215,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
BeanDefinitionBuilder mappingContextBuilder = BeanDefinitionBuilder BeanDefinitionBuilder mappingContextBuilder = BeanDefinitionBuilder
.genericBeanDefinition(MongoMappingContext.class); .genericBeanDefinition(MongoMappingContext.class);
Set<String> classesToAdd = getInititalEntityClasses(element); Set<String> classesToAdd = getInitialEntityClasses(element);
if (classesToAdd != null) { if (classesToAdd != null) {
mappingContextBuilder.addPropertyValue("initialEntitySet", classesToAdd); mappingContextBuilder.addPropertyValue("initialEntitySet", classesToAdd);
@@ -262,6 +266,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
} }
} }
@Nullable
private BeanDefinition getCustomConversions(Element element, ParserContext parserContext) { private BeanDefinition getCustomConversions(Element element, ParserContext parserContext) {
List<Element> customConvertersElements = DomUtils.getChildElementsByTagName(element, "custom-converters"); List<Element> customConvertersElements = DomUtils.getChildElementsByTagName(element, "custom-converters");
@@ -269,7 +274,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
if (customConvertersElements.size() == 1) { if (customConvertersElements.size() == 1) {
Element customerConvertersElement = customConvertersElements.get(0); Element customerConvertersElement = customConvertersElements.get(0);
ManagedList<BeanMetadataElement> converterBeans = new ManagedList<BeanMetadataElement>(); ManagedList<BeanMetadataElement> converterBeans = new ManagedList<>();
List<Element> converterElements = DomUtils.getChildElementsByTagName(customerConvertersElement, "converter"); List<Element> converterElements = DomUtils.getChildElementsByTagName(customerConvertersElement, "converter");
if (converterElements != null) { if (converterElements != null) {
@@ -285,9 +290,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
provider.addExcludeFilter(new NegatingFilter(new AssignableTypeFilter(Converter.class), provider.addExcludeFilter(new NegatingFilter(new AssignableTypeFilter(Converter.class),
new AssignableTypeFilter(GenericConverter.class))); new AssignableTypeFilter(GenericConverter.class)));
for (BeanDefinition candidate : provider.findCandidateComponents(packageToScan)) { converterBeans.addAll(provider.findCandidateComponents(packageToScan));
converterBeans.add(candidate);
}
} }
BeanDefinitionBuilder conversionsBuilder = BeanDefinitionBuilder.rootBeanDefinition(MongoCustomConversions.class); BeanDefinitionBuilder conversionsBuilder = BeanDefinitionBuilder.rootBeanDefinition(MongoCustomConversions.class);
@@ -304,7 +307,8 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
return null; return null;
} }
private static Set<String> getInititalEntityClasses(Element element) { @Nullable
private static Set<String> getInitialEntityClasses(Element element) {
String basePackage = element.getAttribute(BASE_PACKAGE); String basePackage = element.getAttribute(BASE_PACKAGE);
@@ -317,7 +321,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
componentProvider.addIncludeFilter(new AnnotationTypeFilter(Document.class)); componentProvider.addIncludeFilter(new AnnotationTypeFilter(Document.class));
componentProvider.addIncludeFilter(new AnnotationTypeFilter(Persistent.class)); componentProvider.addIncludeFilter(new AnnotationTypeFilter(Persistent.class));
Set<String> classes = new ManagedSet<String>(); Set<String> classes = new ManagedSet<>();
for (BeanDefinition candidate : componentProvider.findCandidateComponents(basePackage)) { for (BeanDefinition candidate : componentProvider.findCandidateComponents(basePackage)) {
classes.add(candidate.getBeanClassName()); classes.add(candidate.getBeanClassName());
} }
@@ -325,6 +329,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
return classes; return classes;
} }
@Nullable
public BeanMetadataElement parseConverter(Element element, ParserContext parserContext) { public BeanMetadataElement parseConverter(Element element, ParserContext parserContext) {
String converterRef = element.getAttribute("ref"); String converterRef = element.getAttribute("ref");
@@ -375,7 +380,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
Assert.notNull(filters, "TypeFilters must not be null"); Assert.notNull(filters, "TypeFilters must not be null");
this.delegates = new HashSet<TypeFilter>(Arrays.asList(filters)); this.delegates = new HashSet<>(Arrays.asList(filters));
} }
/* /*

View File

@@ -35,6 +35,8 @@ import com.mongodb.MongoCredential;
* *
* @author Christoph Strobl * @author Christoph Strobl
* @author Oliver Gierke * @author Oliver Gierke
* @author Stephen Tyler Conrad
* @author Mark Paluch
* @since 1.7 * @since 1.7
*/ */
public class MongoCredentialPropertyEditor extends PropertyEditorSupport { public class MongoCredentialPropertyEditor extends PropertyEditorSupport {
@@ -98,6 +100,12 @@ public class MongoCredentialPropertyEditor extends PropertyEditorSupport {
verifyDatabasePresent(database); verifyDatabasePresent(database);
credentials.add(MongoCredential.createScramSha1Credential(userNameAndPassword[0], database, credentials.add(MongoCredential.createScramSha1Credential(userNameAndPassword[0], database,
userNameAndPassword[1].toCharArray())); userNameAndPassword[1].toCharArray()));
} else if (MongoCredential.SCRAM_SHA_256_MECHANISM.equals(authMechanism)) {
verifyUsernameAndPasswordPresent(userNameAndPassword);
verifyDatabasePresent(database);
credentials.add(MongoCredential.createScramSha256Credential(userNameAndPassword[0], database,
userNameAndPassword[1].toCharArray()));
} else { } else {
throw new IllegalArgumentException( throw new IllegalArgumentException(
String.format("Cannot create MongoCredentials for unknown auth mechanism '%s'!", authMechanism)); String.format("Cannot create MongoCredentials for unknown auth mechanism '%s'!", authMechanism));
@@ -164,7 +172,7 @@ public class MongoCredentialPropertyEditor extends PropertyEditorSupport {
private static Properties extractOptions(String text) { private static Properties extractOptions(String text) {
int optionsSeparationIndex = text.lastIndexOf(OPTIONS_DELIMITER); int optionsSeparationIndex = text.lastIndexOf(OPTIONS_DELIMITER);
int dbSeparationIndex = text.lastIndexOf(OPTIONS_DELIMITER); int dbSeparationIndex = text.lastIndexOf(DATABASE_DELIMITER);
if (optionsSeparationIndex == -1 || dbSeparationIndex > optionsSeparationIndex) { if (optionsSeparationIndex == -1 || dbSeparationIndex > optionsSeparationIndex) {
return new Properties(); return new Properties();
@@ -173,7 +181,13 @@ public class MongoCredentialPropertyEditor extends PropertyEditorSupport {
Properties properties = new Properties(); Properties properties = new Properties();
for (String option : text.substring(optionsSeparationIndex + 1).split(OPTION_VALUE_DELIMITER)) { for (String option : text.substring(optionsSeparationIndex + 1).split(OPTION_VALUE_DELIMITER)) {
String[] optionArgs = option.split("="); String[] optionArgs = option.split("=");
if (optionArgs.length == 1) {
throw new IllegalArgumentException(String.format("Query parameter '%s' has no value!", optionArgs[0]));
}
properties.put(optionArgs[0], optionArgs[1]); properties.put(optionArgs[0], optionArgs[1]);
} }

View File

@@ -0,0 +1,170 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import lombok.AllArgsConstructor;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.bson.Document;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext;
import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
import org.springframework.data.mongodb.core.aggregation.CountOperation;
import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext;
import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.query.CriteriaDefinition;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* Utility methods to map {@link org.springframework.data.mongodb.core.aggregation.Aggregation} pipeline definitions and
* create type-bound {@link AggregationOperationContext}.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.1
*/
@AllArgsConstructor
class AggregationUtil {
QueryMapper queryMapper;
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
/**
* Prepare the {@link AggregationOperationContext} for a given aggregation by either returning the context itself it
* is not {@literal null}, create a {@link TypeBasedAggregationOperationContext} if the aggregation contains type
* information (is a {@link TypedAggregation}) or use the {@link Aggregation#DEFAULT_CONTEXT}.
*
* @param aggregation must not be {@literal null}.
* @param context can be {@literal null}.
* @return the root {@link AggregationOperationContext} to use.
*/
AggregationOperationContext prepareAggregationContext(Aggregation aggregation,
@Nullable AggregationOperationContext context) {
if (context != null) {
return context;
}
if (aggregation instanceof TypedAggregation) {
return new TypeBasedAggregationOperationContext(((TypedAggregation) aggregation).getInputType(), mappingContext,
queryMapper);
}
return Aggregation.DEFAULT_CONTEXT;
}
/**
* Extract and map the aggregation pipeline into a {@link List} of {@link Document}.
*
* @param aggregation
* @param context
* @return
*/
List<Document> createPipeline(Aggregation aggregation, AggregationOperationContext context) {
if (!ObjectUtils.nullSafeEquals(context, Aggregation.DEFAULT_CONTEXT)) {
return aggregation.toPipeline(context);
}
return mapAggregationPipeline(aggregation.toPipeline(context));
}
/**
* Extract the command and map the aggregation pipeline.
*
* @param aggregation
* @param context
* @return
*/
Document createCommand(String collection, Aggregation aggregation, AggregationOperationContext context) {
Document command = aggregation.toDocument(collection, context);
if (!ObjectUtils.nullSafeEquals(context, Aggregation.DEFAULT_CONTEXT)) {
return command;
}
command.put("pipeline", mapAggregationPipeline(command.get("pipeline", List.class)));
return command;
}
/**
* Create a {@code $count} aggregation for {@link Query} and optionally a {@link Class entity class}.
*
* @param query must not be {@literal null}.
* @param entityClass can be {@literal null} if the {@link Query} object is empty.
* @return the {@link Aggregation} pipeline definition to run a {@code $count} aggregation.
*/
Aggregation createCountAggregation(Query query, @Nullable Class<?> entityClass) {
List<AggregationOperation> pipeline = computeCountAggregationPipeline(query, entityClass);
Aggregation aggregation = entityClass != null ? Aggregation.newAggregation(entityClass, pipeline)
: Aggregation.newAggregation(pipeline);
aggregation.withOptions(AggregationOptions.builder().collation(query.getCollation().orElse(null)).build());
return aggregation;
}
private List<AggregationOperation> computeCountAggregationPipeline(Query query, @Nullable Class<?> entityType) {
CountOperation count = Aggregation.count().as("totalEntityCount");
if (query.getQueryObject().isEmpty()) {
return Collections.singletonList(count);
}
Assert.notNull(entityType, "Entity type must not be null!");
Document mappedQuery = queryMapper.getMappedObject(query.getQueryObject(),
mappingContext.getPersistentEntity(entityType));
CriteriaDefinition criteria = new CriteriaDefinition() {
@Override
public Document getCriteriaObject() {
return mappedQuery;
}
@Nullable
@Override
public String getKey() {
return null;
}
};
return Arrays.asList(Aggregation.match(criteria), count);
}
private List<Document> mapAggregationPipeline(List<Document> pipeline) {
return pipeline.stream().map(val -> queryMapper.getMappedObject(val, Optional.empty()))
.collect(Collectors.toList());
}
}

View File

@@ -17,8 +17,11 @@ package org.springframework.data.mongodb.core;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import java.util.concurrent.atomic.AtomicReference; import java.time.Instant;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import org.bson.BsonTimestamp;
import org.bson.BsonValue;
import org.bson.Document; import org.bson.Document;
import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.messaging.Message; import org.springframework.data.mongodb.core.messaging.Message;
@@ -26,6 +29,7 @@ import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import com.mongodb.client.model.changestream.ChangeStreamDocument; import com.mongodb.client.model.changestream.ChangeStreamDocument;
import com.mongodb.client.model.changestream.OperationType;
/** /**
* {@link Message} implementation specific to MongoDB <a href="https://docs.mongodb.com/manual/changeStreams/">Change * {@link Message} implementation specific to MongoDB <a href="https://docs.mongodb.com/manual/changeStreams/">Change
@@ -38,11 +42,17 @@ import com.mongodb.client.model.changestream.ChangeStreamDocument;
@EqualsAndHashCode @EqualsAndHashCode
public class ChangeStreamEvent<T> { public class ChangeStreamEvent<T> {
@SuppressWarnings("rawtypes") //
private static final AtomicReferenceFieldUpdater<ChangeStreamEvent, Object> CONVERTED_UPDATER = AtomicReferenceFieldUpdater
.newUpdater(ChangeStreamEvent.class, Object.class, "converted");
private final @Nullable ChangeStreamDocument<Document> raw; private final @Nullable ChangeStreamDocument<Document> raw;
private final Class<T> targetType; private final Class<T> targetType;
private final MongoConverter converter; private final MongoConverter converter;
private final AtomicReference<T> converted = new AtomicReference<>();
// accessed through CONVERTED_UPDATER.
private volatile @Nullable T converted;
/** /**
* @param raw can be {@literal null}. * @param raw can be {@literal null}.
@@ -67,6 +77,69 @@ public class ChangeStreamEvent<T> {
return raw; return raw;
} }
/**
* Get the {@link ChangeStreamDocument#getClusterTime() cluster time} as {@link Instant} the event was emitted at.
*
* @return can be {@literal null}.
*/
@Nullable
public Instant getTimestamp() {
return getBsonTimestamp() != null ? converter.getConversionService().convert(raw.getClusterTime(), Instant.class)
: null;
}
/**
* Get the {@link ChangeStreamDocument#getClusterTime() cluster time}.
*
* @return can be {@literal null}.
* @since 2.2
*/
@Nullable
public BsonTimestamp getBsonTimestamp() {
return raw != null ? raw.getClusterTime() : null;
}
/**
* Get the {@link ChangeStreamDocument#getResumeToken() resume token} for this event.
*
* @return can be {@literal null}.
*/
@Nullable
public BsonValue getResumeToken() {
return raw != null ? raw.getResumeToken() : null;
}
/**
* Get the {@link ChangeStreamDocument#getOperationType() operation type} for this event.
*
* @return can be {@literal null}.
*/
@Nullable
public OperationType getOperationType() {
return raw != null ? raw.getOperationType() : null;
}
/**
* Get the database name the event was originated at.
*
* @return can be {@literal null}.
*/
@Nullable
public String getDatabaseName() {
return raw != null ? raw.getNamespace().getDatabaseName() : null;
}
/**
* Get the collection name the event was originated at.
*
* @return can be {@literal null}.
*/
@Nullable
public String getCollectionName() {
return raw != null ? raw.getNamespace().getCollectionName() : null;
}
/** /**
* Get the potentially converted {@link ChangeStreamDocument#getFullDocument()}. * Get the potentially converted {@link ChangeStreamDocument#getFullDocument()}.
* *
@@ -80,36 +153,48 @@ public class ChangeStreamEvent<T> {
return null; return null;
} }
if (raw.getFullDocument() == null) { Document fullDocument = raw.getFullDocument();
return targetType.cast(raw.getFullDocument());
if (fullDocument == null) {
return targetType.cast(fullDocument);
} }
return getConverted(); return getConverted(fullDocument);
} }
private T getConverted() { @SuppressWarnings("unchecked")
private T getConverted(Document fullDocument) {
return (T) doGetConverted(fullDocument);
}
private Object doGetConverted(Document fullDocument) {
Object result = CONVERTED_UPDATER.get(this);
T result = converted.get();
if (result != null) { if (result != null) {
return result; return result;
} }
if (ClassUtils.isAssignable(Document.class, raw.getFullDocument().getClass())) { if (ClassUtils.isAssignable(Document.class, fullDocument.getClass())) {
result = converter.read(targetType, raw.getFullDocument()); result = converter.read(targetType, fullDocument);
return converted.compareAndSet(null, result) ? result : converted.get(); return CONVERTED_UPDATER.compareAndSet(this, null, result) ? result : CONVERTED_UPDATER.get(this);
} }
if (converter.getConversionService().canConvert(raw.getFullDocument().getClass(), targetType)) { if (converter.getConversionService().canConvert(fullDocument.getClass(), targetType)) {
result = converter.getConversionService().convert(raw.getFullDocument(), targetType); result = converter.getConversionService().convert(fullDocument, targetType);
return converted.compareAndSet(null, result) ? result : converted.get(); return CONVERTED_UPDATER.compareAndSet(this, null, result) ? result : CONVERTED_UPDATER.get(this);
} }
throw new IllegalArgumentException(String.format("No converter found capable of converting %s to %s", throw new IllegalArgumentException(String.format("No converter found capable of converting %s to %s",
raw.getFullDocument().getClass(), targetType)); fullDocument.getClass(), targetType));
} }
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override @Override
public String toString() { public String toString() {
return "ChangeStreamEvent {" + "raw=" + raw + ", targetType=" + targetType + '}'; return "ChangeStreamEvent {" + "raw=" + raw + ", targetType=" + targetType + '}';

View File

@@ -17,15 +17,19 @@ package org.springframework.data.mongodb.core;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import java.time.Instant;
import java.util.Arrays; import java.util.Arrays;
import java.util.Optional; import java.util.Optional;
import org.bson.BsonTimestamp;
import org.bson.BsonValue; import org.bson.BsonValue;
import org.bson.Document; import org.bson.Document;
import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import com.mongodb.client.model.changestream.ChangeStreamDocument; import com.mongodb.client.model.changestream.ChangeStreamDocument;
import com.mongodb.client.model.changestream.FullDocument; import com.mongodb.client.model.changestream.FullDocument;
@@ -46,6 +50,7 @@ public class ChangeStreamOptions {
private @Nullable BsonValue resumeToken; private @Nullable BsonValue resumeToken;
private @Nullable FullDocument fullDocumentLookup; private @Nullable FullDocument fullDocumentLookup;
private @Nullable Collation collation; private @Nullable Collation collation;
private @Nullable Object resumeTimestamp;
protected ChangeStreamOptions() {} protected ChangeStreamOptions() {}
@@ -77,6 +82,21 @@ public class ChangeStreamOptions {
return Optional.ofNullable(collation); return Optional.ofNullable(collation);
} }
/**
* @return {@link Optional#empty()} if not set.
*/
public Optional<Instant> getResumeTimestamp() {
return Optional.ofNullable(resumeTimestamp).map(timestamp -> asTimestampOfType(timestamp, Instant.class));
}
/**
* @return {@link Optional#empty()} if not set.
* @since 2.2
*/
public Optional<BsonTimestamp> getResumeBsonTimestamp() {
return Optional.ofNullable(resumeTimestamp).map(timestamp -> asTimestampOfType(timestamp, BsonTimestamp.class));
}
/** /**
* @return empty {@link ChangeStreamOptions}. * @return empty {@link ChangeStreamOptions}.
*/ */
@@ -94,6 +114,29 @@ public class ChangeStreamOptions {
return new ChangeStreamOptionsBuilder(); return new ChangeStreamOptionsBuilder();
} }
private static <T> T asTimestampOfType(Object timestamp, Class<T> targetType) {
return targetType.cast(doGetTimestamp(timestamp, targetType));
}
private static <T> Object doGetTimestamp(Object timestamp, Class<T> targetType) {
if (ClassUtils.isAssignableValue(targetType, timestamp)) {
return timestamp;
}
if (timestamp instanceof Instant) {
return new BsonTimestamp((int) ((Instant) timestamp).getEpochSecond(), 0);
}
if (timestamp instanceof BsonTimestamp) {
return Instant.ofEpochSecond(((BsonTimestamp) timestamp).getTime());
}
throw new IllegalArgumentException(
"o_O that should actually not happen. The timestamp should be an Instant or a BsonTimestamp but was "
+ ObjectUtils.nullSafeClassName(timestamp));
}
/** /**
* Builder for creating {@link ChangeStreamOptions}. * Builder for creating {@link ChangeStreamOptions}.
* *
@@ -106,6 +149,7 @@ public class ChangeStreamOptions {
private @Nullable BsonValue resumeToken; private @Nullable BsonValue resumeToken;
private @Nullable FullDocument fullDocumentLookup; private @Nullable FullDocument fullDocumentLookup;
private @Nullable Collation collation; private @Nullable Collation collation;
private @Nullable Object resumeTimestamp;
private ChangeStreamOptionsBuilder() {} private ChangeStreamOptionsBuilder() {}
@@ -200,6 +244,35 @@ public class ChangeStreamOptions {
return this; return this;
} }
/**
* Set the cluster time to resume from.
*
* @param resumeTimestamp must not be {@literal null}.
* @return this.
*/
public ChangeStreamOptionsBuilder resumeAt(Instant resumeTimestamp) {
Assert.notNull(resumeTimestamp, "ResumeTimestamp must not be null!");
this.resumeTimestamp = resumeTimestamp;
return this;
}
/**
* Set the cluster time to resume from.
*
* @param resumeTimestamp must not be {@literal null}.
* @return this.
* @since 2.2
*/
public ChangeStreamOptionsBuilder resumeAt(BsonTimestamp resumeTimestamp) {
Assert.notNull(resumeTimestamp, "ResumeTimestamp must not be null!");
this.resumeTimestamp = resumeTimestamp;
return this;
}
/** /**
* @return the built {@link ChangeStreamOptions} * @return the built {@link ChangeStreamOptions}
*/ */
@@ -211,6 +284,7 @@ public class ChangeStreamOptions {
options.resumeToken = resumeToken; options.resumeToken = resumeToken;
options.fullDocumentLookup = fullDocumentLookup; options.fullDocumentLookup = fullDocumentLookup;
options.collation = collation; options.collation = collation;
options.resumeTimestamp = resumeTimestamp;
return options; return options;
} }

View File

@@ -0,0 +1,683 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import lombok.AccessLevel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.util.Collection;
import java.util.Map;
import org.bson.Document;
import org.springframework.core.convert.ConversionService;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.mapping.IdentifierAccessor;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.mongodb.core.convert.MongoWriter;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import com.mongodb.util.JSONParseException;
/**
* Common operations performed on an entity in the context of it's mapping metadata.
*
* @author Oliver Gierke
* @author Mark Paluch
* @since 2.1
* @see MongoTemplate
* @see ReactiveMongoTemplate
*/
@RequiredArgsConstructor
class EntityOperations {
private static final String ID_FIELD = "_id";
private final @NonNull MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> context;
/**
* Creates a new {@link Entity} for the given bean.
*
* @param entity must not be {@literal null}.
* @return
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public <T> Entity<T> forEntity(T entity) {
Assert.notNull(entity, "Bean must not be null!");
if (entity instanceof String) {
return new UnmappedEntity(parse(entity.toString()));
}
if (entity instanceof Map) {
return new SimpleMappedEntity((Map<String, Object>) entity);
}
return MappedEntity.of(entity, context);
}
/**
* Creates a new {@link AdaptibleEntity} for the given bean and {@link ConversionService}.
*
* @param entity must not be {@literal null}.
* @param conversionService must not be {@literal null}.
* @return
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public <T> AdaptibleEntity<T> forEntity(T entity, ConversionService conversionService) {
Assert.notNull(entity, "Bean must not be null!");
Assert.notNull(conversionService, "ConversionService must not be null!");
if (entity instanceof String) {
return new UnmappedEntity(parse(entity.toString()));
}
if (entity instanceof Map) {
return new SimpleMappedEntity((Map<String, Object>) entity);
}
return AdaptibleMappedEntity.of(entity, context, conversionService);
}
public String determineCollectionName(@Nullable Class<?> entityClass) {
if (entityClass == null) {
throw new InvalidDataAccessApiUsageException(
"No class parameter provided, entity collection can't be determined!");
}
return context.getRequiredPersistentEntity(entityClass).getCollection();
}
/**
* Returns the collection name to be used for the given entity.
*
* @param obj can be {@literal null}.
* @return
*/
@Nullable
public String determineEntityCollectionName(@Nullable Object obj) {
return null == obj ? null : determineCollectionName(obj.getClass());
}
public Query getByIdInQuery(Collection<?> entities) {
MultiValueMap<String, Object> byIds = new LinkedMultiValueMap<>();
entities.stream() //
.map(this::forEntity) //
.forEach(it -> byIds.add(it.getIdFieldName(), it.getId()));
Criteria[] criterias = byIds.entrySet().stream() //
.map(it -> Criteria.where(it.getKey()).in(it.getValue())) //
.toArray(Criteria[]::new);
return new Query(criterias.length == 1 ? criterias[0] : new Criteria().orOperator(criterias));
}
/**
* Returns the name of the identifier property. Considers mapping information but falls back to the MongoDB default of
* {@code _id} if no identifier property can be found.
*
* @param type must not be {@literal null}.
* @return
*/
public String getIdPropertyName(Class<?> type) {
Assert.notNull(type, "Type must not be null!");
MongoPersistentEntity<?> persistentEntity = context.getPersistentEntity(type);
if (persistentEntity != null && persistentEntity.getIdProperty() != null) {
return persistentEntity.getRequiredIdProperty().getName();
}
return ID_FIELD;
}
private static Document parse(String source) {
try {
return Document.parse(source);
} catch (JSONParseException | org.bson.json.JsonParseException o_O) {
throw new MappingException("Could not parse given String to save into a JSON document!", o_O);
}
}
/**
* A representation of information about an entity.
*
* @author Oliver Gierke
* @since 2.1
*/
interface Entity<T> {
/**
* Returns the field name of the identifier of the entity.
*
* @return
*/
String getIdFieldName();
/**
* Returns the identifier of the entity.
*
* @return
*/
Object getId();
/**
* Returns the {@link Query} to find the entity by its identifier.
*
* @return
*/
Query getByIdQuery();
/**
* Returns the {@link Query} to find the entity in its current version.
*
* @return
*/
Query getQueryForVersion();
/**
* Maps the backing entity into a {@link MappedDocument} using the given {@link MongoWriter}.
*
* @param writer must not be {@literal null}.
* @return
*/
MappedDocument toMappedDocument(MongoWriter<? super T> writer);
/**
* Asserts that the identifier type is updatable in case its not already set.
*/
default void assertUpdateableIdIfNotSet() {}
/**
* Returns whether the entity is versioned, i.e. if it contains a version property.
*
* @return
*/
default boolean isVersionedEntity() {
return false;
}
/**
* Returns the value of the version if the entity has a version property, {@literal null} otherwise.
*
* @return
*/
@Nullable
Object getVersion();
/**
* Returns the underlying bean.
*
* @return
*/
T getBean();
/**
* Returns whether the entity is considered to be new.
*
* @return
* @since 2.1.2
*/
boolean isNew();
}
/**
* Information and commands on an entity.
*
* @author Oliver Gierke
* @since 2.1
*/
interface AdaptibleEntity<T> extends Entity<T> {
/**
* Populates the identifier of the backing entity if it has an identifier property and there's no identifier
* currently present.
*
* @param id must not be {@literal null}.
* @return
*/
@Nullable
T populateIdIfNecessary(@Nullable Object id);
/**
* Initializes the version property of the of the current entity if available.
*
* @return the entity with the version property updated if available.
*/
T initializeVersionProperty();
/**
* Increments the value of the version property if available.
*
* @return the entity with the version property incremented if available.
*/
T incrementVersion();
/**
* Returns the current version value if the entity has a version property.
*
* @return the current version or {@literal null} in case it's uninitialized or the entity doesn't expose a version
* property.
*/
@Nullable
Number getVersion();
}
@RequiredArgsConstructor
private static class UnmappedEntity<T extends Map<String, Object>> implements AdaptibleEntity<T> {
private final T map;
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getIdPropertyName()
*/
@Override
public String getIdFieldName() {
return ID_FIELD;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getId()
*/
@Override
public Object getId() {
return map.get(ID_FIELD);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getByIdQuery()
*/
@Override
public Query getByIdQuery() {
return Query.query(Criteria.where(ID_FIELD).is(map.get(ID_FIELD)));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.MutablePersistableSource#populateIdIfNecessary(java.lang.Object)
*/
@Nullable
@Override
public T populateIdIfNecessary(@Nullable Object id) {
map.put(ID_FIELD, id);
return map;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getQueryForVersion()
*/
@Override
public Query getQueryForVersion() {
throw new MappingException("Cannot query for version on plain Documents!");
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#toMappedDocument(org.springframework.data.mongodb.core.convert.MongoWriter)
*/
@Override
public MappedDocument toMappedDocument(MongoWriter<? super T> writer) {
return MappedDocument.of(map instanceof Document //
? (Document) map //
: new Document(map));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.MutablePersistableSource#initializeVersionProperty()
*/
@Override
public T initializeVersionProperty() {
return map;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.MutablePersistableSource#getVersion()
*/
@Override
@Nullable
public Number getVersion() {
return null;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.MutablePersistableSource#incrementVersion()
*/
@Override
public T incrementVersion() {
return map;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getBean()
*/
@Override
public T getBean() {
return map;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.Entity#isNew()
*/
@Override
public boolean isNew() {
return map.get(ID_FIELD) != null;
}
}
private static class SimpleMappedEntity<T extends Map<String, Object>> extends UnmappedEntity<T> {
SimpleMappedEntity(T map) {
super(map);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#toMappedDocument(org.springframework.data.mongodb.core.convert.MongoWriter)
*/
@Override
@SuppressWarnings("unchecked")
public MappedDocument toMappedDocument(MongoWriter<? super T> writer) {
T bean = getBean();
bean = (T) (bean instanceof Document //
? (Document) bean //
: new Document(bean));
Document document = new Document();
writer.write(bean, document);
return MappedDocument.of(document);
}
}
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
private static class MappedEntity<T> implements Entity<T> {
private final @NonNull MongoPersistentEntity<?> entity;
private final @NonNull IdentifierAccessor idAccessor;
private final @NonNull PersistentPropertyAccessor<T> propertyAccessor;
private static <T> MappedEntity<T> of(T bean,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> context) {
MongoPersistentEntity<?> entity = context.getRequiredPersistentEntity(bean.getClass());
IdentifierAccessor identifierAccessor = entity.getIdentifierAccessor(bean);
PersistentPropertyAccessor<T> propertyAccessor = entity.getPropertyAccessor(bean);
return new MappedEntity<>(entity, identifierAccessor, propertyAccessor);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getIdPropertyName()
*/
@Override
public String getIdFieldName() {
return entity.getRequiredIdProperty().getFieldName();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getId()
*/
@Override
public Object getId() {
return idAccessor.getRequiredIdentifier();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getByIdQuery()
*/
@Override
public Query getByIdQuery() {
if (!entity.hasIdProperty()) {
throw new MappingException("No id property found for object of type " + entity.getType() + "!");
}
MongoPersistentProperty idProperty = entity.getRequiredIdProperty();
return Query.query(Criteria.where(idProperty.getName()).is(getId()));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getQueryForVersion(java.lang.Object)
*/
@Override
public Query getQueryForVersion() {
MongoPersistentProperty idProperty = entity.getRequiredIdProperty();
MongoPersistentProperty property = entity.getRequiredVersionProperty();
return new Query(Criteria.where(idProperty.getName()).is(getId())//
.and(property.getName()).is(getVersion()));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#toMappedDocument(org.springframework.data.mongodb.core.convert.MongoWriter)
*/
@Override
public MappedDocument toMappedDocument(MongoWriter<? super T> writer) {
T bean = propertyAccessor.getBean();
Document document = new Document();
writer.write(bean, document);
if (document.containsKey(ID_FIELD) && document.get(ID_FIELD) == null) {
document.remove(ID_FIELD);
}
return MappedDocument.of(document);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.Entity#assertUpdateableIdIfNotSet()
*/
public void assertUpdateableIdIfNotSet() {
if (!entity.hasIdProperty()) {
return;
}
MongoPersistentProperty property = entity.getRequiredIdProperty();
Object propertyValue = idAccessor.getIdentifier();
if (propertyValue != null) {
return;
}
if (!MongoSimpleTypes.AUTOGENERATED_ID_TYPES.contains(property.getType())) {
throw new InvalidDataAccessApiUsageException(
String.format("Cannot autogenerate id of type %s for entity of type %s!", property.getType().getName(),
entity.getType().getName()));
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#isVersionedEntity()
*/
@Override
public boolean isVersionedEntity() {
return entity.hasVersionProperty();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getVersion()
*/
@Override
@Nullable
public Object getVersion() {
return propertyAccessor.getProperty(entity.getRequiredVersionProperty());
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getBean()
*/
@Override
public T getBean() {
return propertyAccessor.getBean();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.Entity#isNew()
*/
@Override
public boolean isNew() {
return entity.isNew(propertyAccessor.getBean());
}
}
private static class AdaptibleMappedEntity<T> extends MappedEntity<T> implements AdaptibleEntity<T> {
private final MongoPersistentEntity<?> entity;
private final ConvertingPropertyAccessor<T> propertyAccessor;
private final IdentifierAccessor identifierAccessor;
private AdaptibleMappedEntity(MongoPersistentEntity<?> entity, IdentifierAccessor identifierAccessor,
ConvertingPropertyAccessor<T> propertyAccessor) {
super(entity, identifierAccessor, propertyAccessor);
this.entity = entity;
this.propertyAccessor = propertyAccessor;
this.identifierAccessor = identifierAccessor;
}
private static <T> AdaptibleEntity<T> of(T bean,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> context,
ConversionService conversionService) {
MongoPersistentEntity<?> entity = context.getRequiredPersistentEntity(bean.getClass());
IdentifierAccessor identifierAccessor = entity.getIdentifierAccessor(bean);
PersistentPropertyAccessor<T> propertyAccessor = entity.getPropertyAccessor(bean);
return new AdaptibleMappedEntity<>(entity, identifierAccessor,
new ConvertingPropertyAccessor<>(propertyAccessor, conversionService));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.AdaptibleEntity#populateIdIfNecessary(java.lang.Object)
*/
@Nullable
@Override
public T populateIdIfNecessary(@Nullable Object id) {
if (id == null) {
return null;
}
T bean = propertyAccessor.getBean();
MongoPersistentProperty idProperty = entity.getIdProperty();
if (idProperty == null) {
return bean;
}
if (identifierAccessor.getIdentifier() != null) {
return bean;
}
propertyAccessor.setProperty(idProperty, id);
return propertyAccessor.getBean();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.MappedEntity#getVersion()
*/
@Override
@Nullable
public Number getVersion() {
MongoPersistentProperty versionProperty = entity.getRequiredVersionProperty();
return propertyAccessor.getProperty(versionProperty, Number.class);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.AdaptibleEntity#initializeVersionProperty()
*/
@Override
public T initializeVersionProperty() {
if (!entity.hasVersionProperty()) {
return propertyAccessor.getBean();
}
MongoPersistentProperty versionProperty = entity.getRequiredVersionProperty();
propertyAccessor.setProperty(versionProperty, versionProperty.getType().isPrimitive() ? 1 : 0);
return propertyAccessor.getBean();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.AdaptibleEntity#incrementVersion()
*/
@Override
public T incrementVersion() {
MongoPersistentProperty versionProperty = entity.getRequiredVersionProperty();
Number version = getVersion();
Number nextVersion = version == null ? 0 : version.longValue() + 1;
propertyAccessor.setProperty(versionProperty, nextVersion);
return propertyAccessor.getBean();
}
}
}

View File

@@ -35,22 +35,10 @@ import org.springframework.util.StringUtils;
* @author Mark Paluch * @author Mark Paluch
* @since 2.0 * @since 2.0
*/ */
@RequiredArgsConstructor
class ExecutableAggregationOperationSupport implements ExecutableAggregationOperation { class ExecutableAggregationOperationSupport implements ExecutableAggregationOperation {
private final MongoTemplate template; private final @NonNull MongoTemplate template;
/**
* Create new instance of {@link ExecutableAggregationOperationSupport}.
*
* @param template must not be {@literal null}.
* @throws IllegalArgumentException if template is {@literal null}.
*/
ExecutableAggregationOperationSupport(MongoTemplate template) {
Assert.notNull(template, "Template must not be null!");
this.template = template;
}
/* /*
* (non-Javadoc) * (non-Javadoc)
@@ -131,11 +119,11 @@ class ExecutableAggregationOperationSupport implements ExecutableAggregationOper
TypedAggregation<?> typedAggregation = (TypedAggregation<?>) aggregation; TypedAggregation<?> typedAggregation = (TypedAggregation<?>) aggregation;
if (typedAggregation.getInputType() != null) { if (typedAggregation.getInputType() != null) {
return template.determineCollectionName(typedAggregation.getInputType()); return template.getCollectionName(typedAggregation.getInputType());
} }
} }
return template.determineCollectionName(domainType); return template.getCollectionName(domainType);
} }
} }
} }

View File

@@ -45,24 +45,12 @@ import com.mongodb.client.FindIterable;
* @author Mark Paluch * @author Mark Paluch
* @since 2.0 * @since 2.0
*/ */
@RequiredArgsConstructor
class ExecutableFindOperationSupport implements ExecutableFindOperation { class ExecutableFindOperationSupport implements ExecutableFindOperation {
private static final Query ALL_QUERY = new Query(); private static final Query ALL_QUERY = new Query();
private final MongoTemplate template; private final @NonNull MongoTemplate template;
/**
* Create new {@link ExecutableFindOperationSupport}.
*
* @param template must not be {@literal null}.
* @throws IllegalArgumentException if template is {@literal null}.
*/
ExecutableFindOperationSupport(MongoTemplate template) {
Assert.notNull(template, "Template must not be null!");
this.template = template;
}
/* /*
* (non-Javadoc) * (non-Javadoc)
@@ -242,7 +230,7 @@ class ExecutableFindOperationSupport implements ExecutableFindOperation {
} }
private String getCollectionName() { private String getCollectionName() {
return StringUtils.hasText(collection) ? collection : template.determineCollectionName(domainType); return StringUtils.hasText(collection) ? collection : template.getCollectionName(domainType);
} }
private String asString() { private String asString() {

View File

@@ -63,17 +63,19 @@ public interface ExecutableInsertOperation {
* Insert exactly one object. * Insert exactly one object.
* *
* @param object must not be {@literal null}. * @param object must not be {@literal null}.
* @return the inserted object.
* @throws IllegalArgumentException if object is {@literal null}. * @throws IllegalArgumentException if object is {@literal null}.
*/ */
void one(T object); T one(T object);
/** /**
* Insert a collection of objects. * Insert a collection of objects.
* *
* @param objects must not be {@literal null}. * @param objects must not be {@literal null}.
* @return the inserted objects.
* @throws IllegalArgumentException if objects is {@literal null}. * @throws IllegalArgumentException if objects is {@literal null}.
*/ */
void all(Collection<? extends T> objects); Collection<? extends T> all(Collection<? extends T> objects);
} }
/** /**

View File

@@ -37,22 +37,10 @@ import com.mongodb.bulk.BulkWriteResult;
* @author Mark Paluch * @author Mark Paluch
* @since 2.0 * @since 2.0
*/ */
@RequiredArgsConstructor
class ExecutableInsertOperationSupport implements ExecutableInsertOperation { class ExecutableInsertOperationSupport implements ExecutableInsertOperation {
private final MongoTemplate template; private final @NonNull MongoTemplate template;
/**
* Create new {@link ExecutableInsertOperationSupport}.
*
* @param template must not be {@literal null}.
* @throws IllegalArgumentException if template is {@literal null}.
*/
ExecutableInsertOperationSupport(MongoTemplate template) {
Assert.notNull(template, "Template must not be null!");
this.template = template;
}
/* /*
* (non-Javadoc) * (non-Javadoc)
@@ -84,11 +72,11 @@ class ExecutableInsertOperationSupport implements ExecutableInsertOperation {
* @see org.springframework.data.mongodb.core.ExecutableInsertOperation.TerminatingInsert#insert(java.lang.Class) * @see org.springframework.data.mongodb.core.ExecutableInsertOperation.TerminatingInsert#insert(java.lang.Class)
*/ */
@Override @Override
public void one(T object) { public T one(T object) {
Assert.notNull(object, "Object must not be null!"); Assert.notNull(object, "Object must not be null!");
template.insert(object, getCollectionName()); return template.insert(object, getCollectionName());
} }
/* /*
@@ -96,11 +84,11 @@ class ExecutableInsertOperationSupport implements ExecutableInsertOperation {
* @see org.springframework.data.mongodb.core.ExecutableInsertOperation.TerminatingInsert#all(java.util.Collection) * @see org.springframework.data.mongodb.core.ExecutableInsertOperation.TerminatingInsert#all(java.util.Collection)
*/ */
@Override @Override
public void all(Collection<? extends T> objects) { public Collection<T> all(Collection<? extends T> objects) {
Assert.notNull(objects, "Objects must not be null!"); Assert.notNull(objects, "Objects must not be null!");
template.insert(objects, getCollectionName()); return template.insert(objects, getCollectionName());
} }
/* /*
@@ -141,7 +129,7 @@ class ExecutableInsertOperationSupport implements ExecutableInsertOperation {
} }
private String getCollectionName() { private String getCollectionName() {
return StringUtils.hasText(collection) ? collection : template.determineCollectionName(domainType); return StringUtils.hasText(collection) ? collection : template.getCollectionName(domainType);
} }
} }
} }

View File

@@ -0,0 +1,199 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import java.util.List;
import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind;
import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
import org.springframework.data.mongodb.core.query.Query;
/**
* {@link ExecutableMapReduceOperation} allows creation and execution of MongoDB mapReduce operations in a fluent API
* style. The starting {@literal domainType} is used for mapping an optional {@link Query} provided via {@code matching}
* into the MongoDB specific representation. By default, the originating {@literal domainType} is also used for mapping
* back the results from the {@link org.bson.Document}. However, it is possible to define an different
* {@literal returnType} via {@code as} to mapping the result.<br />
* The collection to operate on is by default derived from the initial {@literal domainType} and can be defined there
* via {@link org.springframework.data.mongodb.core.mapping.Document}. Using {@code inCollection} allows to override the
* collection name for the execution.
*
* <pre>
* <code>
* mapReduce(Human.class)
* .map("function() { emit(this.id, this.firstname) }")
* .reduce("function(id, name) { return sum(id, name); }")
* .inCollection("star-wars")
* .as(Jedi.class)
* .matching(query(where("lastname").is("skywalker")))
* .all();
* </code>
* </pre>
*
* @author Christoph Strobl
* @since 2.1
*/
public interface ExecutableMapReduceOperation {
/**
* Start creating a mapReduce operation for the given {@literal domainType}.
*
* @param domainType must not be {@literal null}.
* @return new instance of {@link ExecutableFind}.
* @throws IllegalArgumentException if domainType is {@literal null}.
*/
<T> MapReduceWithMapFunction<T> mapReduce(Class<T> domainType);
/**
* Trigger mapReduce execution by calling one of the terminating methods.
*
* @author Christoph Strobl
* @since 2.1
*/
interface TerminatingMapReduce<T> {
/**
* Get the mapReduce results.
*
* @return never {@literal null}.
*/
List<T> all();
}
/**
* Provide the Javascript {@code function()} used to map matching documents.
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithMapFunction<T> {
/**
* Set the Javascript map {@code function()}.
*
* @param mapFunction must not be {@literal null} nor empty.
* @return new instance of {@link MapReduceWithReduceFunction}.
* @throws IllegalArgumentException if {@literal mapFunction} is {@literal null} or empty.
*/
MapReduceWithReduceFunction<T> map(String mapFunction);
}
/**
* Provide the Javascript {@code function()} used to reduce matching documents.
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithReduceFunction<T> {
/**
* Set the Javascript map {@code function()}.
*
* @param reduceFunction must not be {@literal null} nor empty.
* @return new instance of {@link ExecutableMapReduce}.
* @throws IllegalArgumentException if {@literal reduceFunction} is {@literal null} or empty.
*/
ExecutableMapReduce<T> reduce(String reduceFunction);
}
/**
* Collection override (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithCollection<T> extends MapReduceWithQuery<T> {
/**
* Explicitly set the name of the collection to perform the mapReduce operation on. <br />
* Skip this step to use the default collection derived from the domain type.
*
* @param collection must not be {@literal null} nor {@literal empty}.
* @return new instance of {@link MapReduceWithProjection}.
* @throws IllegalArgumentException if collection is {@literal null}.
*/
MapReduceWithProjection<T> inCollection(String collection);
}
/**
* Input document filter query (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithQuery<T> extends TerminatingMapReduce<T> {
/**
* Set the filter query to be used.
*
* @param query must not be {@literal null}.
* @return new instance of {@link TerminatingMapReduce}.
* @throws IllegalArgumentException if query is {@literal null}.
*/
TerminatingMapReduce<T> matching(Query query);
}
/**
* Result type override (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithProjection<T> extends MapReduceWithQuery<T> {
/**
* Define the target type fields should be mapped to. <br />
* Skip this step if you are anyway only interested in the original domain type.
*
* @param resultType must not be {@literal null}.
* @param <R> result type.
* @return new instance of {@link TerminatingMapReduce}.
* @throws IllegalArgumentException if resultType is {@literal null}.
*/
<R> MapReduceWithQuery<R> as(Class<R> resultType);
}
/**
* Additional mapReduce options (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithOptions<T> {
/**
* Set additional options to apply to the mapReduce operation.
*
* @param options must not be {@literal null}.
* @return new instance of {@link ExecutableMapReduce}.
* @throws IllegalArgumentException if options is {@literal null}.
*/
ExecutableMapReduce<T> with(MapReduceOptions options);
}
/**
* {@link ExecutableMapReduce} provides methods for constructing mapReduce operations in a fluent way.
*
* @author Christoph Strobl
* @since 2.1
*/
interface ExecutableMapReduce<T> extends MapReduceWithMapFunction<T>, MapReduceWithReduceFunction<T>,
MapReduceWithCollection<T>, MapReduceWithProjection<T>, MapReduceWithOptions<T> {
}
}

View File

@@ -0,0 +1,177 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.util.List;
import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Implementation of {@link ExecutableMapReduceOperation}.
*
* @author Christoph Strobl
* @since 2.1
*/
@RequiredArgsConstructor
class ExecutableMapReduceOperationSupport implements ExecutableMapReduceOperation {
private static final Query ALL_QUERY = new Query();
private final @NonNull MongoTemplate template;
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation#mapReduce(java.lang.Class)
*/
@Override
public <T> ExecutableMapReduceSupport<T> mapReduce(Class<T> domainType) {
Assert.notNull(domainType, "DomainType must not be null!");
return new ExecutableMapReduceSupport<>(template, domainType, domainType, null, ALL_QUERY, null, null, null);
}
/**
* @author Christoph Strobl
* @since 2.1
*/
static class ExecutableMapReduceSupport<T>
implements ExecutableMapReduce<T>, MapReduceWithOptions<T>, MapReduceWithCollection<T>,
MapReduceWithProjection<T>, MapReduceWithQuery<T>, MapReduceWithReduceFunction<T>, MapReduceWithMapFunction<T> {
private final MongoTemplate template;
private final Class<?> domainType;
private final Class<T> returnType;
private final @Nullable String collection;
private final Query query;
private final @Nullable String mapFunction;
private final @Nullable String reduceFunction;
private final @Nullable MapReduceOptions options;
ExecutableMapReduceSupport(MongoTemplate template, Class<?> domainType, Class<T> returnType,
@Nullable String collection, Query query, @Nullable String mapFunction, @Nullable String reduceFunction,
@Nullable MapReduceOptions options) {
this.template = template;
this.domainType = domainType;
this.returnType = returnType;
this.collection = collection;
this.query = query;
this.mapFunction = mapFunction;
this.reduceFunction = reduceFunction;
this.options = options;
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.TerminatingMapReduce#all()
*/
@Override
public List<T> all() {
return template.mapReduce(query, domainType, getCollectionName(), mapFunction, reduceFunction, options,
returnType);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.MapReduceWithCollection#inCollection(java.lang.String)
*/
@Override
public MapReduceWithProjection<T> inCollection(String collection) {
Assert.hasText(collection, "Collection name must not be null nor empty!");
return new ExecutableMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.MapReduceWithQuery#query(org.springframework.data.mongodb.core.query.Query)
*/
@Override
public TerminatingMapReduce<T> matching(Query query) {
Assert.notNull(query, "Query must not be null!");
return new ExecutableMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.MapReduceWithProjection#as(java.lang.Class)
*/
@Override
public <R> MapReduceWithQuery<R> as(Class<R> resultType) {
Assert.notNull(resultType, "ResultType must not be null!");
return new ExecutableMapReduceSupport<>(template, domainType, resultType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.MapReduceWithOptions#with(org.springframework.data.mongodb.core.mapreduce.MapReduceOptions)
*/
@Override
public ExecutableMapReduce<T> with(MapReduceOptions options) {
Assert.notNull(options, "Options must not be null! Please consider empty MapReduceOptions#options() instead.");
return new ExecutableMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.MapReduceWithMapFunction#map(java.lang.String)
*/
@Override
public MapReduceWithReduceFunction<T> map(String mapFunction) {
Assert.hasText(mapFunction, "MapFunction name must not be null nor empty!");
return new ExecutableMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.MapReduceWithReduceFunction#reduce(java.lang.String)
*/
@Override
public ExecutableMapReduce<T> reduce(String reduceFunction) {
Assert.hasText(reduceFunction, "ReduceFunction name must not be null nor empty!");
return new ExecutableMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
private String getCollectionName() {
return StringUtils.hasText(collection) ? collection : template.getCollectionName(domainType);
}
}
}

View File

@@ -53,6 +53,37 @@ public interface ExecutableRemoveOperation {
*/ */
<T> ExecutableRemove<T> remove(Class<T> domainType); <T> ExecutableRemove<T> remove(Class<T> domainType);
/**
* @author Christoph Strobl
* @since 2.0
*/
interface TerminatingRemove<T> {
/**
* Remove all documents matching.
*
* @return the {@link DeleteResult}. Never {@literal null}.
*/
DeleteResult all();
/**
* Remove the first matching document.
*
* @return the {@link DeleteResult}. Never {@literal null}.
*/
DeleteResult one();
/**
* Remove and return all matching documents. <br/>
* <strong>NOTE</strong> The entire list of documents will be fetched before sending the actual delete commands.
* Also, {@link org.springframework.context.ApplicationEvent}s will be published for each and every delete
* operation.
*
* @return empty {@link List} if no match found. Never {@literal null}.
*/
List<T> findAndRemove();
}
/** /**
* Collection override (optional). * Collection override (optional).
* *
@@ -73,29 +104,6 @@ public interface ExecutableRemoveOperation {
RemoveWithQuery<T> inCollection(String collection); RemoveWithQuery<T> inCollection(String collection);
} }
/**
* @author Christoph Strobl
* @since 2.0
*/
interface TerminatingRemove<T> {
/**
* Remove all documents matching.
*
* @return the {@link DeleteResult}. Never {@literal null}.
*/
DeleteResult all();
/**
* Remove and return all matching documents. <br/>
* <strong>NOTE</strong> The entire list of documents will be fetched before sending the actual delete commands.
* Also, {@link org.springframework.context.ApplicationEvent}s will be published for each and every delete
* operation.
*
* @return empty {@link List} if no match found. Never {@literal null}.
*/
List<T> findAndRemove();
}
/** /**
* @author Christoph Strobl * @author Christoph Strobl

View File

@@ -36,24 +36,12 @@ import com.mongodb.client.result.DeleteResult;
* @author Mark Paluch * @author Mark Paluch
* @since 2.0 * @since 2.0
*/ */
@RequiredArgsConstructor
class ExecutableRemoveOperationSupport implements ExecutableRemoveOperation { class ExecutableRemoveOperationSupport implements ExecutableRemoveOperation {
private static final Query ALL_QUERY = new Query(); private static final Query ALL_QUERY = new Query();
private final MongoTemplate tempate; private final @NonNull MongoTemplate tempate;
/**
* Create new {@link ExecutableRemoveOperationSupport}.
*
* @param template must not be {@literal null}.
* @throws IllegalArgumentException if template is {@literal null}.
*/
ExecutableRemoveOperationSupport(MongoTemplate template) {
Assert.notNull(template, "Template must not be null!");
this.tempate = template;
}
/* /*
* (non-Javadoc) * (non-Javadoc)
@@ -110,10 +98,16 @@ class ExecutableRemoveOperationSupport implements ExecutableRemoveOperation {
*/ */
@Override @Override
public DeleteResult all() { public DeleteResult all() {
return template.doRemove(getCollectionName(), query, domainType, true);
}
String collectionName = getCollectionName(); /*
* (non-Javadoc)
return template.doRemove(collectionName, query, domainType); * @see org.springframework.data.mongodb.core.ExecutableRemoveOperation.TerminatingRemove#one()
*/
@Override
public DeleteResult one() {
return template.doRemove(getCollectionName(), query, domainType, false);
} }
/* /*
@@ -129,7 +123,7 @@ class ExecutableRemoveOperationSupport implements ExecutableRemoveOperation {
} }
private String getCollectionName() { private String getCollectionName() {
return StringUtils.hasText(collection) ? collection : template.determineCollectionName(domainType); return StringUtils.hasText(collection) ? collection : template.getCollectionName(domainType);
} }
} }
} }

View File

@@ -19,13 +19,13 @@ import java.util.Optional;
import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.Update;
import com.mongodb.client.result.UpdateResult;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import com.mongodb.client.result.UpdateResult;
/** /**
* {@link ExecutableUpdateOperation} allows creation and execution of MongoDB update / findAndModify operations in a * {@link ExecutableUpdateOperation} allows creation and execution of MongoDB update / findAndModify / findAndReplace
* fluent API style. <br /> * operations in a fluent API style. <br />
* The starting {@literal domainType} is used for mapping the {@link Query} provided via {@code matching}, as well as * The starting {@literal domainType} is used for mapping the {@link Query} provided via {@code matching}, as well as
* the {@link Update} via {@code apply} into the MongoDB specific representations. The collection to operate on is by * the {@link Update} via {@code apply} into the MongoDB specific representations. The collection to operate on is by
* default derived from the initial {@literal domainType} and can be defined there via * default derived from the initial {@literal domainType} and can be defined there via
@@ -57,6 +57,91 @@ public interface ExecutableUpdateOperation {
*/ */
<T> ExecutableUpdate<T> update(Class<T> domainType); <T> ExecutableUpdate<T> update(Class<T> domainType);
/**
* Trigger findAndModify execution by calling one of the terminating methods.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.0
*/
interface TerminatingFindAndModify<T> {
/**
* Find, modify and return the first matching document.
*
* @return {@link Optional#empty()} if nothing found.
*/
default Optional<T> findAndModify() {
return Optional.ofNullable(findAndModifyValue());
}
/**
* Find, modify and return the first matching document.
*
* @return {@literal null} if nothing found.
*/
@Nullable
T findAndModifyValue();
}
/**
* Trigger
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* execution by calling one of the terminating methods.
*
* @author Mark Paluch
* @since 2.1
*/
interface TerminatingFindAndReplace<T> {
/**
* Find, replace and return the first matching document.
*
* @return {@link Optional#empty()} if nothing found.
*/
default Optional<T> findAndReplace() {
return Optional.ofNullable(findAndReplaceValue());
}
/**
* Find, replace and return the first matching document.
*
* @return {@literal null} if nothing found.
*/
@Nullable
T findAndReplaceValue();
}
/**
* Trigger update execution by calling one of the terminating methods.
*
* @author Christoph Strobl
* @since 2.0
*/
interface TerminatingUpdate<T> extends TerminatingFindAndModify<T>, FindAndModifyWithOptions<T> {
/**
* Update all matching documents in the collection.
*
* @return never {@literal null}.
*/
UpdateResult all();
/**
* Update the first document in the collection.
*
* @return never {@literal null}.
*/
UpdateResult first();
/**
* Creates a new document if no documents match the filter query or updates the matching ones.
*
* @return never {@literal null}.
*/
UpdateResult upsert();
}
/** /**
* Declare the {@link Update} to apply. * Declare the {@link Update} to apply.
* *
@@ -73,6 +158,16 @@ public interface ExecutableUpdateOperation {
* @throws IllegalArgumentException if update is {@literal null}. * @throws IllegalArgumentException if update is {@literal null}.
*/ */
TerminatingUpdate<T> apply(Update update); TerminatingUpdate<T> apply(Update update);
/**
* Specify {@code replacement} object.
*
* @param replacement must not be {@literal null}.
* @return new instance of {@link FindAndReplaceOptions}.
* @throws IllegalArgumentException if options is {@literal null}.
* @since 2.1
*/
FindAndReplaceWithProjection<T> replaceWith(T replacement);
} }
/** /**
@@ -131,56 +226,43 @@ public interface ExecutableUpdateOperation {
} }
/** /**
* Trigger findAndModify execution by calling one of the terminating methods. * Define {@link FindAndReplaceOptions}.
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.1
*/ */
interface TerminatingFindAndModify<T> { interface FindAndReplaceWithOptions<T> extends TerminatingFindAndReplace<T> {
/** /**
* Find, modify and return the first matching document. * Explicitly define {@link FindAndReplaceOptions} for the {@link Update}.
* *
* @return {@link Optional#empty()} if nothing found. * @param options must not be {@literal null}.
* @return new instance of {@link FindAndReplaceOptions}.
* @throws IllegalArgumentException if options is {@literal null}.
*/ */
default Optional<T> findAndModify() { FindAndReplaceWithProjection<T> withOptions(FindAndReplaceOptions options);
return Optional.ofNullable(findAndModifyValue());
}
/**
* Find, modify and return the first matching document.
*
* @return {@literal null} if nothing found.
*/
@Nullable
T findAndModifyValue();
} }
/** /**
* Trigger update execution by calling one of the terminating methods. * Result type override (Optional).
* *
* @author Christoph Strobl * @author Christoph Strobl
* @since 2.0 * @since 2.1
*/ */
interface TerminatingUpdate<T> extends TerminatingFindAndModify<T>, FindAndModifyWithOptions<T> { interface FindAndReplaceWithProjection<T> extends FindAndReplaceWithOptions<T> {
/** /**
* Update all matching documents in the collection. * Define the target type fields should be mapped to. <br />
* Skip this step if you are anyway only interested in the original domain type.
* *
* @return never {@literal null}. * @param resultType must not be {@literal null}.
* @param <R> result type.
* @return new instance of {@link FindAndReplaceWithProjection}.
* @throws IllegalArgumentException if resultType is {@literal null}.
*/ */
UpdateResult all(); <R> FindAndReplaceWithOptions<R> as(Class<R> resultType);
/**
* Update the first document in the collection.
*
* @return never {@literal null}.
*/
UpdateResult first();
/**
* Creates a new document if no documents match the filter query or updates the matching ones.
*
* @return never {@literal null}.
*/
UpdateResult upsert();
} }
/** /**

View File

@@ -35,23 +35,12 @@ import com.mongodb.client.result.UpdateResult;
* @author Mark Paluch * @author Mark Paluch
* @since 2.0 * @since 2.0
*/ */
@RequiredArgsConstructor
class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
private static final Query ALL_QUERY = new Query(); private static final Query ALL_QUERY = new Query();
private final MongoTemplate template; private final @NonNull MongoTemplate template;
/**
* Creates new {@link ExecutableUpdateOperationSupport}.
*
* @param template must not be {@literal null}.
*/
ExecutableUpdateOperationSupport(MongoTemplate template) {
Assert.notNull(template, "Template must not be null!");
this.template = template;
}
/* /*
* (non-Javadoc) * (non-Javadoc)
@@ -62,7 +51,7 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
Assert.notNull(domainType, "DomainType must not be null!"); Assert.notNull(domainType, "DomainType must not be null!");
return new ExecutableUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null); return new ExecutableUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null, null, null, domainType);
} }
/** /**
@@ -72,14 +61,18 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
@RequiredArgsConstructor @RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
static class ExecutableUpdateSupport<T> static class ExecutableUpdateSupport<T>
implements ExecutableUpdate<T>, UpdateWithCollection<T>, UpdateWithQuery<T>, TerminatingUpdate<T> { implements ExecutableUpdate<T>, UpdateWithCollection<T>, UpdateWithQuery<T>, TerminatingUpdate<T>,
FindAndReplaceWithOptions<T>, TerminatingFindAndReplace<T>, FindAndReplaceWithProjection<T> {
@NonNull MongoTemplate template; @NonNull MongoTemplate template;
@NonNull Class<T> domainType; @NonNull Class domainType;
Query query; Query query;
@Nullable Update update; @Nullable Update update;
@Nullable String collection; @Nullable String collection;
@Nullable FindAndModifyOptions options; @Nullable FindAndModifyOptions findAndModifyOptions;
@Nullable FindAndReplaceOptions findAndReplaceOptions;
@Nullable Object replacement;
@NonNull Class<T> targetType;
/* /*
* (non-Javadoc) * (non-Javadoc)
@@ -90,7 +83,8 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
Assert.notNull(update, "Update must not be null!"); Assert.notNull(update, "Update must not be null!");
return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, options); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
findAndReplaceOptions, replacement, targetType);
} }
/* /*
@@ -102,7 +96,8 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
Assert.hasText(collection, "Collection must not be null nor empty!"); Assert.hasText(collection, "Collection must not be null nor empty!");
return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, options); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
findAndReplaceOptions, replacement, targetType);
} }
/* /*
@@ -114,7 +109,34 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
Assert.notNull(options, "Options must not be null!"); Assert.notNull(options, "Options must not be null!");
return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, options); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, options,
findAndReplaceOptions, replacement, targetType);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ExecutableUpdateOperation.UpdateWithUpdate#replaceWith(Object)
*/
@Override
public FindAndReplaceWithProjection<T> replaceWith(T replacement) {
Assert.notNull(replacement, "Replacement must not be null!");
return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
findAndReplaceOptions, replacement, targetType);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ExecutableUpdateOperation.FindAndReplaceWithOptions#withOptions(org.springframework.data.mongodb.core.FindAndReplaceOptions)
*/
@Override
public FindAndReplaceWithProjection<T> withOptions(FindAndReplaceOptions options) {
Assert.notNull(options, "Options must not be null!");
return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
options, replacement, targetType);
} }
/* /*
@@ -126,7 +148,21 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
Assert.notNull(query, "Query must not be null!"); Assert.notNull(query, "Query must not be null!");
return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, options); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
findAndReplaceOptions, replacement, targetType);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ReactiveUpdateOperation.FindAndReplaceWithProjection#as(java.lang.Class)
*/
@Override
public <R> FindAndReplaceWithOptions<R> as(Class<R> resultType) {
Assert.notNull(resultType, "ResultType must not be null!");
return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
findAndReplaceOptions, replacement, resultType);
} }
/* /*
@@ -162,7 +198,22 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
*/ */
@Override @Override
public @Nullable T findAndModifyValue() { public @Nullable T findAndModifyValue() {
return template.findAndModify(query, update, options != null ? options : new FindAndModifyOptions(), domainType, getCollectionName());
return template.findAndModify(query, update,
findAndModifyOptions != null ? findAndModifyOptions : new FindAndModifyOptions(), targetType,
getCollectionName());
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ExecutableUpdateOperation.TerminatingFindAndReplace#findAndReplaceValue()
*/
@Override
public @Nullable T findAndReplaceValue() {
return (T) template.findAndReplace(query, replacement,
findAndReplaceOptions != null ? findAndReplaceOptions : FindAndReplaceOptions.empty(), domainType,
getCollectionName(), targetType);
} }
private UpdateResult doUpdate(boolean multi, boolean upsert) { private UpdateResult doUpdate(boolean multi, boolean upsert) {
@@ -170,7 +221,7 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
} }
private String getCollectionName() { private String getCollectionName() {
return StringUtils.hasText(collection) ? collection : template.determineCollectionName(domainType); return StringUtils.hasText(collection) ? collection : template.getCollectionName(domainType);
} }
} }
} }

View File

@@ -36,15 +36,17 @@ public class FindAndModifyOptions {
/** /**
* Static factory method to create a FindAndModifyOptions instance * Static factory method to create a FindAndModifyOptions instance
* *
* @return a new instance * @return new instance of {@link FindAndModifyOptions}.
*/ */
public static FindAndModifyOptions options() { public static FindAndModifyOptions options() {
return new FindAndModifyOptions(); return new FindAndModifyOptions();
} }
/** /**
* @param options * Create new {@link FindAndModifyOptions} based on option of given {@litearl source}.
* @return *
* @param source can be {@literal null}.
* @return new instance of {@link FindAndModifyOptions}.
* @since 2.0 * @since 2.0
*/ */
public static FindAndModifyOptions of(@Nullable FindAndModifyOptions source) { public static FindAndModifyOptions of(@Nullable FindAndModifyOptions source) {

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
/**
* Options for
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>.
* <br />
* Defaults to
* <dl>
* <dt>returnNew</dt>
* <dd>false</dd>
* <dt>upsert</dt>
* <dd>false</dd>
* </dl>
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.1
*/
public class FindAndReplaceOptions {
private boolean returnNew;
private boolean upsert;
/**
* Static factory method to create a {@link FindAndReplaceOptions} instance.
* <dl>
* <dt>returnNew</dt>
* <dd>false</dd>
* <dt>upsert</dt>
* <dd>false</dd>
* </dl>
*
* @return new instance of {@link FindAndReplaceOptions}.
*/
public static FindAndReplaceOptions options() {
return new FindAndReplaceOptions();
}
/**
* Static factory method to create a {@link FindAndReplaceOptions} instance with
* <dl>
* <dt>returnNew</dt>
* <dd>false</dd>
* <dt>upsert</dt>
* <dd>false</dd>
* </dl>
*
* @return new instance of {@link FindAndReplaceOptions}.
*/
public static FindAndReplaceOptions empty() {
return new FindAndReplaceOptions();
}
/**
* Return the replacement document.
*
* @return this.
*/
public FindAndReplaceOptions returnNew() {
this.returnNew = true;
return this;
}
/**
* Insert a new document if not exists.
*
* @return this.
*/
public FindAndReplaceOptions upsert() {
this.upsert = true;
return this;
}
/**
* Get the bit indicating to return the replacement document.
*
* @return
*/
public boolean isReturnNew() {
return returnNew;
}
/**
* Get the bit indicating if to create a new document if not exists.
*
* @return
*/
public boolean isUpsert() {
return upsert;
}
}

View File

@@ -22,4 +22,4 @@ package org.springframework.data.mongodb.core;
* @since 2.0 * @since 2.0
*/ */
public interface FluentMongoOperations extends ExecutableFindOperation, ExecutableInsertOperation, public interface FluentMongoOperations extends ExecutableFindOperation, ExecutableInsertOperation,
ExecutableUpdateOperation, ExecutableRemoveOperation, ExecutableAggregationOperation {} ExecutableUpdateOperation, ExecutableRemoveOperation, ExecutableAggregationOperation, ExecutableMapReduceOperation {}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Collection;
import java.util.List;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.util.StreamUtils;
import com.mongodb.client.model.Filters;
/**
* A MongoDB document in its mapped state. I.e. after a source document has been mapped using mapping information of the
* entity the source document was supposed to represent.
*
* @author Oliver Gierke
* @since 2.1
*/
@RequiredArgsConstructor(staticName = "of")
public class MappedDocument {
private static final String ID_FIELD = "_id";
private static final Document ID_ONLY_PROJECTION = new Document(ID_FIELD, 1);
private final @Getter Document document;
public static Document getIdOnlyProjection() {
return ID_ONLY_PROJECTION;
}
public static Document getIdIn(Collection<?> ids) {
return new Document(ID_FIELD, new Document("$in", ids));
}
public static List<Object> toIds(Collection<Document> documents) {
return documents.stream()//
.map(it -> it.get(ID_FIELD))//
.collect(StreamUtils.toUnmodifiableList());
}
public boolean hasId() {
return document.containsKey(ID_FIELD);
}
public boolean hasNonNullId() {
return hasId() && document.get(ID_FIELD) != null;
}
public Object getId() {
return document.get(ID_FIELD);
}
public <T> T getId(Class<T> type) {
return document.get(ID_FIELD, type);
}
public boolean isIdPresent(Class<?> type) {
return type.isInstance(getId());
}
public Bson getIdFilter() {
return Filters.eq(ID_FIELD, document.get(ID_FIELD));
}
public Update updateWithoutId() {
return Update.fromDocument(document, ID_FIELD);
}
}

View File

@@ -0,0 +1,272 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import lombok.Value;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.SessionAwareMethodInterceptor;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import com.mongodb.ClientSessionOptions;
import com.mongodb.DB;
import com.mongodb.WriteConcern;
import com.mongodb.client.ClientSession;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
/**
* Common base class for usage with both {@link com.mongodb.client.MongoClients} and {@link com.mongodb.MongoClient}
* defining common properties such as database name and exception translator.
* <p/>
* Not intended to be used directly.
*
* @author Christoph Strobl
* @author Mark Paluch
* @param <C> Client type.
* @since 2.1
* @see SimpleMongoDbFactory
* @see SimpleMongoClientDbFactory
*/
public abstract class MongoDbFactorySupport<C> implements MongoDbFactory {
private final C mongoClient;
private final String databaseName;
private final boolean mongoInstanceCreated;
private final PersistenceExceptionTranslator exceptionTranslator;
private @Nullable WriteConcern writeConcern;
/**
* Create a new {@link MongoDbFactorySupport} object given {@code mongoClient}, {@code databaseName},
* {@code mongoInstanceCreated} and {@link PersistenceExceptionTranslator}.
*
* @param mongoClient must not be {@literal null}.
* @param databaseName must not be {@literal null} or empty.
* @param mongoInstanceCreated {@literal true} if the client instance was created by a subclass of
* {@link MongoDbFactorySupport} to close the client on {@link #destroy()}.
* @param exceptionTranslator must not be {@literal null}.
*/
protected MongoDbFactorySupport(C mongoClient, String databaseName, boolean mongoInstanceCreated,
PersistenceExceptionTranslator exceptionTranslator) {
Assert.notNull(mongoClient, "MongoClient must not be null!");
Assert.hasText(databaseName, "Database name must not be empty!");
Assert.isTrue(databaseName.matches("[^/\\\\.$\"\\s]+"),
"Database name must not contain slashes, dots, spaces, quotes, or dollar signs!");
this.mongoClient = mongoClient;
this.databaseName = databaseName;
this.mongoInstanceCreated = mongoInstanceCreated;
this.exceptionTranslator = exceptionTranslator;
}
/**
* Configures the {@link WriteConcern} to be used on the {@link MongoDatabase} instance being created.
*
* @param writeConcern the writeConcern to set
*/
public void setWriteConcern(WriteConcern writeConcern) {
this.writeConcern = writeConcern;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getDb()
*/
public MongoDatabase getDb() throws DataAccessException {
return getDb(databaseName);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getDb(java.lang.String)
*/
@Override
public MongoDatabase getDb(String dbName) throws DataAccessException {
Assert.hasText(dbName, "Database name must not be empty!");
MongoDatabase db = doGetMongoDatabase(dbName);
if (writeConcern == null) {
return db;
}
return db.withWriteConcern(writeConcern);
}
/**
* Get the actual {@link MongoDatabase} from the client.
*
* @param dbName must not be {@literal null} or empty.
* @return
*/
protected abstract MongoDatabase doGetMongoDatabase(String dbName);
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.DisposableBean#destroy()
*/
public void destroy() throws Exception {
if (mongoInstanceCreated) {
closeClient();
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getExceptionTranslator()
*/
public PersistenceExceptionTranslator getExceptionTranslator() {
return this.exceptionTranslator;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#withSession(com.mongodb.session.Session)
*/
public MongoDbFactory withSession(ClientSession session) {
return new MongoDbFactorySupport.ClientSessionBoundMongoDbFactory(session, this);
}
/**
* Close the client instance.
*/
protected abstract void closeClient();
/**
* @return the Mongo client object.
*/
protected C getMongoClient() {
return mongoClient;
}
/**
* @return the database name.
*/
protected String getDefaultDatabaseName() {
return databaseName;
}
/**
* {@link ClientSession} bound {@link MongoDbFactory} decorating the database with a
* {@link SessionAwareMethodInterceptor}.
*
* @author Christoph Strobl
* @since 2.1
*/
@Value
static class ClientSessionBoundMongoDbFactory implements MongoDbFactory {
ClientSession session;
MongoDbFactory delegate;
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getDb()
*/
@Override
public MongoDatabase getDb() throws DataAccessException {
return proxyMongoDatabase(delegate.getDb());
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getDb(java.lang.String)
*/
@Override
public MongoDatabase getDb(String dbName) throws DataAccessException {
return proxyMongoDatabase(delegate.getDb(dbName));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getExceptionTranslator()
*/
@Override
public PersistenceExceptionTranslator getExceptionTranslator() {
return delegate.getExceptionTranslator();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getLegacyDb()
*/
@Override
public DB getLegacyDb() {
return delegate.getLegacyDb();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getSession(com.mongodb.ClientSessionOptions)
*/
@Override
public ClientSession getSession(ClientSessionOptions options) {
return delegate.getSession(options);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#withSession(com.mongodb.session.ClientSession)
*/
@Override
public MongoDbFactory withSession(ClientSession session) {
return delegate.withSession(session);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#isTransactionActive()
*/
@Override
public boolean isTransactionActive() {
return session != null && session.hasActiveTransaction();
}
private MongoDatabase proxyMongoDatabase(MongoDatabase database) {
return createProxyInstance(session, database, MongoDatabase.class);
}
private MongoDatabase proxyDatabase(com.mongodb.session.ClientSession session, MongoDatabase database) {
return createProxyInstance(session, database, MongoDatabase.class);
}
private MongoCollection<?> proxyCollection(com.mongodb.session.ClientSession session,
MongoCollection<?> collection) {
return createProxyInstance(session, collection, MongoCollection.class);
}
private <T> T createProxyInstance(com.mongodb.session.ClientSession session, T target, Class<T> targetType) {
ProxyFactory factory = new ProxyFactory();
factory.setTarget(target);
factory.setInterfaces(targetType);
factory.setOpaque(true);
factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, ClientSession.class, MongoDatabase.class,
this::proxyDatabase, MongoCollection.class, this::proxyCollection));
return targetType.cast(factory.getProxy());
}
}
}

View File

@@ -16,6 +16,7 @@
package org.springframework.data.mongodb.core; package org.springframework.data.mongodb.core;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@@ -30,6 +31,7 @@ import org.springframework.dao.PermissionDeniedDataAccessException;
import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.mongodb.BulkOperationException; import org.springframework.data.mongodb.BulkOperationException;
import org.springframework.data.mongodb.ClientSessionException; import org.springframework.data.mongodb.ClientSessionException;
import org.springframework.data.mongodb.MongoTransactionException;
import org.springframework.data.mongodb.UncategorizedMongoDbException; import org.springframework.data.mongodb.UncategorizedMongoDbException;
import org.springframework.data.mongodb.util.MongoDbErrorCodes; import org.springframework.data.mongodb.util.MongoDbErrorCodes;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
@@ -52,17 +54,17 @@ import com.mongodb.bulk.BulkWriteError;
*/ */
public class MongoExceptionTranslator implements PersistenceExceptionTranslator { public class MongoExceptionTranslator implements PersistenceExceptionTranslator {
private static final Set<String> DULICATE_KEY_EXCEPTIONS = new HashSet<String>( private static final Set<String> DUPLICATE_KEY_EXCEPTIONS = new HashSet<>(
Arrays.asList("MongoException.DuplicateKey", "DuplicateKeyException")); Arrays.asList("MongoException.DuplicateKey", "DuplicateKeyException"));
private static final Set<String> RESOURCE_FAILURE_EXCEPTIONS = new HashSet<String>( private static final Set<String> RESOURCE_FAILURE_EXCEPTIONS = new HashSet<>(
Arrays.asList("MongoException.Network", "MongoSocketException", "MongoException.CursorNotFound", Arrays.asList("MongoException.Network", "MongoSocketException", "MongoException.CursorNotFound",
"MongoCursorNotFoundException", "MongoServerSelectionException", "MongoTimeoutException")); "MongoCursorNotFoundException", "MongoServerSelectionException", "MongoTimeoutException"));
private static final Set<String> RESOURCE_USAGE_EXCEPTIONS = new HashSet<String>( private static final Set<String> RESOURCE_USAGE_EXCEPTIONS = new HashSet<>(
Arrays.asList("MongoInternalException")); Collections.singletonList("MongoInternalException"));
private static final Set<String> DATA_INTEGRETY_EXCEPTIONS = new HashSet<String>( private static final Set<String> DATA_INTEGRITY_EXCEPTIONS = new HashSet<>(
Arrays.asList("WriteConcernException", "MongoWriteException", "MongoBulkWriteException")); Arrays.asList("WriteConcernException", "MongoWriteException", "MongoBulkWriteException"));
/* /*
@@ -80,7 +82,7 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
String exception = ClassUtils.getShortName(ClassUtils.getUserClass(ex.getClass())); String exception = ClassUtils.getShortName(ClassUtils.getUserClass(ex.getClass()));
if (DULICATE_KEY_EXCEPTIONS.contains(exception)) { if (DUPLICATE_KEY_EXCEPTIONS.contains(exception)) {
return new DuplicateKeyException(ex.getMessage(), ex); return new DuplicateKeyException(ex.getMessage(), ex);
} }
@@ -92,7 +94,7 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
return new InvalidDataAccessResourceUsageException(ex.getMessage(), ex); return new InvalidDataAccessResourceUsageException(ex.getMessage(), ex);
} }
if (DATA_INTEGRETY_EXCEPTIONS.contains(exception)) { if (DATA_INTEGRITY_EXCEPTIONS.contains(exception)) {
if (ex instanceof MongoServerException) { if (ex instanceof MongoServerException) {
if (((MongoServerException) ex).getCode() == 11000) { if (((MongoServerException) ex).getCode() == 11000) {
@@ -128,6 +130,10 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
return new InvalidDataAccessApiUsageException(ex.getMessage(), ex); return new InvalidDataAccessApiUsageException(ex.getMessage(), ex);
} else if (MongoDbErrorCodes.isPermissionDeniedCode(code)) { } else if (MongoDbErrorCodes.isPermissionDeniedCode(code)) {
return new PermissionDeniedDataAccessException(ex.getMessage(), ex); return new PermissionDeniedDataAccessException(ex.getMessage(), ex);
} else if (MongoDbErrorCodes.isClientSessionFailureCode(code)) {
return new ClientSessionException(ex.getMessage(), ex);
} else if (MongoDbErrorCodes.isTransactionFailureCode(code)) {
return new MongoTransactionException(ex.getMessage(), ex);
} }
return new UncategorizedMongoDbException(ex.getMessage(), ex); return new UncategorizedMongoDbException(ex.getMessage(), ex);
} }

View File

@@ -28,6 +28,7 @@ import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOptions; import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
import org.springframework.data.mongodb.core.aggregation.AggregationResults; import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.aggregation.TypedAggregation; import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.index.IndexOperations; import org.springframework.data.mongodb.core.index.IndexOperations;
import org.springframework.data.mongodb.core.mapreduce.GroupBy; import org.springframework.data.mongodb.core.mapreduce.GroupBy;
@@ -42,14 +43,15 @@ import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.util.CloseableIterator; import org.springframework.data.util.CloseableIterator;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import com.mongodb.ClientSessionOptions; import com.mongodb.ClientSessionOptions;
import com.mongodb.Cursor; import com.mongodb.Cursor;
import com.mongodb.ReadPreference; import com.mongodb.ReadPreference;
import com.mongodb.client.ClientSession;
import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoCollection;
import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult; import com.mongodb.client.result.UpdateResult;
import com.mongodb.session.ClientSession;
/** /**
* Interface that specifies a basic set of MongoDB operations. Implemented by {@link MongoTemplate}. Not often used but * Interface that specifies a basic set of MongoDB operations. Implemented by {@link MongoTemplate}. Not often used but
@@ -381,7 +383,7 @@ public interface MongoOperations extends FluentMongoOperations {
* Returns a new {@link BulkOperations} for the given entity type and collection name. * Returns a new {@link BulkOperations} for the given entity type and collection name.
* *
* @param mode the {@link BulkMode} to use for bulk operations, must not be {@literal null}. * @param mode the {@link BulkMode} to use for bulk operations, must not be {@literal null}.
* @param entityClass the name of the entity class. Can be {@literal null}. * @param entityType the name of the entity class. Can be {@literal null}.
* @param collectionName the name of the collection to work on, must not be {@literal null} or empty. * @param collectionName the name of the collection to work on, must not be {@literal null} or empty.
* @return {@link BulkOperations} on the named collection associated with the given entity class. * @return {@link BulkOperations} on the named collection associated with the given entity class.
*/ */
@@ -420,8 +422,6 @@ public interface MongoOperations extends FluentMongoOperations {
* Execute a group operation over the entire collection. The group operation entity class should match the 'shape' of * Execute a group operation over the entire collection. The group operation entity class should match the 'shape' of
* the returned object that takes int account the initial document structure as well as any finalize functions. * the returned object that takes int account the initial document structure as well as any finalize functions.
* *
* @param criteria The criteria that restricts the row that are considered for grouping. If not specified all rows are
* considered.
* @param inputCollectionName the collection where the group operation will read from * @param inputCollectionName the collection where the group operation will read from
* @param groupBy the conditions under which the group operation will be performed, e.g. keys, initial document, * @param groupBy the conditions under which the group operation will be performed, e.g. keys, initial document,
* reduce function. * reduce function.
@@ -894,6 +894,167 @@ public interface MongoOperations extends FluentMongoOperations {
<T> T findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass, <T> T findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass,
String collectionName); String collectionName);
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement}
* document. <br />
* The collection name is derived from the {@literal replacement} type. <br />
* Options are defaulted to {@link FindAndReplaceOptions#empty()}. <br />
* <strong>NOTE:</strong> The replacement entity must not hold an {@literal id}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
* fields specification. Must not be {@literal null}.
* @param replacement the replacement document. Must not be {@literal null}.
* @return the converted object that was updated or {@literal null}, if not found.
* @since 2.1
*/
@Nullable
default <T> T findAndReplace(Query query, T replacement) {
return findAndReplace(query, replacement, FindAndReplaceOptions.empty());
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement}
* document.<br />
* Options are defaulted to {@link FindAndReplaceOptions#empty()}. <br />
* <strong>NOTE:</strong> The replacement entity must not hold an {@literal id}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
* fields specification. Must not be {@literal null}.
* @param replacement the replacement document. Must not be {@literal null}.
* @param collectionName the collection to query. Must not be {@literal null}.
* @return the converted object that was updated or {@literal null}, if not found.
* @since 2.1
*/
@Nullable
default <T> T findAndReplace(Query query, T replacement, String collectionName) {
return findAndReplace(query, replacement, FindAndReplaceOptions.empty(), collectionName);
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
* taking {@link FindAndReplaceOptions} into account.<br />
* <strong>NOTE:</strong> The replacement entity must not hold an {@literal id}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
* fields specification. Must not be {@literal null}.
* @param replacement the replacement document. Must not be {@literal null}.
* @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}.
* @return the converted object that was updated or {@literal null}, if not found. Depending on the value of
* {@link FindAndReplaceOptions#isReturnNew()} this will either be the object as it was before the update or
* as it is after the update.
* @since 2.1
*/
@Nullable
default <T> T findAndReplace(Query query, T replacement, FindAndReplaceOptions options) {
return findAndReplace(query, replacement, options, getCollectionName(ClassUtils.getUserClass(replacement)));
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
* taking {@link FindAndReplaceOptions} into account.<br />
* <strong>NOTE:</strong> The replacement entity must not hold an {@literal id}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
* fields specification. Must not be {@literal null}.
* @param replacement the replacement document. Must not be {@literal null}.
* @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}.
* @return the converted object that was updated or {@literal null}, if not found. Depending on the value of
* {@link FindAndReplaceOptions#isReturnNew()} this will either be the object as it was before the update or
* as it is after the update.
* @since 2.1
*/
@Nullable
default <T> T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, String collectionName) {
Assert.notNull(replacement, "Replacement must not be null!");
return findAndReplace(query, replacement, options, (Class<T>) ClassUtils.getUserClass(replacement), collectionName);
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
* taking {@link FindAndReplaceOptions} into account.<br />
* <strong>NOTE:</strong> The replacement entity must not hold an {@literal id}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
* fields specification. Must not be {@literal null}.
* @param replacement the replacement document. Must not be {@literal null}.
* @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}.
* @param entityType the parametrized type. Must not be {@literal null}.
* @param collectionName the collection to query. Must not be {@literal null}.
* @return the converted object that was updated or {@literal null}, if not found. Depending on the value of
* {@link FindAndReplaceOptions#isReturnNew()} this will either be the object as it was before the update or
* as it is after the update.
* @since 2.1
*/
@Nullable
default <T> T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, Class<T> entityType,
String collectionName) {
return findAndReplace(query, replacement, options, entityType, collectionName, entityType);
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
* taking {@link FindAndReplaceOptions} into account.<br />
* <strong>NOTE:</strong> The replacement entity must not hold an {@literal id}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
* fields specification. Must not be {@literal null}.
* @param replacement the replacement document. Must not be {@literal null}.
* @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}.
* @param entityType the type used for mapping the {@link Query} to domain type fields and deriving the collection
* from. Must not be {@literal null}.
* @param resultType the parametrized type projection return type. Must not be {@literal null}, use the domain type of
* {@code Object.class} instead.
* @return the converted object that was updated or {@literal null}, if not found. Depending on the value of
* {@link FindAndReplaceOptions#isReturnNew()} this will either be the object as it was before the update or
* as it is after the update.
* @since 2.1
*/
@Nullable
default <S, T> T findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class<S> entityType,
Class<T> resultType) {
return findAndReplace(query, replacement, options, entityType,
getCollectionName(ClassUtils.getUserClass(entityType)), resultType);
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
* taking {@link FindAndReplaceOptions} into account.<br />
* <strong>NOTE:</strong> The replacement entity must not hold an {@literal id}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
* fields specification. Must not be {@literal null}.
* @param replacement the replacement document. Must not be {@literal null}.
* @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}.
* @param entityType the type used for mapping the {@link Query} to domain type fields. Must not be {@literal null}.
* @param collectionName the collection to query. Must not be {@literal null}.
* @param resultType the parametrized type projection return type. Must not be {@literal null}, use the domain type of
* {@code Object.class} instead.
* @return the converted object that was updated or {@literal null}, if not found. Depending on the value of
* {@link FindAndReplaceOptions#isReturnNew()} this will either be the object as it was before the update or
* as it is after the update.
* @since 2.1
*/
@Nullable
<S, T> T findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class<S> entityType,
String collectionName, Class<T> resultType);
/** /**
* Map the results of an ad-hoc query on the collection for the entity type to a single instance of an object of the * Map the results of an ad-hoc query on the collection for the entity type to a single instance of an object of the
* specified type. The first document that matches the query is returned and also removed from the collection in the * specified type. The first document that matches the query is returned and also removed from the collection in the
@@ -980,8 +1141,9 @@ public interface MongoOperations extends FluentMongoOperations {
* Insert is used to initially store the object into the database. To update an existing object use the save method. * Insert is used to initially store the object into the database. To update an existing object use the save method.
* *
* @param objectToSave the object to store in the collection. Must not be {@literal null}. * @param objectToSave the object to store in the collection. Must not be {@literal null}.
* @return the inserted object.
*/ */
void insert(Object objectToSave); <T> T insert(T objectToSave);
/** /**
* Insert the object into the specified collection. * Insert the object into the specified collection.
@@ -993,32 +1155,36 @@ public interface MongoOperations extends FluentMongoOperations {
* *
* @param objectToSave the object to store in the collection. Must not be {@literal null}. * @param objectToSave the object to store in the collection. Must not be {@literal null}.
* @param collectionName name of the collection to store the object in. Must not be {@literal null}. * @param collectionName name of the collection to store the object in. Must not be {@literal null}.
* @return the inserted object.
*/ */
void insert(Object objectToSave, String collectionName); <T> T insert(T objectToSave, String collectionName);
/** /**
* Insert a Collection of objects into a collection in a single batch write to the database. * Insert a Collection of objects into a collection in a single batch write to the database.
* *
* @param batchToSave the batch of objects to save. Must not be {@literal null}. * @param batchToSave the batch of objects to save. Must not be {@literal null}.
* @param entityClass class that determines the collection to use. Must not be {@literal null}. * @param entityClass class that determines the collection to use. Must not be {@literal null}.
* @return the inserted objects that.
*/ */
void insert(Collection<? extends Object> batchToSave, Class<?> entityClass); <T> Collection<T> insert(Collection<? extends T> batchToSave, Class<?> entityClass);
/** /**
* Insert a batch of objects into the specified collection in a single batch write to the database. * Insert a batch of objects into the specified collection in a single batch write to the database.
* *
* @param batchToSave the list of objects to save. Must not be {@literal null}. * @param batchToSave the list of objects to save. Must not be {@literal null}.
* @param collectionName name of the collection to store the object in. Must not be {@literal null}. * @param collectionName name of the collection to store the object in. Must not be {@literal null}.
* @return the inserted objects that.
*/ */
void insert(Collection<? extends Object> batchToSave, String collectionName); <T> Collection<T> insert(Collection<? extends T> batchToSave, String collectionName);
/** /**
* Insert a mixed Collection of objects into a database collection determining the collection name to use based on the * Insert a mixed Collection of objects into a database collection determining the collection name to use based on the
* class. * class.
* *
* @param collectionToSave the list of objects to save. Must not be {@literal null}. * @param objectsToSave the list of objects to save. Must not be {@literal null}.
* @return the inserted objects.
*/ */
void insertAll(Collection<? extends Object> objectsToSave); <T> Collection<T> insertAll(Collection<? extends T> objectsToSave);
/** /**
* Save the object to the collection for the entity type of the object to save. This will perform an insert if the * Save the object to the collection for the entity type of the object to save. This will perform an insert if the
@@ -1034,8 +1200,9 @@ public interface MongoOperations extends FluentMongoOperations {
* Conversion"</a> for more details. * Conversion"</a> for more details.
* *
* @param objectToSave the object to store in the collection. Must not be {@literal null}. * @param objectToSave the object to store in the collection. Must not be {@literal null}.
* @return the saved object.
*/ */
void save(Object objectToSave); <T> T save(T objectToSave);
/** /**
* Save the object to the specified collection. This will perform an insert if the object is not already present, that * Save the object to the specified collection. This will perform an insert if the object is not already present, that
@@ -1052,8 +1219,9 @@ public interface MongoOperations extends FluentMongoOperations {
* *
* @param objectToSave the object to store in the collection. Must not be {@literal null}. * @param objectToSave the object to store in the collection. Must not be {@literal null}.
* @param collectionName name of the collection to store the object in. Must not be {@literal null}. * @param collectionName name of the collection to store the object in. Must not be {@literal null}.
* @return the saved object.
*/ */
void save(Object objectToSave, String collectionName); <T> T save(T objectToSave, String collectionName);
/** /**
* Performs an upsert. If no document is found that matches the query, a new document is created and inserted by * Performs an upsert. If no document is found that matches the query, a new document is created and inserted by

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.bson.Document;
import org.springframework.data.mapping.SimplePropertyHandler;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.ProjectionInformation;
import org.springframework.util.ClassUtils;
/**
* Common operations performed on properties of an entity like extracting fields information for projection creation.
*
* @author Christoph Strobl
* @since 2.1
*/
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
class PropertyOperations {
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
/**
* For cases where {@code fields} is {@link Document#isEmpty() empty} include only fields that are required for
* creating the projection (target) type if the {@code targetType} is a {@literal DTO projection} or a
* {@literal closed interface projection}.
*
* @param projectionFactory must not be {@literal null}.
* @param fields must not be {@literal null}.
* @param domainType must not be {@literal null}.
* @param targetType must not be {@literal null}.
* @return {@link Document} with fields to be included.
*/
Document computeFieldsForProjection(ProjectionFactory projectionFactory, Document fields, Class<?> domainType,
Class<?> targetType) {
if (!fields.isEmpty() || ClassUtils.isAssignable(domainType, targetType)) {
return fields;
}
Document projectedFields = new Document();
if (targetType.isInterface()) {
ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(targetType);
if (projectionInformation.isClosed()) {
projectionInformation.getInputProperties().forEach(it -> projectedFields.append(it.getName(), 1));
}
} else {
mappingContext.getRequiredPersistentEntity(targetType).doWithProperties(
(SimplePropertyHandler) persistentProperty -> projectedFields.append(persistentProperty.getName(), 1));
}
return projectedFields;
}
}

View File

@@ -30,5 +30,4 @@ import com.mongodb.reactivestreams.client.MongoCollection;
public interface ReactiveCollectionCallback<T> { public interface ReactiveCollectionCallback<T> {
Publisher<T> doInCollection(MongoCollection<Document> collection) throws MongoException, DataAccessException; Publisher<T> doInCollection(MongoCollection<Document> collection) throws MongoException, DataAccessException;
} }

View File

@@ -85,6 +85,23 @@ public interface ReactiveFindOperation {
*/ */
Flux<T> all(); Flux<T> all();
/**
* Get all matching elements using a {@link com.mongodb.CursorType#TailableAwait tailable cursor}. The stream will
* not be completed unless the {@link org.reactivestreams.Subscription} is
* {@link org.reactivestreams.Subscription#cancel() canceled}.
* <p />
* However, the stream may become dead, or invalid, if either the query returns no match or the cursor returns the
* document at the "end" of the collection and then the application deletes that document.
* <p />
* A stream that is no longer in use must be {@link reactor.core.Disposable#dispose()} disposed} otherwise the
* streams will linger and exhaust resources. <br/>
* <strong>NOTE:</strong> Requires a capped collection.
*
* @return the {@link Flux} emitting converted objects.
* @since 2.1
*/
Flux<T> tail();
/** /**
* Get the number of matching elements. * Get the number of matching elements.
* *

View File

@@ -169,6 +169,15 @@ class ReactiveFindOperationSupport implements ReactiveFindOperation {
return doFind(null); return doFind(null);
} }
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ReactiveFindOperation.TerminatingFind#tail()
*/
@Override
public Flux<T> tail() {
return doFind(template.new TailingQueryFindPublisherPreparer(query, domainType));
}
/* /*
* (non-Javadoc) * (non-Javadoc)
* @see org.springframework.data.mongodb.core.ReactiveFindOperation.FindWithQuery#near(org.springframework.data.mongodb.core.query.NearQuery) * @see org.springframework.data.mongodb.core.ReactiveFindOperation.FindWithQuery#near(org.springframework.data.mongodb.core.query.NearQuery)

View File

@@ -19,7 +19,8 @@ package org.springframework.data.mongodb.core;
* Stripped down interface providing access to a fluent API that specifies a basic set of reactive MongoDB operations. * Stripped down interface providing access to a fluent API that specifies a basic set of reactive MongoDB operations.
* *
* @author Mark Paluch * @author Mark Paluch
* @author Christoph Strobl
* @since 2.0 * @since 2.0
*/ */
public interface ReactiveFluentMongoOperations extends ReactiveFindOperation, ReactiveInsertOperation, public interface ReactiveFluentMongoOperations extends ReactiveFindOperation, ReactiveInsertOperation,
ReactiveUpdateOperation, ReactiveRemoveOperation, ReactiveAggregationOperation {} ReactiveUpdateOperation, ReactiveRemoveOperation, ReactiveAggregationOperation, ReactiveMapReduceOperation {}

View File

@@ -0,0 +1,199 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import reactor.core.publisher.Flux;
import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind;
import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
import org.springframework.data.mongodb.core.query.Query;
/**
* {@link ReactiveMapReduceOperation} allows creation and execution of MongoDB mapReduce operations in a fluent API
* style. The starting {@literal domainType} is used for mapping an optional {@link Query} provided via {@code matching}
* into the MongoDB specific representation. By default, the originating {@literal domainType} is also used for mapping
* back the results from the {@link org.bson.Document}. However, it is possible to define an different
* {@literal returnType} via {@code as} to mapping the result.<br />
* The collection to operate on is by default derived from the initial {@literal domainType} and can be defined there
* via {@link org.springframework.data.mongodb.core.mapping.Document}. Using {@code inCollection} allows to override the
* collection name for the execution.
*
* <pre>
* <code>
* mapReduce(Human.class)
* .map("function() { emit(this.id, this.firstname) }")
* .reduce("function(id, name) { return sum(id, name); }")
* .inCollection("star-wars")
* .as(Jedi.class)
* .matching(query(where("lastname").is("skywalker")))
* .all();
* </code>
* </pre>
*
* @author Christoph Strobl
* @since 2.1
*/
public interface ReactiveMapReduceOperation {
/**
* Start creating a mapReduce operation for the given {@literal domainType}.
*
* @param domainType must not be {@literal null}.
* @return new instance of {@link ExecutableFind}.
* @throws IllegalArgumentException if domainType is {@literal null}.
*/
<T> MapReduceWithMapFunction<T> mapReduce(Class<T> domainType);
/**
* Trigger mapReduce execution by calling one of the terminating methods.
*
* @author Christoph Strobl
* @since 2.1
*/
interface TerminatingMapReduce<T> {
/**
* Get the {@link Flux} emitting mapReduce results.
*
* @return a {@link Flux} emitting the already mapped operation results.
*/
Flux<T> all();
}
/**
* Provide the Javascript {@code function()} used to map matching documents.
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithMapFunction<T> {
/**
* Set the Javascript map {@code function()}.
*
* @param mapFunction must not be {@literal null} nor empty.
* @return new instance of {@link MapReduceWithReduceFunction}.
* @throws IllegalArgumentException if {@literal mapFunction} is {@literal null} or empty.
*/
MapReduceWithReduceFunction<T> map(String mapFunction);
}
/**
* Provide the Javascript {@code function()} used to reduce matching documents.
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithReduceFunction<T> {
/**
* Set the Javascript map {@code function()}.
*
* @param reduceFunction must not be {@literal null} nor empty.
* @return new instance of {@link ReactiveMapReduce}.
* @throws IllegalArgumentException if {@literal reduceFunction} is {@literal null} or empty.
*/
ReactiveMapReduce<T> reduce(String reduceFunction);
}
/**
* Collection override (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithCollection<T> extends MapReduceWithQuery<T> {
/**
* Explicitly set the name of the collection to perform the mapReduce operation on. <br />
* Skip this step to use the default collection derived from the domain type.
*
* @param collection must not be {@literal null} nor {@literal empty}.
* @return new instance of {@link MapReduceWithProjection}.
* @throws IllegalArgumentException if collection is {@literal null}.
*/
MapReduceWithProjection<T> inCollection(String collection);
}
/**
* Input document filter query (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithQuery<T> extends TerminatingMapReduce<T> {
/**
* Set the filter query to be used.
*
* @param query must not be {@literal null}.
* @return new instance of {@link TerminatingMapReduce}.
* @throws IllegalArgumentException if query is {@literal null}.
*/
TerminatingMapReduce<T> matching(Query query);
}
/**
* Result type override (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithProjection<T> extends MapReduceWithQuery<T> {
/**
* Define the target type fields should be mapped to. <br />
* Skip this step if you are anyway only interested in the original domain type.
*
* @param resultType must not be {@literal null}.
* @param <R> result type.
* @return new instance of {@link TerminatingMapReduce}.
* @throws IllegalArgumentException if resultType is {@literal null}.
*/
<R> MapReduceWithQuery<R> as(Class<R> resultType);
}
/**
* Additional mapReduce options (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithOptions<T> {
/**
* Set additional options to apply to the mapReduce operation.
*
* @param options must not be {@literal null}.
* @return new instance of {@link ReactiveMapReduce}.
* @throws IllegalArgumentException if options is {@literal null}.
*/
ReactiveMapReduce<T> with(MapReduceOptions options);
}
/**
* {@link ReactiveMapReduce} provides methods for constructing reactive mapReduce operations in a fluent way.
*
* @author Christoph Strobl
* @since 2.1
*/
interface ReactiveMapReduce<T> extends MapReduceWithMapFunction<T>, MapReduceWithReduceFunction<T>,
MapReduceWithCollection<T>, MapReduceWithProjection<T>, MapReduceWithOptions<T> {
}
}

View File

@@ -0,0 +1,177 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import reactor.core.publisher.Flux;
import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Implementation of {@link ReactiveMapReduceOperation}.
*
* @author Christoph Strobl
* @since 2.1
*/
@RequiredArgsConstructor
class ReactiveMapReduceOperationSupport implements ReactiveMapReduceOperation {
private static final Query ALL_QUERY = new Query();
private final @NonNull ReactiveMongoTemplate template;
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation#mapReduce(java.lang.Class)
*/
@Override
public <T> ReactiveMapReduceSupport<T> mapReduce(Class<T> domainType) {
Assert.notNull(domainType, "DomainType must not be null!");
return new ReactiveMapReduceSupport<>(template, domainType, domainType, null, ALL_QUERY, null, null, null);
}
/**
* @author Christoph Strobl
* @since 2.1
*/
static class ReactiveMapReduceSupport<T>
implements ReactiveMapReduce<T>, MapReduceWithOptions<T>, MapReduceWithCollection<T>, MapReduceWithProjection<T>,
MapReduceWithQuery<T>, MapReduceWithReduceFunction<T>, MapReduceWithMapFunction<T> {
private final ReactiveMongoTemplate template;
private final Class<?> domainType;
private final Class<T> returnType;
private final @Nullable String collection;
private final Query query;
private final @Nullable String mapFunction;
private final @Nullable String reduceFunction;
private final @Nullable MapReduceOptions options;
ReactiveMapReduceSupport(ReactiveMongoTemplate template, Class<?> domainType, Class<T> returnType,
@Nullable String collection, Query query, @Nullable String mapFunction, @Nullable String reduceFunction,
@Nullable MapReduceOptions options) {
this.template = template;
this.domainType = domainType;
this.returnType = returnType;
this.collection = collection;
this.query = query;
this.mapFunction = mapFunction;
this.reduceFunction = reduceFunction;
this.options = options;
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.TerminatingMapReduce#all()
*/
@Override
public Flux<T> all() {
return template.mapReduce(query, domainType, getCollectionName(), returnType, mapFunction, reduceFunction,
options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ReactiveMapReduceOperation.MapReduceWithCollection#inCollection(java.lang.String)
*/
@Override
public MapReduceWithProjection<T> inCollection(String collection) {
Assert.hasText(collection, "Collection name must not be null nor empty!");
return new ReactiveMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ReactiveMapReduceOperation.MapReduceWithQuery#query(org.springframework.data.mongodb.core.query.Query)
*/
@Override
public TerminatingMapReduce<T> matching(Query query) {
Assert.notNull(query, "Query must not be null!");
return new ReactiveMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ReactiveMapReduceOperation.MapReduceWithProjection#as(java.lang.Class)
*/
@Override
public <R> MapReduceWithQuery<R> as(Class<R> resultType) {
Assert.notNull(resultType, "ResultType must not be null!");
return new ReactiveMapReduceSupport<>(template, domainType, resultType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ReactiveMapReduceOperation.MapReduceWithOptions#with(org.springframework.data.mongodb.core.mapreduce.MapReduceOptions)
*/
@Override
public ReactiveMapReduce<T> with(MapReduceOptions options) {
Assert.notNull(options, "Options must not be null! Please consider empty MapReduceOptions#options() instead.");
return new ReactiveMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ReactiveMapReduceOperation.MapReduceWithMapFunction#map(java.lang.String)
*/
@Override
public MapReduceWithReduceFunction<T> map(String mapFunction) {
Assert.hasText(mapFunction, "MapFunction name must not be null nor empty!");
return new ReactiveMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ReactiveMapReduceOperation.MapReduceWithReduceFunction#reduce(java.lang.String)
*/
@Override
public ReactiveMapReduce<T> reduce(String reduceFunction) {
Assert.hasText(reduceFunction, "ReduceFunction name must not be null nor empty!");
return new ReactiveMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
private String getCollectionName() {
return StringUtils.hasText(collection) ? collection : template.determineCollectionName(domainType);
}
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import org.reactivestreams.Publisher;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
import com.mongodb.reactivestreams.client.ClientSession;
/**
* {@link ReactiveMongoContext} utilizes and enriches the Reactor {@link Context} with information potentially required
* for e.g. {@link ClientSession} handling and transactions.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.1
* @see Mono#subscriberContext()
* @see Context
*/
public class ReactiveMongoContext {
private static final Class<?> SESSION_KEY = ClientSession.class;
/**
* Gets the {@code Mono<ClientSession>} from Reactor {@link reactor.util.context.Context}. The resulting {@link Mono}
* emits the {@link ClientSession} if a session is associated with the current {@link reactor.util.context.Context
* subscriber context}. If the context does not contain a session, the resulting {@link Mono} terminates empty (i.e.
* without emitting a value).
*
* @return the {@link Mono} emitting the client session if present; otherwise the {@link Mono} terminates empty.
*/
public static Mono<ClientSession> getSession() {
return Mono.subscriberContext().filter(ctx -> ctx.hasKey(SESSION_KEY))
.flatMap(ctx -> ctx.<Mono<ClientSession>> get(SESSION_KEY));
}
/**
* Sets the {@link ClientSession} into the Reactor {@link reactor.util.context.Context}.
*
* @param context must not be {@literal null}.
* @param session must not be {@literal null}.
* @return a new {@link Context}.
* @see Context#put(Object, Object)
*/
public static Context setSession(Context context, Publisher<ClientSession> session) {
Assert.notNull(context, "Context must not be null!");
Assert.notNull(session, "Session publisher must not be null!");
return context.put(SESSION_KEY, Mono.from(session));
}
}

View File

@@ -19,7 +19,6 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
@@ -27,12 +26,14 @@ import org.bson.Document;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import org.reactivestreams.Subscription; import org.reactivestreams.Subscription;
import org.springframework.data.geo.GeoResult; import org.springframework.data.geo.GeoResult;
import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory;
import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOptions; import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
import org.springframework.data.mongodb.core.aggregation.TypedAggregation; import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.index.ReactiveIndexOperations; import org.springframework.data.mongodb.core.index.ReactiveIndexOperations;
import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.NearQuery;
@@ -40,13 +41,14 @@ import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.Update;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import com.mongodb.ClientSessionOptions; import com.mongodb.ClientSessionOptions;
import com.mongodb.ReadPreference; import com.mongodb.ReadPreference;
import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult; import com.mongodb.client.result.UpdateResult;
import com.mongodb.reactivestreams.client.ClientSession;
import com.mongodb.reactivestreams.client.MongoCollection; import com.mongodb.reactivestreams.client.MongoCollection;
import com.mongodb.session.ClientSession;
/** /**
* Interface that specifies a basic set of MongoDB operations executed in a reactive way. * Interface that specifies a basic set of MongoDB operations executed in a reactive way.
@@ -155,7 +157,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* {@link ClientSession} when done. * {@link ClientSession} when done.
* *
* @param sessionProvider must not be {@literal null}. * @param sessionProvider must not be {@literal null}.
* @return new instance of {@link SessionScoped}. Never {@literal null}. * @return new instance of {@link ReactiveSessionScoped}. Never {@literal null}.
* @since 2.1 * @since 2.1
*/ */
default ReactiveSessionScoped withSession(Supplier<ClientSession> sessionProvider) { default ReactiveSessionScoped withSession(Supplier<ClientSession> sessionProvider) {
@@ -168,40 +170,30 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/** /**
* Obtain a {@link ClientSession session} bound instance of {@link SessionScoped} binding a new {@link ClientSession} * Obtain a {@link ClientSession session} bound instance of {@link SessionScoped} binding a new {@link ClientSession}
* with given {@literal sessionOptions} to each and every command issued against MongoDB. * with given {@literal sessionOptions} to each and every command issued against MongoDB.
* <p />
* <strong>Note:</strong> It is up to the caller to manage the {@link ClientSession} lifecycle. Use
* {@link ReactiveSessionScoped#execute(ReactiveSessionCallback, Consumer)} to provide a hook for processing the
* {@link ClientSession} when done.
* *
* @param sessionOptions must not be {@literal null}. * @param sessionOptions must not be {@literal null}.
* @return new instance of {@link SessionScoped}. Never {@literal null}. * @return new instance of {@link ReactiveSessionScoped}. Never {@literal null}.
* @since 2.1 * @since 2.1
*/ */
ReactiveSessionScoped withSession(ClientSessionOptions sessionOptions); ReactiveSessionScoped withSession(ClientSessionOptions sessionOptions);
/** /**
* Obtain a {@link ClientSession session} bound instance of {@link SessionScoped} binding the {@link ClientSession} * Obtain a {@link ClientSession session} bound instance of {@link ReactiveSessionScoped} binding the
* provided by the given {@link Supplier} to each and every command issued against MongoDB. * {@link ClientSession} provided by the given {@link Publisher} to each and every command issued against MongoDB.
* <p /> * <p />
* <strong>Note:</strong> It is up to the caller to manage the {@link ClientSession} lifecycle. Use the * <strong>Note:</strong> It is up to the caller to manage the {@link ClientSession} lifecycle. Use
* {@literal onComplete} hook to potentially close the {@link ClientSession}. * {@link ReactiveSessionScoped#execute(ReactiveSessionCallback, Consumer)} to provide a hook for processing the
* {@link ClientSession} when done.
* *
* @param sessionProvider must not be {@literal null}. * @param sessionProvider must not be {@literal null}.
* @return new instance of {@link SessionScoped}. Never {@literal null}. * @return new instance of {@link ReactiveSessionScoped}. Never {@literal null}.
* @since 2.1 * @since 2.1
*/ */
default ReactiveSessionScoped withSession(Publisher<ClientSession> sessionProvider) { ReactiveSessionScoped withSession(Publisher<ClientSession> sessionProvider);
return new ReactiveSessionScoped() {
private final Mono<ClientSession> cachedSession = Mono.from(sessionProvider).cache();
@Override
public <T> Flux<T> execute(ReactiveSessionCallback<T> action, Consumer<ClientSession> doFinally) {
return cachedSession.flatMapMany(session -> {
return Flux.from(action.doInSession(ReactiveMongoOperations.this.withSession(session)))
.doFinally((signalType) -> doFinally.accept(session));
});
}
};
}
/** /**
* Obtain a {@link ClientSession} bound instance of {@link ReactiveMongoOperations}. * Obtain a {@link ClientSession} bound instance of {@link ReactiveMongoOperations}.
@@ -214,6 +206,34 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
*/ */
ReactiveMongoOperations withSession(ClientSession session); ReactiveMongoOperations withSession(ClientSession session);
/**
* Initiate a new {@link ClientSession} and obtain a {@link ClientSession session} bound instance of
* {@link ReactiveSessionScoped}. Starts the transaction and adds the {@link ClientSession} to each and every command
* issued against MongoDB.
* <p/>
* Each {@link ReactiveSessionScoped#execute(ReactiveSessionCallback) execution} initiates a new managed transaction
* that is {@link ClientSession#commitTransaction() committed} on success. Transactions are
* {@link ClientSession#abortTransaction() rolled back} upon errors.
*
* @return new instance of {@link ReactiveSessionScoped}. Never {@literal null}.
*/
ReactiveSessionScoped inTransaction();
/**
* Obtain a {@link ClientSession session} bound instance of {@link ReactiveSessionScoped}, start the transaction and
* bind the {@link ClientSession} provided by the given {@link Publisher} to each and every command issued against
* MongoDB.
* <p/>
* Each {@link ReactiveSessionScoped#execute(ReactiveSessionCallback) execution} initiates a new managed transaction
* that is {@link ClientSession#commitTransaction() committed} on success. Transactions are
* {@link ClientSession#abortTransaction() rolled back} upon errors.
*
* @param sessionProvider must not be {@literal null}.
* @return new instance of {@link ReactiveSessionScoped}. Never {@literal null}.
* @since 2.1
*/
ReactiveSessionScoped inTransaction(Publisher<ClientSession> sessionProvider);
/** /**
* Create an uncapped collection with a name based on the provided entity class. * Create an uncapped collection with a name based on the provided entity class.
* *
@@ -669,6 +689,160 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
<T> Mono<T> findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass, <T> Mono<T> findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass,
String collectionName); String collectionName);
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement}
* document. <br />
* Options are defaulted to {@link FindAndReplaceOptions#empty()}. <br />
* <strong>NOTE:</strong> The replacement entity must not hold an {@literal id}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
* fields specification. Must not be {@literal null}.
* @param replacement the replacement document. Must not be {@literal null}.
* @return the converted object that was updated or {@link Mono#empty()}, if not found.
* @since 2.1
*/
default <T> Mono<T> findAndReplace(Query query, T replacement) {
return findAndReplace(query, replacement, FindAndReplaceOptions.empty());
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement}
* document. <br />
* Options are defaulted to {@link FindAndReplaceOptions#empty()}. <br />
* <strong>NOTE:</strong> The replacement entity must not hold an {@literal id}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
* fields specification. Must not be {@literal null}.
* @param replacement the replacement document. Must not be {@literal null}.
* @param collectionName the collection to query. Must not be {@literal null}.
* @return the converted object that was updated or {@link Mono#empty()}, if not found.
* @since 2.1
*/
default <T> Mono<T> findAndReplace(Query query, T replacement, String collectionName) {
return findAndReplace(query, replacement, FindAndReplaceOptions.empty(), collectionName);
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
* taking {@link FindAndReplaceOptions} into account. <br />
* <strong>NOTE:</strong> The replacement entity must not hold an {@literal id}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
* fields specification. Must not be {@literal null}.
* @param replacement the replacement document. Must not be {@literal null}.
* @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}.
* @return the converted object that was updated or {@link Mono#empty()}, if not found. Depending on the value of
* {@link FindAndReplaceOptions#isReturnNew()} this will either be the object as it was before the update or
* as it is after the update.
* @since 2.1
*/
default <T> Mono<T> findAndReplace(Query query, T replacement, FindAndReplaceOptions options) {
return findAndReplace(query, replacement, options, getCollectionName(ClassUtils.getUserClass(replacement)));
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
* taking {@link FindAndReplaceOptions} into account. <br />
* <strong>NOTE:</strong> The replacement entity must not hold an {@literal id}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
* fields specification. Must not be {@literal null}.
* @param replacement the replacement document. Must not be {@literal null}.
* @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}.
* @return the converted object that was updated or {@link Mono#empty()}, if not found. Depending on the value of
* {@link FindAndReplaceOptions#isReturnNew()} this will either be the object as it was before the update or
* as it is after the update.
* @since 2.1
*/
default <T> Mono<T> findAndReplace(Query query, T replacement, FindAndReplaceOptions options, String collectionName) {
Assert.notNull(replacement, "Replacement must not be null!");
return findAndReplace(query, replacement, options, (Class<T>) ClassUtils.getUserClass(replacement), collectionName);
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
* taking {@link FindAndReplaceOptions} into account. <br />
* <strong>NOTE:</strong> The replacement entity must not hold an {@literal id}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
* fields specification. Must not be {@literal null}.
* @param replacement the replacement document. Must not be {@literal null}.
* @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}.
* @param entityType the parametrized type. Must not be {@literal null}.
* @param collectionName the collection to query. Must not be {@literal null}.
* @return the converted object that was updated or {@link Mono#empty()}, if not found. Depending on the value of
* {@link FindAndReplaceOptions#isReturnNew()} this will either be the object as it was before the update or
* as it is after the update.
* @since 2.1
*/
default <T> Mono<T> findAndReplace(Query query, T replacement, FindAndReplaceOptions options, Class<T> entityType,
String collectionName) {
return findAndReplace(query, replacement, options, entityType, collectionName, entityType);
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
* taking {@link FindAndReplaceOptions} into account. <br />
* <strong>NOTE:</strong> The replacement entity must not hold an {@literal id}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
* fields specification. Must not be {@literal null}.
* @param replacement the replacement document. Must not be {@literal null}.
* @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}.
* @param entityType the type used for mapping the {@link Query} to domain type fields and deriving the collection
* from. Must not be {@literal null}.
* @param resultType the parametrized type projection return type. Must not be {@literal null}, use the domain type of
* {@code Object.class} instead.
* @return the converted object that was updated or {@link Mono#empty()}, if not found. Depending on the value of
* {@link FindAndReplaceOptions#isReturnNew()} this will either be the object as it was before the update or
* as it is after the update.
* @since 2.1
*/
default <S, T> Mono<T> findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class<S> entityType,
Class<T> resultType) {
return findAndReplace(query, replacement, options, entityType,
getCollectionName(ClassUtils.getUserClass(entityType)), resultType);
}
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* to replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
* taking {@link FindAndReplaceOptions} into account. <br />
* <strong>NOTE:</strong> The replacement entity must not hold an {@literal id}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
* fields specification. Must not be {@literal null}.
* @param replacement the replacement document. Must not be {@literal null}.
* @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}.
* @param entityType the type used for mapping the {@link Query} to domain type fields and deriving the collection
* from. Must not be {@literal null}.
* @param collectionName the collection to query. Must not be {@literal null}.
* @param resultType resultType the parametrized type projection return type. Must not be {@literal null}, use the
* domain type of {@code Object.class} instead.
* @return the converted object that was updated or {@link Mono#empty()}, if not found. Depending on the value of
* {@link FindAndReplaceOptions#isReturnNew()} this will either be the object as it was before the update or
* as it is after the update.
* @since 2.1
*/
<S, T> Mono<T> findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class<S> entityType,
String collectionName, Class<T> resultType);
/** /**
* Map the results of an ad-hoc query on the collection for the entity type to a single instance of an object of the * Map the results of an ad-hoc query on the collection for the entity type to a single instance of an object of the
* specified type. The first document that matches the query is returned and also removed from the collection in the * specified type. The first document that matches the query is returned and also removed from the collection in the
@@ -753,7 +927,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* Insert is used to initially store the object into the database. To update an existing object use the save method. * Insert is used to initially store the object into the database. To update an existing object use the save method.
* *
* @param objectToSave the object to store in the collection. Must not be {@literal null}. * @param objectToSave the object to store in the collection. Must not be {@literal null}.
* @return the saved object. * @return the inserted object.
*/ */
<T> Mono<T> insert(T objectToSave); <T> Mono<T> insert(T objectToSave);
@@ -767,7 +941,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* *
* @param objectToSave the object to store in the collection. Must not be {@literal null}. * @param objectToSave the object to store in the collection. Must not be {@literal null}.
* @param collectionName name of the collection to store the object in. Must not be {@literal null}. * @param collectionName name of the collection to store the object in. Must not be {@literal null}.
* @return the saved object. * @return the inserted object.
*/ */
<T> Mono<T> insert(T objectToSave, String collectionName); <T> Mono<T> insert(T objectToSave, String collectionName);
@@ -776,7 +950,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* *
* @param batchToSave the batch of objects to save. Must not be {@literal null}. * @param batchToSave the batch of objects to save. Must not be {@literal null}.
* @param entityClass class that determines the collection to use. Must not be {@literal null}. * @param entityClass class that determines the collection to use. Must not be {@literal null}.
* @return the saved objects. * @return the inserted objects .
*/ */
<T> Flux<T> insert(Collection<? extends T> batchToSave, Class<?> entityClass); <T> Flux<T> insert(Collection<? extends T> batchToSave, Class<?> entityClass);
@@ -785,7 +959,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* *
* @param batchToSave the list of objects to save. Must not be {@literal null}. * @param batchToSave the list of objects to save. Must not be {@literal null}.
* @param collectionName name of the collection to store the object in. Must not be {@literal null}. * @param collectionName name of the collection to store the object in. Must not be {@literal null}.
* @return the saved objects. * @return the inserted objects.
*/ */
<T> Flux<T> insert(Collection<? extends T> batchToSave, String collectionName); <T> Flux<T> insert(Collection<? extends T> batchToSave, String collectionName);
@@ -813,7 +987,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* Insert is used to initially store the object into the database. To update an existing object use the save method. * Insert is used to initially store the object into the database. To update an existing object use the save method.
* *
* @param objectToSave the object to store in the collection. Must not be {@literal null}. * @param objectToSave the object to store in the collection. Must not be {@literal null}.
* @return the saved object. * @return the inserted objects.
*/ */
<T> Mono<T> insert(Mono<? extends T> objectToSave); <T> Mono<T> insert(Mono<? extends T> objectToSave);
@@ -822,7 +996,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* *
* @param batchToSave the publisher which provides objects to save. Must not be {@literal null}. * @param batchToSave the publisher which provides objects to save. Must not be {@literal null}.
* @param entityClass class that determines the collection to use. Must not be {@literal null}. * @param entityClass class that determines the collection to use. Must not be {@literal null}.
* @return the saved objects. * @return the inserted objects.
*/ */
<T> Flux<T> insertAll(Mono<? extends Collection<? extends T>> batchToSave, Class<?> entityClass); <T> Flux<T> insertAll(Mono<? extends Collection<? extends T>> batchToSave, Class<?> entityClass);
@@ -831,7 +1005,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* *
* @param batchToSave the publisher which provides objects to save. Must not be {@literal null}. * @param batchToSave the publisher which provides objects to save. Must not be {@literal null}.
* @param collectionName name of the collection to store the object in. Must not be {@literal null}. * @param collectionName name of the collection to store the object in. Must not be {@literal null}.
* @return the saved objects. * @return the inserted objects.
*/ */
<T> Flux<T> insertAll(Mono<? extends Collection<? extends T>> batchToSave, String collectionName); <T> Flux<T> insertAll(Mono<? extends Collection<? extends T>> batchToSave, String collectionName);
@@ -840,7 +1014,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* class. * class.
* *
* @param objectsToSave the publisher which provides objects to save. Must not be {@literal null}. * @param objectsToSave the publisher which provides objects to save. Must not be {@literal null}.
* @return the saved objects. * @return the inserted objects.
*/ */
<T> Flux<T> insertAll(Mono<? extends Collection<? extends T>> objectsToSave); <T> Flux<T> insertAll(Mono<? extends Collection<? extends T>> objectsToSave);
@@ -1183,7 +1357,57 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
<T> Flux<T> tail(Query query, Class<T> entityClass, String collectionName); <T> Flux<T> tail(Query query, Class<T> entityClass, String collectionName);
/** /**
* Subscribe to a MongoDB <a href="https://docs.mongodb.com/manual/changeStreams/">Change Streams</a> via the reactive * Subscribe to a MongoDB <a href="https://docs.mongodb.com/manual/changeStreams/">Change Stream</a> for all events in
* the configured default database via the reactive infrastructure. Use the optional provided {@link Aggregation} to
* filter events. The stream will not be completed unless the {@link org.reactivestreams.Subscription} is
* {@link Subscription#cancel() canceled}.
* <p />
* The {@link ChangeStreamEvent#getBody()} is mapped to the {@literal resultType} while the
* {@link ChangeStreamEvent#getRaw()} contains the unmodified payload.
* <p />
* Use {@link ChangeStreamOptions} to set arguments like {@link ChangeStreamOptions#getResumeToken() the resumseToken}
* for resuming change streams.
*
* @param options must not be {@literal null}. Use {@link ChangeStreamOptions#empty()}.
* @param targetType the result type to use.
* @param <T>
* @return the {@link Flux} emitting {@link ChangeStreamEvent events} as they arrive.
* @since 2.1
* @see ReactiveMongoDatabaseFactory#getMongoDatabase()
* @see ChangeStreamOptions#getFilter()
*/
default <T> Flux<ChangeStreamEvent<T>> changeStream(ChangeStreamOptions options, Class<T> targetType) {
return changeStream(null, options, targetType);
}
/**
* Subscribe to a MongoDB <a href="https://docs.mongodb.com/manual/changeStreams/">Change Stream</a> for all events in
* the given collection via the reactive infrastructure. Use the optional provided {@link Aggregation} to filter
* events. The stream will not be completed unless the {@link org.reactivestreams.Subscription} is
* {@link Subscription#cancel() canceled}.
* <p />
* The {@link ChangeStreamEvent#getBody()} is mapped to the {@literal resultType} while the
* {@link ChangeStreamEvent#getRaw()} contains the unmodified payload.
* <p />
* Use {@link ChangeStreamOptions} to set arguments like {@link ChangeStreamOptions#getResumeToken() the resumseToken}
* for resuming change streams.
*
* @param collectionName the collection to watch. Can be {@literal null} to watch all collections.
* @param options must not be {@literal null}. Use {@link ChangeStreamOptions#empty()}.
* @param targetType the result type to use.
* @param <T>
* @return the {@link Flux} emitting {@link ChangeStreamEvent events} as they arrive.
* @since 2.1
* @see ChangeStreamOptions#getFilter()
*/
default <T> Flux<ChangeStreamEvent<T>> changeStream(@Nullable String collectionName, ChangeStreamOptions options,
Class<T> targetType) {
return changeStream(null, collectionName, options, targetType);
}
/**
* Subscribe to a MongoDB <a href="https://docs.mongodb.com/manual/changeStreams/">Change Stream</a> via the reactive
* infrastructure. Use the optional provided {@link Aggregation} to filter events. The stream will not be completed * infrastructure. Use the optional provided {@link Aggregation} to filter events. The stream will not be completed
* unless the {@link org.reactivestreams.Subscription} is {@link Subscription#cancel() canceled}. * unless the {@link org.reactivestreams.Subscription} is {@link Subscription#cancel() canceled}.
* <p /> * <p />
@@ -1193,38 +1417,53 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* Use {@link ChangeStreamOptions} to set arguments like {@link ChangeStreamOptions#getResumeToken() the resumseToken} * Use {@link ChangeStreamOptions} to set arguments like {@link ChangeStreamOptions#getResumeToken() the resumseToken}
* for resuming change streams. * for resuming change streams.
* *
* @param filter can be {@literal null}. * @param database the database to watch. Can be {@literal null}, uses configured default if so.
* @param resultType must not be {@literal null}. * @param collectionName the collection to watch. Can be {@literal null}, watches all collections if so.
* @param options must not be {@literal null}. * @param options must not be {@literal null}. Use {@link ChangeStreamOptions#empty()}.
* @param collectionName must not be {@literal null} nor empty. * @param targetType the result type to use.
* @param <T> * @param <T>
* @return * @return the {@link Flux} emitting {@link ChangeStreamEvent events} as they arrive.
* @since 2.1 * @since 2.1
* @see ChangeStreamOptions#getFilter()
*/ */
<T> Flux<ChangeStreamEvent<T>> changeStream(@Nullable Aggregation filter, Class<T> resultType, <T> Flux<ChangeStreamEvent<T>> changeStream(@Nullable String database, @Nullable String collectionName,
ChangeStreamOptions options, String collectionName); ChangeStreamOptions options, Class<T> targetType);
/** /**
* Subscribe to a MongoDB <a href="https://docs.mongodb.com/manual/changeStreams/">Change Streams</a> via the reactive * Execute a map-reduce operation. Use {@link MapReduceOptions} to optionally specify an output collection and other
* infrastructure. Use the optional provided aggregation chain to filter events. The stream will not be completed * args.
* unless the {@link org.reactivestreams.Subscription} is {@link Subscription#cancel() canceled}.
* <p />
* The {@link ChangeStreamEvent#getBody()} is mapped to the {@literal resultType} while the
* {@link ChangeStreamEvent#getRaw()} contains the unmodified payload.
* <p />
* Use {@link ChangeStreamOptions} to set arguments like {@link ChangeStreamOptions#getResumeToken() the resumeToken}
* for resuming change streams.
* *
* @param filter can be empty, must not be {@literal null}. * @param filterQuery the selection criteria for the documents going input to the map function. Must not be
* @param resultType must not be {@literal null}. * {@literal null}.
* @param options must not be {@literal null}. * @param domainType source type used to determine the input collection name and map the filter {@link Query} against.
* @param collectionName must not be {@literal null} nor empty. * Must not be {@literal null}.
* @param <T> * @param resultType the mapping target of the operations result documents. Must not be {@literal null}.
* @return * @param mapFunction the JavaScript map function. Must not be {@literal null}.
* @param reduceFunction the JavaScript reduce function. Must not be {@literal null}.
* @param options additional options like output collection. Must not be {@literal null}.
* @return a {@link Flux} emitting the result document sequence. Never {@literal null}.
* @since 2.1 * @since 2.1
*/ */
<T> Flux<ChangeStreamEvent<T>> changeStream(List<Document> filter, Class<T> resultType, ChangeStreamOptions options, <T> Flux<T> mapReduce(Query filterQuery, Class<?> domainType, Class<T> resultType, String mapFunction,
String collectionName); String reduceFunction, MapReduceOptions options);
/**
* Execute a map-reduce operation. Use {@link MapReduceOptions} to optionally specify an output collection and other
* args.
*
* @param filterQuery the selection criteria for the documents going input to the map function. Must not be
* {@literal null}.
* @param domainType source type used to map the filter {@link Query} against. Must not be {@literal null}.
* @param inputCollectionName the input collection.
* @param resultType the mapping target of the operations result documents. Must not be {@literal null}.
* @param mapFunction the JavaScript map function. Must not be {@literal null}.
* @param reduceFunction the JavaScript reduce function. Must not be {@literal null}.
* @param options additional options like output collection. Must not be {@literal null}.
* @return a {@link Flux} emitting the result document sequence. Never {@literal null}.
* @since 2.1
*/
<T> Flux<T> mapReduce(Query filterQuery, Class<?> domainType, String inputCollectionName, Class<T> resultType,
String mapFunction, String reduceFunction, MapReduceOptions options);
/** /**
* Returns the underlying {@link MongoConverter}. * Returns the underlying {@link MongoConverter}.
@@ -1233,4 +1472,13 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
*/ */
MongoConverter getConverter(); MongoConverter getConverter();
/**
* The collection name used for the specified class by this template.
*
* @param entityClass must not be {@literal null}.
* @return
* @since 2.1
*/
String getCollectionName(Class<?> entityClass);
} }

View File

@@ -19,12 +19,12 @@ import org.reactivestreams.Publisher;
import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Query;
/** /**
* Callback interface for executing operations within a {@link com.mongodb.session.ClientSession} using reactive * Callback interface for executing operations within a {@link com.mongodb.reactivestreams.client.ClientSession} using
* infrastructure. * reactive infrastructure.
* *
* @author Christoph Strobl * @author Christoph Strobl
* @since 2.1 * @since 2.1
* @see com.mongodb.session.ClientSession * @see com.mongodb.reactivestreams.client.ClientSession
*/ */
@FunctionalInterface @FunctionalInterface
public interface ReactiveSessionCallback<T> { public interface ReactiveSessionCallback<T> {

View File

@@ -19,7 +19,7 @@ import reactor.core.publisher.Flux;
import java.util.function.Consumer; import java.util.function.Consumer;
import com.mongodb.session.ClientSession; import com.mongodb.reactivestreams.client.ClientSession;
/** /**
* Gateway interface to execute {@link ClientSession} bound operations against MongoDB via a * Gateway interface to execute {@link ClientSession} bound operations against MongoDB via a
@@ -39,7 +39,7 @@ public interface ReactiveSessionScoped {
* *
* @param action callback object that specifies the MongoDB action the callback action. Must not be {@literal null}. * @param action callback object that specifies the MongoDB action the callback action. Must not be {@literal null}.
* @param <T> return type. * @param <T> return type.
* @return a result object returned by the action. Can be {@literal null}. * @return a result object returned by the action, can be {@link Flux#empty()}.
*/ */
default <T> Flux<T> execute(ReactiveSessionCallback<T> action) { default <T> Flux<T> execute(ReactiveSessionCallback<T> action) {
return execute(action, (session) -> {}); return execute(action, (session) -> {});
@@ -56,7 +56,7 @@ public interface ReactiveSessionScoped {
* This {@link Consumer} is guaranteed to be notified in any case (successful and exceptional outcome of * This {@link Consumer} is guaranteed to be notified in any case (successful and exceptional outcome of
* {@link ReactiveSessionCallback}). * {@link ReactiveSessionCallback}).
* @param <T> return type. * @param <T> return type.
* @return a result object returned by the action. Can be {@literal null}. * @return a result object returned by the action, can be {@link Flux#empty()}.
*/ */
<T> Flux<T> execute(ReactiveSessionCallback<T> action, Consumer<ClientSession> doFinally); <T> Flux<T> execute(ReactiveSessionCallback<T> action, Consumer<ClientSession> doFinally);
} }

View File

@@ -18,12 +18,13 @@ package org.springframework.data.mongodb.core;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import com.mongodb.client.result.UpdateResult; import com.mongodb.client.result.UpdateResult;
/** /**
* {@link ReactiveUpdateOperation} allows creation and execution of reactive MongoDB update / findAndModify operations * {@link ReactiveUpdateOperation} allows creation and execution of reactive MongoDB update / findAndModify /
* in a fluent API style. <br /> * findAndReplace operations in a fluent API style. <br />
* The starting {@literal domainType} is used for mapping the {@link Query} provided via {@code matching}, as well as * The starting {@literal domainType} is used for mapping the {@link Query} provided via {@code matching}, as well as
* the {@link org.springframework.data.mongodb.core.query.Update} via {@code apply} into the MongoDB specific * the {@link org.springframework.data.mongodb.core.query.Update} via {@code apply} into the MongoDB specific
* representations. The collection to operate on is by default derived from the initial {@literal domainType} and can be * representations. The collection to operate on is by default derived from the initial {@literal domainType} and can be
@@ -68,6 +69,22 @@ public interface ReactiveUpdateOperation {
Mono<T> findAndModify(); Mono<T> findAndModify();
} }
/**
* Compose findAndReplace execution by calling one of the terminating methods.
*
* @author Mark Paluch
* @since 2.1
*/
interface TerminatingFindAndReplace<T> {
/**
* Find, replace and return the first matching document.
*
* @return {@link Mono#empty()} if nothing found. Never {@literal null}.
*/
Mono<T> findAndReplace();
}
/** /**
* Compose update execution by calling one of the terminating methods. * Compose update execution by calling one of the terminating methods.
*/ */
@@ -108,6 +125,16 @@ public interface ReactiveUpdateOperation {
* @throws IllegalArgumentException if update is {@literal null}. * @throws IllegalArgumentException if update is {@literal null}.
*/ */
TerminatingUpdate<T> apply(org.springframework.data.mongodb.core.query.Update update); TerminatingUpdate<T> apply(org.springframework.data.mongodb.core.query.Update update);
/**
* Specify {@code replacement} object.
*
* @param replacement must not be {@literal null}.
* @return new instance of {@link FindAndReplaceOptions}.
* @throws IllegalArgumentException if options is {@literal null}.
* @since 2.1
*/
FindAndReplaceWithProjection<T> replaceWith(T replacement);
} }
/** /**
@@ -157,5 +184,45 @@ public interface ReactiveUpdateOperation {
TerminatingFindAndModify<T> withOptions(FindAndModifyOptions options); TerminatingFindAndModify<T> withOptions(FindAndModifyOptions options);
} }
/**
* Define {@link FindAndReplaceOptions}.
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.1
*/
interface FindAndReplaceWithOptions<T> extends TerminatingFindAndReplace<T> {
/**
* Explicitly define {@link FindAndReplaceOptions} for the {@link Update}.
*
* @param options must not be {@literal null}.
* @return new instance of {@link FindAndReplaceOptions}.
* @throws IllegalArgumentException if options is {@literal null}.
*/
FindAndReplaceWithProjection<T> withOptions(FindAndReplaceOptions options);
}
/**
* Result type override (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface FindAndReplaceWithProjection<T> extends FindAndReplaceWithOptions<T> {
/**
* Define the target type fields should be mapped to. <br />
* Skip this step if you are anyway only interested in the original domain type.
*
* @param resultType must not be {@literal null}.
* @param <R> result type.
* @return new instance of {@link FindAndReplaceWithProjection}.
* @throws IllegalArgumentException if resultType is {@literal null}.
*/
<R> FindAndReplaceWithOptions<R> as(Class<R> resultType);
}
interface ReactiveUpdate<T> extends UpdateWithCollection<T>, UpdateWithQuery<T>, UpdateWithUpdate<T> {} interface ReactiveUpdate<T> extends UpdateWithCollection<T>, UpdateWithQuery<T>, UpdateWithUpdate<T> {}
} }

View File

@@ -22,6 +22,7 @@ import lombok.experimental.FieldDefaults;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Query;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@@ -50,20 +51,24 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
Assert.notNull(domainType, "DomainType must not be null!"); Assert.notNull(domainType, "DomainType must not be null!");
return new ReactiveUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null); return new ReactiveUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null, null, null, domainType);
} }
@RequiredArgsConstructor @RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
static class ReactiveUpdateSupport<T> static class ReactiveUpdateSupport<T>
implements ReactiveUpdate<T>, UpdateWithCollection<T>, UpdateWithQuery<T>, TerminatingUpdate<T> { implements ReactiveUpdate<T>, UpdateWithCollection<T>, UpdateWithQuery<T>, TerminatingUpdate<T>,
FindAndReplaceWithOptions<T>, FindAndReplaceWithProjection<T>, TerminatingFindAndReplace<T> {
@NonNull ReactiveMongoTemplate template; @NonNull ReactiveMongoTemplate template;
@NonNull Class<T> domainType; @NonNull Class<?> domainType;
Query query; Query query;
org.springframework.data.mongodb.core.query.Update update; org.springframework.data.mongodb.core.query.Update update;
String collection; @Nullable String collection;
FindAndModifyOptions options; @Nullable FindAndModifyOptions findAndModifyOptions;
@Nullable FindAndReplaceOptions findAndReplaceOptions;
@Nullable Object replacement;
@NonNull Class<T> targetType;
/* /*
* (non-Javadoc) * (non-Javadoc)
@@ -74,7 +79,8 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
Assert.notNull(update, "Update must not be null!"); Assert.notNull(update, "Update must not be null!");
return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, options); return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
findAndReplaceOptions, replacement, targetType);
} }
/* /*
@@ -86,7 +92,8 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
Assert.hasText(collection, "Collection must not be null nor empty!"); Assert.hasText(collection, "Collection must not be null nor empty!");
return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, options); return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
findAndReplaceOptions, replacement, targetType);
} }
/* /*
@@ -116,7 +123,18 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
String collectionName = getCollectionName(); String collectionName = getCollectionName();
return template.findAndModify(query, update, options, domainType, collectionName); return template.findAndModify(query, update, findAndModifyOptions, targetType, collectionName);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ReactiveUpdateOperation.TerminatingFindAndReplace#findAndReplace()
*/
@Override
public Mono<T> findAndReplace() {
return template.findAndReplace(query, replacement,
findAndReplaceOptions != null ? findAndReplaceOptions : new FindAndReplaceOptions(), (Class) domainType,
getCollectionName(), targetType);
} }
/* /*
@@ -128,7 +146,8 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
Assert.notNull(query, "Query must not be null!"); Assert.notNull(query, "Query must not be null!");
return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, options); return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
findAndReplaceOptions, replacement, targetType);
} }
/* /*
@@ -149,7 +168,47 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
Assert.notNull(options, "Options must not be null!"); Assert.notNull(options, "Options must not be null!");
return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, options); return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, options,
findAndReplaceOptions, replacement, targetType);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ReactiveUpdateOperation.UpdateWithUpdate#replaceWith(java.lang.Object)
*/
@Override
public FindAndReplaceWithProjection<T> replaceWith(T replacement) {
Assert.notNull(replacement, "Replacement must not be null!");
return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
findAndReplaceOptions, replacement, targetType);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ReactiveUpdateOperation.FindAndReplaceWithOptions#withOptions(org.springframework.data.mongodb.core.FindAndReplaceOptions)
*/
@Override
public FindAndReplaceWithProjection<T> withOptions(FindAndReplaceOptions options) {
Assert.notNull(options, "Options must not be null!");
return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, options,
replacement, targetType);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ReactiveUpdateOperation.FindAndReplaceWithProjection#as(java.lang.Class)
*/
@Override
public <R> FindAndReplaceWithOptions<R> as(Class<R> resultType) {
Assert.notNull(resultType, "ResultType must not be null!");
return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions,
findAndReplaceOptions, replacement, resultType);
} }
private Mono<UpdateResult> doUpdate(boolean multi, boolean upsert) { private Mono<UpdateResult> doUpdate(boolean multi, boolean upsert) {

View File

@@ -25,6 +25,7 @@ import org.springframework.lang.Nullable;
* @since 2.1 * @since 2.1
* @see com.mongodb.session.ClientSession * @see com.mongodb.session.ClientSession
*/ */
@FunctionalInterface
public interface SessionCallback<T> { public interface SessionCallback<T> {
/** /**

View File

@@ -19,7 +19,7 @@ import java.util.function.Consumer;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import com.mongodb.session.ClientSession; import com.mongodb.client.ClientSession;
/** /**
* Gateway interface to execute {@link ClientSession} bound operations against MongoDB via a {@link SessionCallback}. * Gateway interface to execute {@link ClientSession} bound operations against MongoDB via a {@link SessionCallback}.

View File

@@ -0,0 +1,116 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import org.springframework.beans.factory.DisposableBean;
import com.mongodb.ClientSessionOptions;
import com.mongodb.ConnectionString;
import com.mongodb.DB;
import com.mongodb.client.ClientSession;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoDatabase;
/**
* Factory to create {@link MongoDatabase} instances from a {@link MongoClient} instance.
*
* @author Christoph Strobl
* @since 2.1
*/
public class SimpleMongoClientDbFactory extends MongoDbFactorySupport<MongoClient> implements DisposableBean {
/**
* Creates a new {@link SimpleMongoClientDbFactory} instance for the given {@code connectionString}.
*
* @param connectionString connection coordinates for a database connection. Must contain a database name and must not
* be {@literal null} or empty.
* @see <a href="https://docs.mongodb.com/manual/reference/connection-string/">MongoDB Connection String reference</a>
*/
public SimpleMongoClientDbFactory(String connectionString) {
this(new ConnectionString(connectionString));
}
/**
* Creates a new {@link SimpleMongoClientDbFactory} instance from the given {@link MongoClient}.
*
* @param connectionString connection coordinates for a database connection. Must contain also a database name and not
* be {@literal null}.
*/
public SimpleMongoClientDbFactory(ConnectionString connectionString) {
this(MongoClients.create(connectionString), connectionString.getDatabase(), true);
}
/**
* Creates a new {@link SimpleMongoClientDbFactory} instance from the given {@link MongoClient}.
*
* @param mongoClient must not be {@literal null}.
* @param databaseName must not be {@literal null} or empty.
*/
public SimpleMongoClientDbFactory(MongoClient mongoClient, String databaseName) {
this(mongoClient, databaseName, false);
}
/**
* Creates a new {@link SimpleMongoClientDbFactory} instance from the given {@link MongoClient}.
*
* @param mongoClient must not be {@literal null}.
* @param databaseName must not be {@literal null} or empty.
* @param mongoInstanceCreated
*/
private SimpleMongoClientDbFactory(MongoClient mongoClient, String databaseName, boolean mongoInstanceCreated) {
super(mongoClient, databaseName, mongoInstanceCreated, new MongoExceptionTranslator());
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getLegacyDb()
*/
@Override
public DB getLegacyDb() {
throw new UnsupportedOperationException(String.format(
"%s does not support legacy DBObject API! Please consider using SimpleMongoDbFactory for that purpose.",
MongoClient.class));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getSession(com.mongodb.ClientSessionOptions)
*/
@Override
public ClientSession getSession(ClientSessionOptions options) {
return getMongoClient().startSession(options);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoDbFactoryBase#closeClient()
*/
@Override
protected void closeClient() {
getMongoClient().close();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoDbFactoryBase#doGetMongoDatabase(java.lang.String)
*/
@Override
protected MongoDatabase doGetMongoDatabase(String dbName) {
return getMongoClient().getDatabase(dbName);
}
}

View File

@@ -15,25 +15,16 @@
*/ */
package org.springframework.data.mongodb.core; package org.springframework.data.mongodb.core;
import lombok.Value;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.DisposableBean;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.SessionAwareMethodInterceptor;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import com.mongodb.ClientSessionOptions; import com.mongodb.ClientSessionOptions;
import com.mongodb.DB; import com.mongodb.DB;
import com.mongodb.MongoClient; import com.mongodb.MongoClient;
import com.mongodb.MongoClientURI; import com.mongodb.MongoClientURI;
import com.mongodb.WriteConcern; import com.mongodb.WriteConcern;
import com.mongodb.client.ClientSession;
import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase; import com.mongodb.client.MongoDatabase;
import com.mongodb.session.ClientSession;
/** /**
* Factory to create {@link MongoDatabase} instances from a {@link MongoClient} instance. * Factory to create {@link MongoDatabase} instances from a {@link MongoClient} instance.
@@ -45,19 +36,12 @@ import com.mongodb.session.ClientSession;
* @author George Moraitis * @author George Moraitis
* @author Mark Paluch * @author Mark Paluch
*/ */
public class SimpleMongoDbFactory implements DisposableBean, MongoDbFactory { public class SimpleMongoDbFactory extends MongoDbFactorySupport<MongoClient> implements DisposableBean {
private final MongoClient mongoClient;
private final String databaseName;
private final boolean mongoInstanceCreated;
private final PersistenceExceptionTranslator exceptionTranslator;
private @Nullable WriteConcern writeConcern;
/** /**
* Creates a new {@link SimpleMongoDbFactory} instance from the given {@link MongoClientURI}. * Creates a new {@link SimpleMongoDbFactory} instance from the given {@link MongoClientURI}.
* *
* @param uri must not be {@literal null}. * @param uri coordinates for a database connection. Must contain a database name and must not be {@literal null}.
* @since 1.7 * @since 1.7
*/ */
public SimpleMongoDbFactory(MongoClientURI uri) { public SimpleMongoDbFactory(MongoClientURI uri) {
@@ -68,7 +52,7 @@ public class SimpleMongoDbFactory implements DisposableBean, MongoDbFactory {
* Creates a new {@link SimpleMongoDbFactory} instance from the given {@link MongoClient}. * Creates a new {@link SimpleMongoDbFactory} instance from the given {@link MongoClient}.
* *
* @param mongoClient must not be {@literal null}. * @param mongoClient must not be {@literal null}.
* @param databaseName must not be {@literal null}. * @param databaseName must not be {@literal null} or empty.
* @since 1.7 * @since 1.7
*/ */
public SimpleMongoDbFactory(MongoClient mongoClient, String databaseName) { public SimpleMongoDbFactory(MongoClient mongoClient, String databaseName) {
@@ -82,76 +66,16 @@ public class SimpleMongoDbFactory implements DisposableBean, MongoDbFactory {
* @since 1.7 * @since 1.7
*/ */
private SimpleMongoDbFactory(MongoClient mongoClient, String databaseName, boolean mongoInstanceCreated) { private SimpleMongoDbFactory(MongoClient mongoClient, String databaseName, boolean mongoInstanceCreated) {
super(mongoClient, databaseName, mongoInstanceCreated, new MongoExceptionTranslator());
Assert.notNull(mongoClient, "MongoClient must not be null!");
Assert.hasText(databaseName, "Database name must not be empty!");
Assert.isTrue(databaseName.matches("[^/\\\\.$\"\\s]+"),
"Database name must not contain slashes, dots, spaces, quotes, or dollar signs!");
this.mongoClient = mongoClient;
this.databaseName = databaseName;
this.mongoInstanceCreated = mongoInstanceCreated;
this.exceptionTranslator = new MongoExceptionTranslator();
} }
/** /*
* Configures the {@link WriteConcern} to be used on the {@link DB} instance being created.
*
* @param writeConcern the writeConcern to set
*/
public void setWriteConcern(WriteConcern writeConcern) {
this.writeConcern = writeConcern;
}
/*
* (non-Javadoc) * (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getDb() * @see org.springframework.data.mongodb.MongoDbFactory#getLegacyDb()
*/ */
public MongoDatabase getDb() throws DataAccessException {
return getDb(databaseName);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getDb(java.lang.String)
*/
public MongoDatabase getDb(String dbName) throws DataAccessException {
Assert.hasText(dbName, "Database name must not be empty.");
MongoDatabase db = mongoClient.getDatabase(dbName);
if (writeConcern == null) {
return db;
}
return db.withWriteConcern(writeConcern);
}
/**
* Clean up the Mongo instance if it was created by the factory itself.
*
* @see DisposableBean#destroy()
*/
public void destroy() throws Exception {
if (mongoInstanceCreated) {
mongoClient.close();
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getExceptionTranslator()
*/
@Override
public PersistenceExceptionTranslator getExceptionTranslator() {
return this.exceptionTranslator;
}
@SuppressWarnings("deprecation")
@Override @Override
public DB getLegacyDb() { public DB getLegacyDb() {
return mongoClient.getDB(databaseName); return getMongoClient().getDB(getDefaultDatabaseName());
} }
/* /*
@@ -160,108 +84,24 @@ public class SimpleMongoDbFactory implements DisposableBean, MongoDbFactory {
*/ */
@Override @Override
public ClientSession getSession(ClientSessionOptions options) { public ClientSession getSession(ClientSessionOptions options) {
return mongoClient.startSession(options); return getMongoClient().startSession(options);
} }
/* /*
* (non-Javadoc) * (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#withSession(com.mongodb.session.ClientSession) * @see org.springframework.data.mongodb.core.MongoDbFactoryBase#closeClient()
*/ */
@Override @Override
public MongoDbFactory withSession(ClientSession session) { protected void closeClient() {
return new ClientSessionBoundMongoDbFactory(session, this); getMongoClient().close();
} }
/** /*
* {@link ClientSession} bound {@link MongoDbFactory} decorating the database with a * (non-Javadoc)
* {@link SessionAwareMethodInterceptor}. * @see org.springframework.data.mongodb.core.MongoDbFactoryBase#doGetMongoDatabase(java.lang.String)
*
* @author Christoph Strobl
* @since 2.1
*/ */
@Value @Override
static class ClientSessionBoundMongoDbFactory implements MongoDbFactory { protected MongoDatabase doGetMongoDatabase(String dbName) {
return getMongoClient().getDatabase(dbName);
ClientSession session;
MongoDbFactory delegate;
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getDb()
*/
@Override
public MongoDatabase getDb() throws DataAccessException {
return proxyMongoDatabase(delegate.getDb());
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getDb(java.lang.String)
*/
@Override
public MongoDatabase getDb(String dbName) throws DataAccessException {
return proxyMongoDatabase(delegate.getDb(dbName));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getExceptionTranslator()
*/
@Override
public PersistenceExceptionTranslator getExceptionTranslator() {
return delegate.getExceptionTranslator();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getLegacyDb()
*/
@Override
public DB getLegacyDb() {
return delegate.getLegacyDb();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getSession(com.mongodb.ClientSessionOptions)
*/
@Override
public ClientSession getSession(ClientSessionOptions options) {
return delegate.getSession(options);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#withSession(com.mongodb.session.ClientSession)
*/
@Override
public MongoDbFactory withSession(ClientSession session) {
return delegate.withSession(session);
}
private MongoDatabase proxyMongoDatabase(MongoDatabase database) {
return createProxyInstance(session, database, MongoDatabase.class);
}
private MongoDatabase proxyDatabase(ClientSession session, MongoDatabase database) {
return createProxyInstance(session, database, MongoDatabase.class);
}
private MongoCollection proxyCollection(ClientSession session, MongoCollection collection) {
return createProxyInstance(session, collection, MongoCollection.class);
}
private <T> T createProxyInstance(ClientSession session, T target, Class<T> targetType) {
ProxyFactory factory = new ProxyFactory();
factory.setTarget(target);
factory.setInterfaces(targetType);
factory.setOpaque(true);
factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, MongoDatabase.class, this::proxyDatabase,
MongoCollection.class, this::proxyCollection));
return targetType.cast(factory.getProxy());
}
} }
} }

View File

@@ -30,11 +30,11 @@ import org.springframework.util.Assert;
import com.mongodb.ClientSessionOptions; import com.mongodb.ClientSessionOptions;
import com.mongodb.ConnectionString; import com.mongodb.ConnectionString;
import com.mongodb.WriteConcern; import com.mongodb.WriteConcern;
import com.mongodb.reactivestreams.client.ClientSession;
import com.mongodb.reactivestreams.client.MongoClient; import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoClients; import com.mongodb.reactivestreams.client.MongoClients;
import com.mongodb.reactivestreams.client.MongoCollection; import com.mongodb.reactivestreams.client.MongoCollection;
import com.mongodb.reactivestreams.client.MongoDatabase; import com.mongodb.reactivestreams.client.MongoDatabase;
import com.mongodb.session.ClientSession;
/** /**
* Factory to create {@link MongoDatabase} instances from a {@link MongoClient} instance. * Factory to create {@link MongoDatabase} instances from a {@link MongoClient} instance.
@@ -215,23 +215,23 @@ public class SimpleReactiveMongoDatabaseFactory implements DisposableBean, React
return createProxyInstance(session, database, MongoDatabase.class); return createProxyInstance(session, database, MongoDatabase.class);
} }
private MongoDatabase proxyDatabase(ClientSession session, MongoDatabase database) { private MongoDatabase proxyDatabase(com.mongodb.session.ClientSession session, MongoDatabase database) {
return createProxyInstance(session, database, MongoDatabase.class); return createProxyInstance(session, database, MongoDatabase.class);
} }
private MongoCollection proxyCollection(ClientSession session, MongoCollection collection) { private MongoCollection proxyCollection(com.mongodb.session.ClientSession session, MongoCollection collection) {
return createProxyInstance(session, collection, MongoCollection.class); return createProxyInstance(session, collection, MongoCollection.class);
} }
private <T> T createProxyInstance(ClientSession session, T target, Class<T> targetType) { private <T> T createProxyInstance(com.mongodb.session.ClientSession session, T target, Class<T> targetType) {
ProxyFactory factory = new ProxyFactory(); ProxyFactory factory = new ProxyFactory();
factory.setTarget(target); factory.setTarget(target);
factory.setInterfaces(targetType); factory.setInterfaces(targetType);
factory.setOpaque(true); factory.setOpaque(true);
factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, MongoDatabase.class, this::proxyDatabase, factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, ClientSession.class, MongoDatabase.class,
MongoCollection.class, this::proxyCollection)); this::proxyDatabase, MongoCollection.class, this::proxyCollection));
return targetType.cast(factory.getProxy()); return targetType.cast(factory.getProxy());
} }

View File

@@ -23,6 +23,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import org.bson.Document; import org.bson.Document;
import org.springframework.data.mongodb.core.aggregation.Aggregation.SystemVariable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
@@ -92,6 +93,10 @@ abstract class AbstractAggregationExpression implements AggregationExpression {
return targetDocument; return targetDocument;
} }
if (value instanceof SystemVariable) {
return value.toString();
}
return value; return value;
} }

View File

@@ -41,7 +41,7 @@ class AggregationOperationRenderer {
* {@link Document} representation. * {@link Document} representation.
* *
* @param operations must not be {@literal null}. * @param operations must not be {@literal null}.
* @param context must not be {@literal null}. * @param rootContext must not be {@literal null}.
* @return the {@link List} of {@link Document}. * @return the {@link List} of {@link Document}.
*/ */
static List<Document> toDocument(List<AggregationOperation> operations, AggregationOperationContext rootContext) { static List<Document> toDocument(List<AggregationOperation> operations, AggregationOperationContext rootContext) {

View File

@@ -267,6 +267,19 @@ public class ArrayOperators {
return (usesFieldRef() ? In.arrayOf(fieldReference) : In.arrayOf(expression)).containsValue(value); return (usesFieldRef() ? In.arrayOf(fieldReference) : In.arrayOf(expression)).containsValue(value);
} }
/**
* Creates new {@link AggregationExpression} that converts the associated expression into an object.
* <strong>NOTE:</strong> Requires MongoDB 3.6 or later.
*
* @return new instance of {@link ArrayToObject}.
* @since 2.1
*/
public ArrayToObject toObject() {
return usesFieldRef() ? ArrayToObject.arrayValueOfToObject(fieldReference)
: ArrayToObject.arrayValueOfToObject(expression);
}
/** /**
* @author Christoph Strobl * @author Christoph Strobl
*/ */
@@ -1497,4 +1510,59 @@ public class ArrayOperators {
In containsValue(Object value); In containsValue(Object value);
} }
} }
/**
* {@link AggregationExpression} for {@code $arrayToObject} that transforms an array into a single document. <br />
* <strong>NOTE:</strong> Requires MongoDB 3.6 or later.
*
* @author Christoph Strobl
* @see <a href=
* "https://docs.mongodb.com/manual/reference/operator/aggregation/arrayToObject/">https://docs.mongodb.com/manual/reference/operator/aggregation/arrayToObject/</a>
* @since 2.1
*/
public static class ArrayToObject extends AbstractAggregationExpression {
private ArrayToObject(Object value) {
super(value);
}
/**
* Converts the given array (e.g. an array of two-element arrays, a field reference to an array,...) to an object.
*
* @param array must not be {@literal null}.
* @return new instance of {@link ArrayToObject}.
*/
public static ArrayToObject arrayToObject(Object array) {
return new ArrayToObject(array);
}
/**
* Converts the array pointed to by the given {@link Field field reference} to an object.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link ArrayToObject}.
*/
public static ArrayToObject arrayValueOfToObject(String fieldReference) {
return new ArrayToObject(Fields.field(fieldReference));
}
/**
* Converts the result array of the given {@link AggregationExpression expression} to an object.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link ArrayToObject}.
*/
public static ArrayToObject arrayValueOfToObject(AggregationExpression expression) {
return new ArrayToObject(expression);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AbstractAggregationExpression#getMongoMethod()
*/
@Override
protected String getMongoMethod() {
return "$arrayToObject";
}
}
} }

View File

@@ -0,0 +1,695 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.aggregation;
import java.util.Collections;
import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Gateway to {@literal convert} aggregation operations.
*
* @author Christoph Strobl
* @since 2.1
*/
public class ConvertOperators {
/**
* Take the field referenced by given {@literal fieldReference}.
*
* @param fieldReference must not be {@literal null}.
* @return
*/
public static ConvertOperatorFactory valueOf(String fieldReference) {
return new ConvertOperatorFactory(fieldReference);
}
/**
* Take the value resulting from the given {@link AggregationExpression}.
*
* @param expression must not be {@literal null}.
* @return
*/
public static ConvertOperatorFactory valueOf(AggregationExpression expression) {
return new ConvertOperatorFactory(expression);
}
/**
* @author Christoph Strobl
*/
public static class ConvertOperatorFactory {
private final @Nullable String fieldReference;
private final @Nullable AggregationExpression expression;
/**
* Creates new {@link ConvertOperatorFactory} for given {@literal fieldReference}.
*
* @param fieldReference must not be {@literal null}.
*/
public ConvertOperatorFactory(String fieldReference) {
Assert.notNull(fieldReference, "FieldReference must not be null!");
this.fieldReference = fieldReference;
this.expression = null;
}
/**
* Creates new {@link ConvertOperatorFactory} for given {@link AggregationExpression}.
*
* @param expression must not be {@literal null}.
*/
public ConvertOperatorFactory(AggregationExpression expression) {
Assert.notNull(expression, "Expression must not be null!");
this.fieldReference = null;
this.expression = expression;
}
/**
* Creates new {@link Convert aggregation expression} that takes the associated value and converts it into the type
* specified by the given {@code stringTypeIdentifier}. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @param stringTypeIdentifier must not be {@literal null}.
* @return new instance of {@link Convert}.
*/
public Convert convertTo(String stringTypeIdentifier) {
return createConvert().to(stringTypeIdentifier);
}
/**
* Creates new {@link Convert aggregation expression} that takes the associated value and converts it into the type
* specified by the given {@code numericTypeIdentifier}. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @param numericTypeIdentifier must not be {@literal null}.
* @return new instance of {@link Convert}.
*/
public Convert convertTo(int numericTypeIdentifier) {
return createConvert().to(numericTypeIdentifier);
}
/**
* Creates new {@link Convert aggregation expression} that takes the associated value and converts it into the type
* specified by the given {@link Type}. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @param type must not be {@literal null}.
* @return new instance of {@link Convert}.
*/
public Convert convertTo(Type type) {
return createConvert().to(type);
}
/**
* Creates new {@link Convert aggregation expression} that takes the associated value and converts it into the type
* specified by the value of the given {@link Field field reference}. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link Convert}.
*/
public Convert convertToTypeOf(String fieldReference) {
return createConvert().toTypeOf(fieldReference);
}
/**
* Creates new {@link Convert aggregation expression} that takes the associated value and converts it into the type
* specified by the given {@link AggregationExpression expression}. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link Convert}.
*/
public Convert convertToTypeOf(AggregationExpression expression) {
return createConvert().toTypeOf(expression);
}
/**
* Creates new {@link ToBool aggregation expression} for {@code $toBool} that converts a value to boolean. Shorthand
* for {@link #convertTo(String) #convertTo("bool")}. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @return new instance of {@link ToBool}.
*/
public ToBool convertToBoolean() {
return ToBool.toBoolean(valueObject());
}
/**
* Creates new {@link ToDate aggregation expression} for {@code $toDate} that converts a value to a date. Shorthand
* for {@link #convertTo(String) #convertTo("date")}. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @return new instance of {@link ToDate}.
*/
public ToDate convertToDate() {
return ToDate.toDate(valueObject());
}
/**
* Creates new {@link ToDecimal aggregation expression} for {@code $toDecimal} that converts a value to a decimal.
* Shorthand for {@link #convertTo(String) #convertTo("decimal")}. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @return new instance of {@link ToDecimal}.
*/
public ToDecimal convertToDecimal() {
return ToDecimal.toDecimal(valueObject());
}
/**
* Creates new {@link ToDouble aggregation expression} for {@code $toDouble} that converts a value to a decimal.
* Shorthand for {@link #convertTo(String) #convertTo("double")}. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @return new instance of {@link ToDouble}.
*/
public ToDouble convertToDouble() {
return ToDouble.toDouble(valueObject());
}
/**
* Creates new {@link ToInt aggregation expression} for {@code $toInt} that converts a value to an int. Shorthand
* for {@link #convertTo(String) #convertTo("int")}. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @return new instance of {@link ToInt}.
*/
public ToInt convertToInt() {
return ToInt.toInt(valueObject());
}
/**
* Creates new {@link ToInt aggregation expression} for {@code $toLong} that converts a value to a long. Shorthand
* for {@link #convertTo(String) #convertTo("long")}. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @return new instance of {@link ToInt}.
*/
public ToLong convertToLong() {
return ToLong.toLong(valueObject());
}
/**
* Creates new {@link ToInt aggregation expression} for {@code $toObjectId} that converts a value to a objectId. Shorthand
* for {@link #convertTo(String) #convertTo("objectId")}. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @return new instance of {@link ToInt}.
*/
public ToObjectId convertToObjectId() {
return ToObjectId.toObjectId(valueObject());
}
/**
* Creates new {@link ToInt aggregation expression} for {@code $toString} that converts a value to a string. Shorthand
* for {@link #convertTo(String) #convertTo("string")}. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @return new instance of {@link ToInt}.
*/
public ToString convertToString() {
return ToString.toString(valueObject());
}
private Convert createConvert() {
return usesFieldRef() ? Convert.convertValueOf(fieldReference) : Convert.convertValueOf(expression);
}
private Object valueObject() {
return usesFieldRef() ? Fields.field(fieldReference) : expression;
}
private boolean usesFieldRef() {
return fieldReference != null;
}
}
/**
* {@link AggregationExpression} for {@code $convert} that converts a value to a specified type. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @author Christoph Strobl
* @see <a href=
* "https://docs.mongodb.com/manual/reference/operator/aggregation/convert/">https://docs.mongodb.com/manual/reference/operator/aggregation/convert/</a>
* @since 2.1
*/
public static class Convert extends AbstractAggregationExpression {
private Convert(Object value) {
super(value);
}
/**
* Creates new {@link Convert} using the given value for the {@literal input} attribute.
*
* @param value must not be {@literal null}.
* @return new instance of {@link Convert}.
*/
public static Convert convertValue(Object value) {
return new Convert(Collections.singletonMap("input", value));
}
/**
* Creates new {@link Convert} using the value of the provided {@link Field fieldReference} as {@literal input}
* value.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link Convert}.
*/
public static Convert convertValueOf(String fieldReference) {
return convertValue(Fields.field(fieldReference));
}
/**
* Creates new {@link Convert} using the result of the provided {@link AggregationExpression expression} as
* {@literal input} value.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link Convert}.
*/
public static Convert convertValueOf(AggregationExpression expression) {
return convertValue(expression);
}
/**
* Specify the conversion target type via its {@link String} representation.
* <ul>
* <li>double</li>
* <li>string</li>
* <li>objectId</li>
* <li>bool</li>
* <li>date</li>
* <li>int</li>
* <li>long</li>
* <li>decimal</li>
* </ul>
*
* @param stringTypeIdentifier must not be {@literal null}.
* @return new instance of {@link Convert}.
*/
public Convert to(String stringTypeIdentifier) {
return new Convert(append("to", stringTypeIdentifier));
}
/**
* Specify the conversion target type via its numeric representation.
* <dl>
* <dt>1</dt>
* <dd>double</dd>
* <dt>2</dt>
* <dd>string</li>
* <dt>7</dt>
* <dd>objectId</li>
* <dt>8</dt>
* <dd>bool</dd>
* <dt>9</dt>
* <dd>date</dd>
* <dt>16</dt>
* <dd>int</dd>
* <dt>18</dt>
* <dd>long</dd>
* <dt>19</dt>
* <dd>decimal</dd>
* </dl>
*
* @param numericTypeIdentifier must not be {@literal null}.
* @return new instance of {@link Convert}.
*/
public Convert to(int numericTypeIdentifier) {
return new Convert(append("to", numericTypeIdentifier));
}
/**
* Specify the conversion target type.
*
* @param type must not be {@literal null}.
* @return new instance of {@link Convert}.
*/
public Convert to(Type type) {
String typeString = Type.BOOLEAN.equals(type) ? "bool" : type.value().toString();
return to(typeString);
}
/**
* Specify the conversion target type via the value of the given field.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link Convert}.
*/
public Convert toTypeOf(String fieldReference) {
return new Convert(append("to", Fields.field(fieldReference)));
}
/**
* Specify the conversion target type via the value of the given {@link AggregationExpression expression}.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link Convert}.
*/
public Convert toTypeOf(AggregationExpression expression) {
return new Convert(append("to", expression));
}
/**
* Optionally specify the value to return on encountering an error during conversion.
*
* @param value must not be {@literal null}.
* @return new instance of {@link Convert}.
*/
public Convert onErrorReturn(Object value) {
return new Convert(append("onError", value));
}
/**
* Optionally specify the field holding the value to return on encountering an error during conversion.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link Convert}.
*/
public Convert onErrorReturnValueOf(String fieldReference) {
return onErrorReturn(Fields.field(fieldReference));
}
/**
* Optionally specify the expression to evaluate and return on encountering an error during conversion.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link Convert}.
*/
public Convert onErrorReturnValueOf(AggregationExpression expression) {
return onErrorReturn(expression);
}
/**
* Optionally specify the value to return when the input is {@literal null} or missing.
*
* @param value must not be {@literal null}.
* @return new instance of {@link Convert}.
*/
public Convert onNullReturn(Object value) {
return new Convert(append("onNull", value));
}
/**
* Optionally specify the field holding the value to return when the input is {@literal null} or missing.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link Convert}.
*/
public Convert onNullReturnValueOf(String fieldReference) {
return onNullReturn(Fields.field(fieldReference));
}
/**
* Optionally specify the expression to evaluate and return when the input is {@literal null} or missing.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link Convert}.
*/
public Convert onNullReturnValueOf(AggregationExpression expression) {
return onNullReturn(expression);
}
@Override
protected String getMongoMethod() {
return "$convert";
}
}
/**
* {@link AggregationExpression} for {@code $toBool} that converts a value to {@literal boolean}. Shorthand for
* {@link Convert#to(String) Convert#to("bool")}. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @author Christoph Strobl
* @see <a href=
* "https://docs.mongodb.com/manual/reference/operator/aggregation/toBool/">https://docs.mongodb.com/manual/reference/operator/aggregation/toBool/</a>
* @since 2.1
*/
public static class ToBool extends AbstractAggregationExpression {
private ToBool(Object value) {
super(value);
}
/**
* Creates new {@link ToBool} using the given value as input.
*
* @param value must not be {@literal null}.
* @return new instance of {@link ToBool}.
*/
public static ToBool toBoolean(Object value) {
return new ToBool(value);
}
@Override
protected String getMongoMethod() {
return "$toBool";
}
}
/**
* {@link AggregationExpression} for {@code $toDate} that converts a value to {@literal date}. Shorthand for
* {@link Convert#to(String) Convert#to("date")}. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @author Christoph Strobl
* @see <a href=
* "https://docs.mongodb.com/manual/reference/operator/aggregation/toDate/">https://docs.mongodb.com/manual/reference/operator/aggregation/toDate/</a>
* @since 2.1
*/
public static class ToDate extends AbstractAggregationExpression {
private ToDate(Object value) {
super(value);
}
/**
* Creates new {@link ToDate} using the given value as input.
*
* @param value must not be {@literal null}.
* @return new instance of {@link ToDate}.
*/
public static ToDate toDate(Object value) {
return new ToDate(value);
}
@Override
protected String getMongoMethod() {
return "$toDate";
}
}
/**
* {@link AggregationExpression} for {@code $toDecimal} that converts a value to {@literal decimal}. Shorthand for
* {@link Convert#to(String) Convert#to("decimal")}. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @author Christoph Strobl
* @see <a href=
* "https://docs.mongodb.com/manual/reference/operator/aggregation/toDecimal/">https://docs.mongodb.com/manual/reference/operator/aggregation/toDecimal/</a>
* @since 2.1
*/
public static class ToDecimal extends AbstractAggregationExpression {
private ToDecimal(Object value) {
super(value);
}
/**
* Creates new {@link ToDecimal} using the given value as input.
*
* @param value must not be {@literal null}.
* @return new instance of {@link ToDecimal}.
*/
public static ToDecimal toDecimal(Object value) {
return new ToDecimal(value);
}
@Override
protected String getMongoMethod() {
return "$toDecimal";
}
}
/**
* {@link AggregationExpression} for {@code $toDouble} that converts a value to {@literal double}. Shorthand for
* {@link Convert#to(String) Convert#to("double")}. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @author Christoph Strobl
* @see <a href=
* "https://docs.mongodb.com/manual/reference/operator/aggregation/toDouble/">https://docs.mongodb.com/manual/reference/operator/aggregation/toDouble/</a>
* @since 2.1
*/
public static class ToDouble extends AbstractAggregationExpression {
private ToDouble(Object value) {
super(value);
}
/**
* Creates new {@link ToDouble} using the given value as input.
*
* @param value must not be {@literal null}.
* @return new instance of {@link ToDouble}.
*/
public static ToDouble toDouble(Object value) {
return new ToDouble(value);
}
@Override
protected String getMongoMethod() {
return "$toDouble";
}
}
/**
* {@link AggregationExpression} for {@code $toInt} that converts a value to {@literal integer}. Shorthand for
* {@link Convert#to(String) Convert#to("int")}. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @author Christoph Strobl
* @see <a href=
* "https://docs.mongodb.com/manual/reference/operator/aggregation/toInt/">https://docs.mongodb.com/manual/reference/operator/aggregation/toInt/</a>
* @since 2.1
*/
public static class ToInt extends AbstractAggregationExpression {
private ToInt(Object value) {
super(value);
}
/**
* Creates new {@link ToInt} using the given value as input.
*
* @param value must not be {@literal null}.
* @return new instance of {@link ToInt}.
*/
public static ToInt toInt(Object value) {
return new ToInt(value);
}
@Override
protected String getMongoMethod() {
return "$toInt";
}
}
/**
* {@link AggregationExpression} for {@code $toLong} that converts a value to {@literal long}. Shorthand for
* {@link Convert#to(String) Convert#to("long")}. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @author Christoph Strobl
* @see <a href=
* "https://docs.mongodb.com/manual/reference/operator/aggregation/toLong/">https://docs.mongodb.com/manual/reference/operator/aggregation/toLong/</a>
* @since 2.1
*/
public static class ToLong extends AbstractAggregationExpression {
private ToLong(Object value) {
super(value);
}
/**
* Creates new {@link ToLong} using the given value as input.
*
* @param value must not be {@literal null}.
* @return new instance of {@link ToLong}.
*/
public static ToLong toLong(Object value) {
return new ToLong(value);
}
@Override
protected String getMongoMethod() {
return "$toLong";
}
}
/**
* {@link AggregationExpression} for {@code $toObjectId} that converts a value to {@literal objectId}. Shorthand for
* {@link Convert#to(String) Convert#to("objectId")}. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @author Christoph Strobl
* @see <a href=
* "https://docs.mongodb.com/manual/reference/operator/aggregation/toObjectId/">https://docs.mongodb.com/manual/reference/operator/aggregation/toObjectId/</a>
* @since 2.1
*/
public static class ToObjectId extends AbstractAggregationExpression {
private ToObjectId(Object value) {
super(value);
}
/**
* Creates new {@link ToObjectId} using the given value as input.
*
* @param value must not be {@literal null}.
* @return new instance of {@link ToObjectId}.
*/
public static ToObjectId toObjectId(Object value) {
return new ToObjectId(value);
}
@Override
protected String getMongoMethod() {
return "$toObjectId";
}
}
/**
* {@link AggregationExpression} for {@code $toString} that converts a value to {@literal string}. Shorthand for
* {@link Convert#to(String) Convert#to("string")}. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @author Christoph Strobl
* @see <a href=
* "https://docs.mongodb.com/manual/reference/operator/aggregation/toString/">https://docs.mongodb.com/manual/reference/operator/aggregation/toString/</a>
* @since 2.1
*/
public static class ToString extends AbstractAggregationExpression {
private ToString(Object value) {
super(value);
}
/**
* Creates new {@link ToString} using the given value as input.
*
* @param value must not be {@literal null}.
* @return new instance of {@link ToString}.
*/
public static ToString toString(Object value) {
return new ToString(value);
}
@Override
protected String getMongoMethod() {
return "$toString";
}
}
}

View File

@@ -22,6 +22,7 @@ import java.util.Map;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/** /**
* Gateway to {@literal Date} aggregation operations. * Gateway to {@literal Date} aggregation operations.
@@ -178,7 +179,7 @@ public class DateOperators {
* Create a {@link Timezone} for the {@link AggregationExpression} resulting in the Olson Timezone Identifier or UTC * Create a {@link Timezone} for the {@link AggregationExpression} resulting in the Olson Timezone Identifier or UTC
* Offset. * Offset.
* *
* @param value the {@link AggregationExpression} resulting in the timezone. * @param expression the {@link AggregationExpression} resulting in the timezone.
* @return new instance of {@link Timezone}. * @return new instance of {@link Timezone}.
*/ */
public static Timezone ofExpression(AggregationExpression expression) { public static Timezone ofExpression(AggregationExpression expression) {
@@ -380,6 +381,17 @@ public class DateOperators {
return applyTimezone(DateToString.dateToString(dateReference()).toString(format), timezone); return applyTimezone(DateToString.dateToString(dateReference()).toString(format), timezone);
} }
/**
* Creates new {@link AggregationExpression} that converts a date object to a string according to the server default
* format.
*
* @return new instance of {@link DateToString}.
* @since 2.1
*/
public DateToString toStringWithDefaultFormat() {
return applyTimezone(DateToString.dateToString(dateReference()).defaultFormat(), timezone);
}
/** /**
* Creates new {@link AggregationExpression} that returns the weekday number in ISO 8601-2018 format, ranging from 1 * Creates new {@link AggregationExpression} that returns the weekday number in ISO 8601-2018 format, ranging from 1
* (for Monday) to 7 (for Sunday). * (for Monday) to 7 (for Sunday).
@@ -1352,6 +1364,11 @@ public class DateOperators {
Assert.notNull(format, "Format must not be null!"); Assert.notNull(format, "Format must not be null!");
return new DateToString(argumentMap(value, format, Timezone.none())); return new DateToString(argumentMap(value, format, Timezone.none()));
} }
@Override
public DateToString defaultFormat() {
return new DateToString(argumentMap(value, null, Timezone.none()));
}
}; };
} }
@@ -1392,7 +1409,43 @@ public class DateOperators {
public DateToString withTimezone(Timezone timezone) { public DateToString withTimezone(Timezone timezone) {
Assert.notNull(timezone, "Timezone must not be null."); Assert.notNull(timezone, "Timezone must not be null.");
return new DateToString(argumentMap(get("date"), get("format"), timezone)); return new DateToString(append("timezone", timezone));
}
/**
* Optionally specify the value to return when the date is {@literal null} or missing. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @param value must not be {@literal null}.
* @return new instance of {@link DateToString}.
* @since 2.1
*/
public DateToString onNullReturn(Object value) {
return new DateToString(append("onNull", value));
}
/**
* Optionally specify the field holding the value to return when the date is {@literal null} or missing. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link DateToString}.
* @since 2.1
*/
public DateToString onNullReturnValueOf(String fieldReference) {
return onNullReturn(Fields.field(fieldReference));
}
/**
* Optionally specify the expression to evaluate and return when the date is {@literal null} or missing. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link DateToString}.
* @since 2.1
*/
public DateToString onNullReturnValueOf(AggregationExpression expression) {
return onNullReturn(expression);
} }
@Override @Override
@@ -1400,10 +1453,14 @@ public class DateOperators {
return "$dateToString"; return "$dateToString";
} }
private static java.util.Map<String, Object> argumentMap(Object date, String format, Timezone timezone) { private static java.util.Map<String, Object> argumentMap(Object date, @Nullable String format, Timezone timezone) {
java.util.Map<String, Object> args = new LinkedHashMap<>(2);
if (StringUtils.hasText(format)) {
args.put("format", format);
}
java.util.Map<String, Object> args = new LinkedHashMap<String, Object>(2);
args.put("format", format);
args.put("date", date); args.put("date", date);
if (!ObjectUtils.nullSafeEquals(timezone, Timezone.none())) { if (!ObjectUtils.nullSafeEquals(timezone, Timezone.none())) {
@@ -1412,6 +1469,25 @@ public class DateOperators {
return args; return args;
} }
protected java.util.Map<String, Object> append(String key, Object value) {
java.util.Map<String, Object> clone = new LinkedHashMap<>(argumentMap());
if (value instanceof Timezone) {
if (ObjectUtils.nullSafeEquals(value, Timezone.none())) {
clone.remove("timezone");
} else {
clone.put("timezone", ((Timezone) value).value);
}
} else {
clone.put(key, value);
}
return clone;
}
public interface FormatBuilder { public interface FormatBuilder {
/** /**
@@ -1421,6 +1497,16 @@ public class DateOperators {
* @return * @return
*/ */
DateToString toString(String format); DateToString toString(String format);
/**
* Creates new {@link DateToString} using the server default string format ({@code %Y-%m-%dT%H:%M:%S.%LZ}) for
* dates. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @return new instance of {@link DateToString}.
* @since 2.1
*/
DateToString defaultFormat();
} }
} }
@@ -2269,6 +2355,20 @@ public class DateOperators {
return new DateFromString(appendTimezone(argumentMap(), timezone)); return new DateFromString(appendTimezone(argumentMap(), timezone));
} }
/**
* Optionally set the date format to use. If not specified {@code %Y-%m-%dT%H:%M:%S.%LZ} is used.<br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @param format must not be {@literal null}.
* @return new instance of {@link DateFromString}.
* @throws IllegalArgumentException if given {@literal format} is {@literal null}.
*/
public DateFromString withFormat(String format) {
Assert.notNull(format, "Format must not be null!");
return new DateFromString(append("format", format));
}
@Override @Override
protected String getMongoMethod() { protected String getMongoMethod() {
return "$dateFromString"; return "$dateFromString";

View File

@@ -17,7 +17,9 @@ package org.springframework.data.mongodb.core.aggregation;
import org.bson.Document; import org.bson.Document;
import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/** /**
* Represents a {@code geoNear} aggregation operation. * Represents a {@code geoNear} aggregation operation.
@@ -28,26 +30,55 @@ import org.springframework.util.Assert;
* @author Thomas Darimont * @author Thomas Darimont
* @author Christoph Strobl * @author Christoph Strobl
* @since 1.3 * @since 1.3
* @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/geoNear/">MongoDB Aggregation Framework:
* $geoNear</a>
*/ */
public class GeoNearOperation implements AggregationOperation { public class GeoNearOperation implements AggregationOperation {
private final NearQuery nearQuery; private final NearQuery nearQuery;
private final String distanceField; private final String distanceField;
private final @Nullable String indexKey;
/** /**
* Creates a new {@link GeoNearOperation} from the given {@link NearQuery} and the given distance field. The * Creates a new {@link GeoNearOperation} from the given {@link NearQuery} and the given distance field. The
* {@code distanceField} defines output field that contains the calculated distance. * {@code distanceField} defines output field that contains the calculated distance.
* *
* @param query must not be {@literal null}. * @param nearQuery must not be {@literal null}.
* @param distanceField must not be {@literal null}. * @param distanceField must not be {@literal null}.
*/ */
public GeoNearOperation(NearQuery nearQuery, String distanceField) { public GeoNearOperation(NearQuery nearQuery, String distanceField) {
this(nearQuery, distanceField, null);
}
/**
* Creates a new {@link GeoNearOperation} from the given {@link NearQuery} and the given distance field. The
* {@code distanceField} defines output field that contains the calculated distance.
*
* @param nearQuery must not be {@literal null}.
* @param distanceField must not be {@literal null}.
* @param indexKey can be {@literal null};
* @since 2.1
*/
private GeoNearOperation(NearQuery nearQuery, String distanceField, @Nullable String indexKey) {
Assert.notNull(nearQuery, "NearQuery must not be null."); Assert.notNull(nearQuery, "NearQuery must not be null.");
Assert.hasLength(distanceField, "Distance field must not be null or empty."); Assert.hasLength(distanceField, "Distance field must not be null or empty.");
this.nearQuery = nearQuery; this.nearQuery = nearQuery;
this.distanceField = distanceField; this.distanceField = distanceField;
this.indexKey = indexKey;
}
/**
* Optionally specify the geospatial index to use via the field to use in the calculation. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @param key the geospatial index field to use when calculating the distance.
* @return new instance of {@link GeoNearOperation}.
* @since 2.1
*/
public GeoNearOperation useIndex(String key) {
return new GeoNearOperation(nearQuery, distanceField, key);
} }
/* /*
@@ -60,6 +91,10 @@ public class GeoNearOperation implements AggregationOperation {
Document command = context.getMappedObject(nearQuery.toDocument()); Document command = context.getMappedObject(nearQuery.toDocument());
command.put("distanceField", distanceField); command.put("distanceField", distanceField);
if (StringUtils.hasText(indexKey)) {
command.put("key", indexKey);
}
return new Document("$geoNear", command); return new Document("$geoNear", command);
} }
} }

View File

@@ -103,8 +103,8 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
graphLookup.put("startWith", mappedStartWith.size() == 1 ? mappedStartWith.iterator().next() : mappedStartWith); graphLookup.put("startWith", mappedStartWith.size() == 1 ? mappedStartWith.iterator().next() : mappedStartWith);
graphLookup.put("connectFromField", connectFrom.getName()); graphLookup.put("connectFromField", connectFrom.getTarget());
graphLookup.put("connectToField", connectTo.getName()); graphLookup.put("connectToField", connectTo.getTarget());
graphLookup.put("as", as.getName()); graphLookup.put("as", as.getName());
if (maxDepth != null) { if (maxDepth != null) {
@@ -112,7 +112,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
} }
if (depthField != null) { if (depthField != null) {
graphLookup.put("depthField", depthField.getName()); graphLookup.put("depthField", depthField.getTarget());
} }
if (restrictSearchWithMatch != null) { if (restrictSearchWithMatch != null) {

View File

@@ -0,0 +1,298 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.aggregation;
import java.util.Arrays;
import java.util.Collection;
import org.bson.Document;
import org.springframework.util.Assert;
/**
* Gateway for
* <a href="https://docs.mongodb.com/manual/meta/aggregation-quick-reference/#object-expression-operators">object
* expression operators</a>.
*
* @author Christoph Strobl
* @since 2.1
*/
public class ObjectOperators {
/**
* Take the value referenced by given {@literal fieldReference}.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link ObjectOperatorFactory}.
*/
public static ObjectOperatorFactory valueOf(String fieldReference) {
return new ObjectOperatorFactory(Fields.field(fieldReference));
}
/**
* Take the value provided by the given {@link AggregationExpression}.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link ObjectOperatorFactory}.
*/
public static ObjectOperatorFactory valueOf(AggregationExpression expression) {
return new ObjectOperatorFactory(expression);
}
/**
* @author Christoph Strobl
*/
public static class ObjectOperatorFactory {
private final Object value;
/**
* Creates new {@link ObjectOperatorFactory} for given {@literal value}.
*
* @param value must not be {@literal null}.
*/
public ObjectOperatorFactory(Object value) {
Assert.notNull(value, "Value must not be null!");
this.value = value;
}
/**
* Creates new {@link MergeObjects aggregation expression} that takes the associated value and uses
* {@literal $mergeObjects} as an accumulator within the {@literal $group} stage. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @return new instance of {@link MergeObjects}.
*/
public MergeObjects merge() {
return MergeObjects.merge(value);
}
/**
* Creates new {@link MergeObjects aggregation expression} that takes the associated value and combines it with the
* given values (documents or mapped objects) into a single document. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @return new instance of {@link MergeObjects}.
*/
public MergeObjects mergeWith(Object... values) {
return merge().mergeWith(values);
}
/**
* Creates new {@link MergeObjects aggregation expression} that takes the associated value and combines it with the
* values of the given {@link Field field references} into a single document. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @return new instance of {@link MergeObjects}.
*/
public MergeObjects mergeWithValuesOf(String... fieldReferences) {
return merge().mergeWithValuesOf(fieldReferences);
}
/**
* Creates new {@link MergeObjects aggregation expression} that takes the associated value and combines it with the
* result values of the given {@link Aggregation expressions} into a single document. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @return new instance of {@link MergeObjects}.
*/
public MergeObjects mergeWithValuesOf(AggregationExpression... expression) {
return merge().mergeWithValuesOf(expression);
}
/**
* Creates new {@link ObjectToArray aggregation expression} that takes the associated value and converts it to an
* array of {@link Document documents} that contain two fields {@literal k} and {@literal v} each. <br />
* <strong>NOTE:</strong> Requires MongoDB 3.6 or later.
*
* @since 2.1
*/
public ObjectToArray toArray() {
return ObjectToArray.toArray(value);
}
}
/**
* {@link AggregationExpression} for {@code $mergeObjects} that combines multiple documents into a single document.
* <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @author Christoph Strobl
* @see <a href=
* "https://docs.mongodb.com/manual/reference/operator/aggregation/mergeObjects/">https://docs.mongodb.com/manual/reference/operator/aggregation/mergeObjects/</a>
* @since 2.1
*/
public static class MergeObjects extends AbstractAggregationExpression {
private MergeObjects(Object value) {
super(value);
}
/**
* Creates new {@link MergeObjects aggregation expression} that takes given values and combines them into a single
* document. <br />
*
* @param values must not be {@literal null}.
* @return new instance of {@link MergeObjects}.
*/
public static MergeObjects merge(Object... values) {
return new MergeObjects(Arrays.asList(values));
}
/**
* Creates new {@link MergeObjects aggregation expression} that takes the given {@link Field field references} and
* combines them into a single document.
*
* @param fieldReferences must not be {@literal null}.
* @return new instance of {@link MergeObjects}.
*/
public static MergeObjects mergeValuesOf(String... fieldReferences) {
return merge(Arrays.stream(fieldReferences).map(Fields::field).toArray());
}
/**
* Creates new {@link MergeObjects aggregation expression} that takes the result of the given {@link Aggregation
* expressions} and combines them into a single document.
*
* @param expressions must not be {@literal null}.
* @return new instance of {@link MergeObjects}.
*/
public static MergeObjects mergeValuesOf(AggregationExpression... expressions) {
return merge(expressions);
}
/**
* Creates new {@link MergeObjects aggregation expression} by adding the given {@link Field field references}.
*
* @param fieldReferences must not be {@literal null}.
* @return new instance of {@link MergeObjects}.
*/
public MergeObjects mergeWithValuesOf(String... fieldReferences) {
return mergeWith(Arrays.stream(fieldReferences).map(Fields::field).toArray());
}
/**
* Creates new {@link MergeObjects aggregation expression} by adding the given {@link AggregationExpression
* expressions}.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link MergeObjects}.
*/
public MergeObjects mergeWithValuesOf(AggregationExpression... expression) {
return mergeWith(expression);
}
/**
* Creates new {@link MergeObjects aggregation expression} by adding the given values (documents or mapped objects).
*
* @param values must not be {@literal null}.
* @return new instance of {@link MergeObjects}.
*/
public MergeObjects mergeWith(Object... values) {
return new MergeObjects(append(Arrays.asList(values)));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AbstractAggregationExpression#toDocument(java.lang.Object, org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
*/
@Override
public Document toDocument(Object value, AggregationOperationContext context) {
return super.toDocument(potentiallyExtractSingleValue(value), context);
}
@SuppressWarnings("unchecked")
private Object potentiallyExtractSingleValue(Object value) {
if (value instanceof Collection) {
Collection<Object> collection = ((Collection<Object>) value);
if (collection.size() == 1) {
return collection.iterator().next();
}
}
return value;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AbstractAggregationExpression#getMongoMethod()
*/
@Override
protected String getMongoMethod() {
return "$mergeObjects";
}
}
/**
* {@link AggregationExpression} for {@code $objectToArray} that converts a document to an array of {@link Document
* documents} that each contains two fields {@literal k} and {@literal v}. <br />
* <strong>NOTE:</strong> Requires MongoDB 3.6 or later.
*
* @author Christoph Strobl
* @see <a href=
* "https://docs.mongodb.com/manual/reference/operator/aggregation/objectToArray/">https://docs.mongodb.com/manual/reference/operator/aggregation/objectToArray/</a>
* @since 2.1
*/
public static class ObjectToArray extends AbstractAggregationExpression {
private ObjectToArray(Object value) {
super(value);
}
/**
* Creates new {@link ObjectToArray aggregation expression} that takes the value pointed to by given {@link Field
* fieldReference} and converts it to an array.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link ObjectToArray}.
*/
public static ObjectToArray valueOfToArray(String fieldReference) {
return toArray(Fields.field(fieldReference));
}
/**
* Creates new {@link ObjectToArray aggregation expression} that takes the result value of the given
* {@link AggregationExpression expression} and converts it to an array.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link ObjectToArray}.
*/
public static ObjectToArray valueOfToArray(AggregationExpression expression) {
return toArray(expression);
}
/**
* Creates new {@link ObjectToArray aggregation expression} that takes the given value and converts it to an array.
*
* @param value must not be {@literal null}.
* @return new instance of {@link ObjectToArray}.
*/
public static ObjectToArray toArray(Object value) {
return new ObjectToArray(value);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AbstractAggregationExpression#getMongoMethod()
*/
@Override
protected String getMongoMethod() {
return "$objectToArray";
}
}
}

View File

@@ -102,7 +102,7 @@ public class PrefixingDelegatingAggregationOperationContext implements Aggregati
} }
private String prefixKey(String key) { private String prefixKey(String key) {
return (key.startsWith("$") || blacklist.contains(key)) ? key : (prefix + "." + key); return (key.startsWith("$") || isBlacklisted(key)) ? key : (prefix + "." + key);
} }
private Object prefixCollection(Collection<Object> sourceCollection) { private Object prefixCollection(Collection<Object> sourceCollection) {
@@ -119,4 +119,23 @@ public class PrefixingDelegatingAggregationOperationContext implements Aggregati
return prefixed; return prefixed;
} }
private boolean isBlacklisted(String key) {
if (blacklist.contains(key)) {
return true;
}
if (!key.contains(".")) {
return false;
}
for (String blacklisted : blacklist) {
if (key.startsWith(blacklisted + ".")) {
return true;
}
}
return false;
}
} }

View File

@@ -1204,6 +1204,18 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
return this.operation.and(DateOperators.DateToString.dateOf(getRequiredName()).toString(format)); return this.operation.and(DateOperators.DateToString.dateOf(getRequiredName()).toString(format));
} }
/**
* Generates a {@code $dateToString} expression that takes the date representation of the previously mentioned field
* using the server default format. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @return
* @since 2.1
*/
public ProjectionOperationBuilder dateAsFormattedString() {
return this.operation.and(DateOperators.DateToString.dateOf(getRequiredName()).defaultFormat());
}
/** /**
* Generates a {@code $let} expression that binds variables for use in the specified expression, and returns the * Generates a {@code $let} expression that binds variables for use in the specified expression, and returns the
* result of the expression. * result of the expression.

View File

@@ -350,8 +350,7 @@ public class StringOperators {
* @return * @return
*/ */
public StrLenBytes length() { public StrLenBytes length() {
return usesFieldRef() ? StrLenBytes.stringLengthOf(fieldReference) return usesFieldRef() ? StrLenBytes.stringLengthOf(fieldReference) : StrLenBytes.stringLengthOf(expression);
: StrLenBytes.stringLengthOf(expression);
} }
/** /**
@@ -391,6 +390,132 @@ public class StringOperators {
return usesFieldRef() ? SubstrCP.valueOf(fieldReference) : SubstrCP.valueOf(expression); return usesFieldRef() ? SubstrCP.valueOf(fieldReference) : SubstrCP.valueOf(expression);
} }
/**
* Creates new {@link AggregationExpression} that takes the associated string representation and trims whitespaces
* from the beginning and end. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @return new instance of {@link Trim}.
* @since 2.1
*/
public Trim trim() {
return createTrim();
}
/**
* Creates new {@link AggregationExpression} that takes the associated string representation and trims the given
* character sequence from the beginning and end. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @param chars must not be {@literal null}.
* @return new instance of {@link Trim}.
* @since 2.1
*/
public Trim trim(String chars) {
return trim().chars(chars);
}
/**
* Creates new {@link AggregationExpression} that takes the associated string representation and trims the character
* sequence resulting from the given {@link AggregationExpression} from the beginning and end. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link Trim}.
* @since 2.1
*/
public Trim trim(AggregationExpression expression) {
return trim().charsOf(expression);
}
private Trim createTrim() {
return usesFieldRef() ? Trim.valueOf(fieldReference) : Trim.valueOf(expression);
}
/**
* Creates new {@link AggregationExpression} that takes the associated string representation and trims whitespaces
* from the beginning. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @return new instance of {@link LTrim}.
* @since 2.1
*/
public LTrim ltrim() {
return createLTrim();
}
/**
* Creates new {@link AggregationExpression} that takes the associated string representation and trims the given
* character sequence from the beginning. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @param chars must not be {@literal null}.
* @return new instance of {@link LTrim}.
* @since 2.1
*/
public LTrim ltrim(String chars) {
return ltrim().chars(chars);
}
/**
* Creates new {@link AggregationExpression} that takes the associated string representation and trims the character
* sequence resulting from the given {@link AggregationExpression} from the beginning. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link LTrim}.
* @since 2.1
*/
public LTrim ltrim(AggregationExpression expression) {
return ltrim().charsOf(expression);
}
private LTrim createLTrim() {
return usesFieldRef() ? LTrim.valueOf(fieldReference) : LTrim.valueOf(expression);
}
/**
* Creates new {@link AggregationExpression} that takes the associated string representation and trims whitespaces
* from the end. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @return new instance of {@link RTrim}.
* @since 2.1
*/
public RTrim rtrim() {
return createRTrim();
}
/**
* Creates new {@link AggregationExpression} that takes the associated string representation and trims the given
* character sequence from the end. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @param chars must not be {@literal null}.
* @return new instance of {@link RTrim}.
* @since 2.1
*/
public RTrim rtrim(String chars) {
return rtrim().chars(chars);
}
/**
* Creates new {@link AggregationExpression} that takes the associated string representation and trims the character
* sequence resulting from the given {@link AggregationExpression} from the end. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link RTrim}.
* @since 2.1
*/
public RTrim rtrim(AggregationExpression expression) {
return rtrim().charsOf(expression);
}
private RTrim createRTrim() {
return usesFieldRef() ? RTrim.valueOf(fieldReference) : RTrim.valueOf(expression);
}
private boolean usesFieldRef() { private boolean usesFieldRef() {
return fieldReference != null; return fieldReference != null;
} }
@@ -1072,4 +1197,257 @@ public class StringOperators {
return new SubstrCP(append(Arrays.asList(start, nrOfChars))); return new SubstrCP(append(Arrays.asList(start, nrOfChars)));
} }
} }
/**
* {@link AggregationExpression} for {@code $trim} which removes whitespace or the specified characters from the
* beginning and end of a string. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @author Christoph Strobl
* @since 2.1
*/
public static class Trim extends AbstractAggregationExpression {
private Trim(Object value) {
super(value);
}
/**
* Creates new {@link Trim} using the value of the provided {@link Field fieldReference} as {@literal input} value.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link LTrim}.
*/
public static Trim valueOf(String fieldReference) {
Assert.notNull(fieldReference, "FieldReference must not be null!");
return new Trim(Collections.singletonMap("input", Fields.field(fieldReference)));
}
/**
* Creates new {@link Trim} using the result of the provided {@link AggregationExpression} as {@literal input}
* value.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link Trim}.
*/
public static Trim valueOf(AggregationExpression expression) {
Assert.notNull(expression, "Expression must not be null!");
return new Trim(Collections.singletonMap("input", expression));
}
/**
* Optional specify the character(s) to trim from the beginning.
*
* @param chars must not be {@literal null}.
* @return new instance of {@link Trim}.
*/
public Trim chars(String chars) {
Assert.notNull(chars, "Chars must not be null!");
return new Trim(append("chars", chars));
}
/**
* Optional specify the reference to the {@link Field field} holding the character values to trim from the
* beginning.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link Trim}.
*/
public Trim charsOf(String fieldReference) {
return new Trim(append("chars", Fields.field(fieldReference)));
}
/**
* Optional specify the {@link AggregationExpression} evaluating to the character sequence to trim from the
* beginning.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link Trim}.
*/
public Trim charsOf(AggregationExpression expression) {
return new Trim(append("chars", expression));
}
/**
* Remove whitespace or the specified characters from the beginning of a string.<br />
*
* @return new instance of {@link LTrim}.
*/
public LTrim left() {
return new LTrim(argumentMap());
}
/**
* Remove whitespace or the specified characters from the end of a string.<br />
*
* @return new instance of {@link RTrim}.
*/
public RTrim right() {
return new RTrim(argumentMap());
}
@Override
protected String getMongoMethod() {
return "$trim";
}
}
/**
* {@link AggregationExpression} for {@code $ltrim} which removes whitespace or the specified characters from the
* beginning of a string. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @author Christoph Strobl
* @since 2.1
*/
public static class LTrim extends AbstractAggregationExpression {
private LTrim(Object value) {
super(value);
}
/**
* Creates new {@link LTrim} using the value of the provided {@link Field fieldReference} as {@literal input} value.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link LTrim}.
*/
public static LTrim valueOf(String fieldReference) {
Assert.notNull(fieldReference, "FieldReference must not be null!");
return new LTrim(Collections.singletonMap("input", Fields.field(fieldReference)));
}
/**
* Creates new {@link LTrim} using the result of the provided {@link AggregationExpression} as {@literal input}
* value.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link LTrim}.
*/
public static LTrim valueOf(AggregationExpression expression) {
Assert.notNull(expression, "Expression must not be null!");
return new LTrim(Collections.singletonMap("input", expression));
}
/**
* Optional specify the character(s) to trim from the beginning.
*
* @param chars must not be {@literal null}.
* @return new instance of {@link LTrim}.
*/
public LTrim chars(String chars) {
Assert.notNull(chars, "Chars must not be null!");
return new LTrim(append("chars", chars));
}
/**
* Optional specify the reference to the {@link Field field} holding the character values to trim from the
* beginning.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link LTrim}.
*/
public LTrim charsOf(String fieldReference) {
return new LTrim(append("chars", Fields.field(fieldReference)));
}
/**
* Optional specify the {@link AggregationExpression} evaluating to the character sequence to trim from the
* beginning.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link LTrim}.
*/
public LTrim charsOf(AggregationExpression expression) {
return new LTrim(append("chars", expression));
}
@Override
protected String getMongoMethod() {
return "$ltrim";
}
}
/**
* {@link AggregationExpression} for {@code $rtrim} which removes whitespace or the specified characters from the end
* of a string. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @author Christoph Strobl
* @since 2.1
*/
public static class RTrim extends AbstractAggregationExpression {
private RTrim(Object value) {
super(value);
}
/**
* Creates new {@link RTrim} using the value of the provided {@link Field fieldReference} as {@literal input} value.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link RTrim}.
*/
public static RTrim valueOf(String fieldReference) {
Assert.notNull(fieldReference, "FieldReference must not be null!");
return new RTrim(Collections.singletonMap("input", Fields.field(fieldReference)));
}
/**
* Creates new {@link RTrim} using the result of the provided {@link AggregationExpression} as {@literal input}
* value.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link RTrim}.
*/
public static RTrim valueOf(AggregationExpression expression) {
Assert.notNull(expression, "Expression must not be null!");
return new RTrim(Collections.singletonMap("input", expression));
}
/**
* Optional specify the character(s) to trim from the end.
*
* @param chars must not be {@literal null}.
* @return new instance of {@link RTrim}.
*/
public RTrim chars(String chars) {
Assert.notNull(chars, "Chars must not be null!");
return new RTrim(append("chars", chars));
}
/**
* Optional specify the reference to the {@link Field field} holding the character values to trim from the end.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link RTrim}.
*/
public RTrim charsOf(String fieldReference) {
return new RTrim(append("chars", Fields.field(fieldReference)));
}
/**
* Optional specify the {@link AggregationExpression} evaluating to the character sequence to trim from the end.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link RTrim}.
*/
public RTrim charsOf(AggregationExpression expression) {
return new RTrim(append("chars", expression));
}
@Override
protected String getMongoMethod() {
return "$rtrim";
}
}
} }

View File

@@ -22,6 +22,7 @@ import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import com.mongodb.DBRef; import com.mongodb.DBRef;
@@ -59,9 +60,15 @@ public interface DbRefResolver {
* @param id will never be {@literal null}. * @param id will never be {@literal null}.
* @return * @return
*/ */
DBRef createDbRef(@Nullable org.springframework.data.mongodb.core.mapping.DBRef annotation, default DBRef createDbRef(@Nullable org.springframework.data.mongodb.core.mapping.DBRef annotation,
MongoPersistentEntity<?> entity, MongoPersistentEntity<?> entity, Object id) {
Object id);
if (annotation != null && StringUtils.hasText(annotation.db())) {
return new DBRef(annotation.db(), entity.getCollection(), id);
}
return new DBRef(entity.getCollection(), id);
}
/** /**
* Actually loads the {@link DBRef} from the datasource. * Actually loads the {@link DBRef} from the datasource.

View File

@@ -43,7 +43,6 @@ import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.mongodb.ClientSessionException; import org.springframework.data.mongodb.ClientSessionException;
import org.springframework.data.mongodb.LazyLoadingException; import org.springframework.data.mongodb.LazyLoadingException;
import org.springframework.data.mongodb.MongoDbFactory; 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.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.objenesis.ObjenesisStd; import org.springframework.objenesis.ObjenesisStd;
@@ -104,21 +103,6 @@ public class DefaultDbRefResolver implements DbRefResolver {
return callback.resolve(property); return callback.resolve(property);
} }
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.DbRefResolver#created(org.springframework.data.mongodb.core.mapping.MongoPersistentProperty, org.springframework.data.mongodb.core.mapping.MongoPersistentEntity, java.lang.Object)
*/
@Override
public DBRef createDbRef(@Nullable org.springframework.data.mongodb.core.mapping.DBRef annotation,
MongoPersistentEntity<?> entity, Object id) {
if (annotation != null && StringUtils.hasText(annotation.db())) {
return new DBRef(annotation.db(), entity.getCollection(), id);
}
return new DBRef(entity.getCollection(), id);
}
/* /*
* (non-Javadoc) * (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.DbRefResolver#fetch(com.mongodb.DBRef) * @see org.springframework.data.mongodb.core.convert.DbRefResolver#fetch(com.mongodb.DBRef)

View File

@@ -21,6 +21,7 @@ import java.util.Map;
import org.bson.Document; import org.bson.Document;
import org.bson.conversions.Bson; import org.bson.conversions.Bson;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.mongodb.util.BsonUtils;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
@@ -58,6 +59,14 @@ class DocumentAccessor {
this.document = document; this.document = document;
} }
/**
* @return the underlying {@link Bson document}.
* @since 2.1
*/
Bson getDocument() {
return this.document;
}
/** /**
* Puts the given value into the backing {@link Document} based on the coordinates defined through the given * Puts the given value into the backing {@link Document} based on the coordinates defined through the given
* {@link MongoPersistentProperty}. By default this will be the plain field name. But field names might also consist * {@link MongoPersistentProperty}. By default this will be the plain field name. But field names might also consist
@@ -103,13 +112,14 @@ class DocumentAccessor {
public Object get(MongoPersistentProperty property) { public Object get(MongoPersistentProperty property) {
String fieldName = property.getFieldName(); String fieldName = property.getFieldName();
Map<String, Object> map = BsonUtils.asMap(document);
if (!fieldName.contains(".")) { if (!fieldName.contains(".")) {
return BsonUtils.asMap(this.document).get(fieldName); return map.get(fieldName);
} }
Iterator<String> parts = Arrays.asList(fieldName.split("\\.")).iterator(); Iterator<String> parts = Arrays.asList(fieldName.split("\\.")).iterator();
Map<String, Object> source = BsonUtils.asMap(this.document); Map<String, Object> source = map;
Object result = null; Object result = null;
while (source != null && parts.hasNext()) { while (source != null && parts.hasNext()) {
@@ -124,6 +134,17 @@ class DocumentAccessor {
return result; return result;
} }
/**
* Returns the raw identifier for the given {@link MongoPersistentEntity} or the value of the default identifier
* field.
*
* @param entity must not be {@literal null}.
* @return
*/
public Object getRawId(MongoPersistentEntity<?> entity) {
return entity.hasIdProperty() ? get(entity.getRequiredIdProperty()) : BsonUtils.asMap(document).get("_id");
}
/** /**
* Returns whether the underlying {@link Document} has a value ({@literal null} or non-{@literal null}) for the given * Returns whether the underlying {@link Document} has a value ({@literal null} or non-{@literal null}) for the given
* {@link MongoPersistentProperty}. * {@link MongoPersistentProperty}.
@@ -131,21 +152,27 @@ class DocumentAccessor {
* @param property must not be {@literal null}. * @param property must not be {@literal null}.
* @return * @return
*/ */
@SuppressWarnings("unchecked")
public boolean hasValue(MongoPersistentProperty property) { public boolean hasValue(MongoPersistentProperty property) {
Assert.notNull(property, "Property must not be null!"); Assert.notNull(property, "Property must not be null!");
String fieldName = property.getFieldName(); String fieldName = property.getFieldName();
if (this.document instanceof Document) {
if (((Document) this.document).containsKey(fieldName)) {
return true;
}
} else if (this.document instanceof DBObject) {
if (((DBObject) this.document).containsField(fieldName)) {
return true;
}
}
if (!fieldName.contains(".")) { if (!fieldName.contains(".")) {
return false;
if (this.document instanceof Document) {
return ((Document) this.document).containsKey(fieldName);
}
if (this.document instanceof DBObject) {
return ((DBObject) this.document).containsField(fieldName);
}
} }
String[] parts = fieldName.split("\\."); String[] parts = fieldName.split("\\.");

View File

@@ -15,10 +15,13 @@
*/ */
package org.springframework.data.mongodb.core.convert; package org.springframework.data.mongodb.core.convert;
import java.text.Collator;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.bson.Document; import org.bson.Document;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
@@ -41,11 +44,13 @@ import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
import org.springframework.data.mongodb.core.geo.GeoJsonPolygon; import org.springframework.data.mongodb.core.geo.GeoJsonPolygon;
import org.springframework.data.mongodb.core.geo.Sphere; import org.springframework.data.mongodb.core.geo.Sphere;
import org.springframework.data.mongodb.core.query.GeoCommand; import org.springframework.data.mongodb.core.query.GeoCommand;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.NumberUtils; import org.springframework.util.NumberUtils;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
import com.mongodb.BasicDBList; import com.mongodb.BasicDBList;
import com.mongodb.Function;
/** /**
* Wrapper class to contain useful geo structure converters for the usage with Mongo. * Wrapper class to contain useful geo structure converters for the usage with Mongo.
@@ -58,6 +63,26 @@ import com.mongodb.BasicDBList;
*/ */
abstract class GeoConverters { abstract class GeoConverters {
private final static Map<String, Function<Document, GeoJson<?>>> converters;
static {
Collator caseInsensitive = Collator.getInstance();
caseInsensitive.setStrength(Collator.PRIMARY);
Map<String, Function<Document, GeoJson<?>>> geoConverters = new TreeMap<>(caseInsensitive);
geoConverters.put("point", DocumentToGeoJsonPointConverter.INSTANCE::convert);
geoConverters.put("multipoint", DocumentToGeoJsonMultiPointConverter.INSTANCE::convert);
geoConverters.put("linestring", DocumentToGeoJsonLineStringConverter.INSTANCE::convert);
geoConverters.put("multilinestring", DocumentToGeoJsonMultiLineStringConverter.INSTANCE::convert);
geoConverters.put("polygon", DocumentToGeoJsonPolygonConverter.INSTANCE::convert);
geoConverters.put("multipolygon", DocumentToGeoJsonMultiPolygonConverter.INSTANCE::convert);
geoConverters.put("geometrycollection", DocumentToGeoJsonGeometryCollectionConverter.INSTANCE::convert);
converters = geoConverters;
}
/** /**
* Private constructor to prevent instantiation. * Private constructor to prevent instantiation.
*/ */
@@ -91,7 +116,8 @@ abstract class GeoConverters {
, DocumentToGeoJsonMultiLineStringConverter.INSTANCE // , DocumentToGeoJsonMultiLineStringConverter.INSTANCE //
, DocumentToGeoJsonMultiPointConverter.INSTANCE // , DocumentToGeoJsonMultiPointConverter.INSTANCE //
, DocumentToGeoJsonMultiPolygonConverter.INSTANCE // , DocumentToGeoJsonMultiPolygonConverter.INSTANCE //
, DocumentToGeoJsonGeometryCollectionConverter.INSTANCE); , DocumentToGeoJsonGeometryCollectionConverter.INSTANCE //
, DocumentToGeoJsonConverter.INSTANCE);
} }
/** /**
@@ -101,7 +127,7 @@ abstract class GeoConverters {
* @since 1.5 * @since 1.5
*/ */
@ReadingConverter @ReadingConverter
static enum DocumentToPointConverter implements Converter<Document, Point> { enum DocumentToPointConverter implements Converter<Document, Point> {
INSTANCE; INSTANCE;
@@ -132,7 +158,7 @@ abstract class GeoConverters {
* @author Thomas Darimont * @author Thomas Darimont
* @since 1.5 * @since 1.5
*/ */
static enum PointToDocumentConverter implements Converter<Point, Document> { enum PointToDocumentConverter implements Converter<Point, Document> {
INSTANCE; INSTANCE;
@@ -147,13 +173,13 @@ abstract class GeoConverters {
} }
/** /**
* Converts a {@link Box} into a {@link BasicDBList}. * Converts a {@link Box} into a {@link Document}.
* *
* @author Thomas Darimont * @author Thomas Darimont
* @since 1.5 * @since 1.5
*/ */
@WritingConverter @WritingConverter
static enum BoxToDocumentConverter implements Converter<Box, Document> { enum BoxToDocumentConverter implements Converter<Box, Document> {
INSTANCE; INSTANCE;
@@ -176,13 +202,13 @@ abstract class GeoConverters {
} }
/** /**
* Converts a {@link BasicDBList} into a {@link Box}. * Converts a {@link Document} into a {@link Box}.
* *
* @author Thomas Darimont * @author Thomas Darimont
* @since 1.5 * @since 1.5
*/ */
@ReadingConverter @ReadingConverter
static enum DocumentToBoxConverter implements Converter<Document, Box> { enum DocumentToBoxConverter implements Converter<Document, Box> {
INSTANCE; INSTANCE;
@@ -205,12 +231,12 @@ abstract class GeoConverters {
} }
/** /**
* Converts a {@link Circle} into a {@link BasicDBList}. * Converts a {@link Circle} into a {@link Document}.
* *
* @author Thomas Darimont * @author Thomas Darimont
* @since 1.5 * @since 1.5
*/ */
static enum CircleToDocumentConverter implements Converter<Circle, Document> { enum CircleToDocumentConverter implements Converter<Circle, Document> {
INSTANCE; INSTANCE;
@@ -276,7 +302,7 @@ abstract class GeoConverters {
} }
/** /**
* Converts a {@link Sphere} into a {@link BasicDBList}. * Converts a {@link Sphere} into a {@link Document}.
* *
* @author Thomas Darimont * @author Thomas Darimont
* @since 1.5 * @since 1.5
@@ -305,13 +331,13 @@ abstract class GeoConverters {
} }
/** /**
* Converts a {@link BasicDBList} into a {@link Sphere}. * Converts a {@link Document} into a {@link Sphere}.
* *
* @author Thomas Darimont * @author Thomas Darimont
* @since 1.5 * @since 1.5
*/ */
@ReadingConverter @ReadingConverter
static enum DocumentToSphereConverter implements Converter<Document, Sphere> { enum DocumentToSphereConverter implements Converter<Document, Sphere> {
INSTANCE; INSTANCE;
@@ -347,12 +373,12 @@ abstract class GeoConverters {
} }
/** /**
* Converts a {@link Polygon} into a {@link BasicDBList}. * Converts a {@link Polygon} into a {@link Document}.
* *
* @author Thomas Darimont * @author Thomas Darimont
* @since 1.5 * @since 1.5
*/ */
static enum PolygonToDocumentConverter implements Converter<Polygon, Document> { enum PolygonToDocumentConverter implements Converter<Polygon, Document> {
INSTANCE; INSTANCE;
@@ -368,7 +394,7 @@ abstract class GeoConverters {
} }
List<Point> points = source.getPoints(); List<Point> points = source.getPoints();
List<Document> pointTuples = new ArrayList<Document>(points.size()); List<Document> pointTuples = new ArrayList<>(points.size());
for (Point point : points) { for (Point point : points) {
pointTuples.add(PointToDocumentConverter.INSTANCE.convert(point)); pointTuples.add(PointToDocumentConverter.INSTANCE.convert(point));
@@ -381,13 +407,13 @@ abstract class GeoConverters {
} }
/** /**
* Converts a {@link BasicDBList} into a {@link Polygon}. * Converts a {@link Document} into a {@link Polygon}.
* *
* @author Thomas Darimont * @author Thomas Darimont
* @since 1.5 * @since 1.5
*/ */
@ReadingConverter @ReadingConverter
static enum DocumentToPolygonConverter implements Converter<Document, Polygon> { enum DocumentToPolygonConverter implements Converter<Document, Polygon> {
INSTANCE; INSTANCE;
@@ -404,7 +430,7 @@ abstract class GeoConverters {
} }
List<Document> points = (List<Document>) source.get("points"); List<Document> points = (List<Document>) source.get("points");
List<Point> newPoints = new ArrayList<Point>(points.size()); List<Point> newPoints = new ArrayList<>(points.size());
for (Document element : points) { for (Document element : points) {
@@ -417,12 +443,12 @@ abstract class GeoConverters {
} }
/** /**
* Converts a {@link Sphere} into a {@link BasicDBList}. * Converts a {@link Sphere} into a {@link Document}.
* *
* @author Thomas Darimont * @author Thomas Darimont
* @since 1.5 * @since 1.5
*/ */
static enum GeoCommandToDocumentConverter implements Converter<GeoCommand, Document> { enum GeoCommandToDocumentConverter implements Converter<GeoCommand, Document> {
INSTANCE; INSTANCE;
@@ -482,7 +508,7 @@ abstract class GeoConverters {
* @since 1.7 * @since 1.7
*/ */
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
static enum GeoJsonToDocumentConverter implements Converter<GeoJson, Document> { enum GeoJsonToDocumentConverter implements Converter<GeoJson, Document> {
INSTANCE; INSTANCE;
@@ -545,7 +571,7 @@ abstract class GeoConverters {
* @author Christoph Strobl * @author Christoph Strobl
* @since 1.7 * @since 1.7
*/ */
static enum GeoJsonPointToDocumentConverter implements Converter<GeoJsonPoint, Document> { enum GeoJsonPointToDocumentConverter implements Converter<GeoJsonPoint, Document> {
INSTANCE; INSTANCE;
@@ -563,7 +589,7 @@ abstract class GeoConverters {
* @author Christoph Strobl * @author Christoph Strobl
* @since 1.7 * @since 1.7
*/ */
static enum GeoJsonPolygonToDocumentConverter implements Converter<GeoJsonPolygon, Document> { enum GeoJsonPolygonToDocumentConverter implements Converter<GeoJsonPolygon, Document> {
INSTANCE; INSTANCE;
@@ -581,7 +607,7 @@ abstract class GeoConverters {
* @author Christoph Strobl * @author Christoph Strobl
* @since 1.7 * @since 1.7
*/ */
static enum DocumentToGeoJsonPointConverter implements Converter<Document, GeoJsonPoint> { enum DocumentToGeoJsonPointConverter implements Converter<Document, GeoJsonPoint> {
INSTANCE; INSTANCE;
@@ -609,7 +635,7 @@ abstract class GeoConverters {
* @author Christoph Strobl * @author Christoph Strobl
* @since 1.7 * @since 1.7
*/ */
static enum DocumentToGeoJsonPolygonConverter implements Converter<Document, GeoJsonPolygon> { enum DocumentToGeoJsonPolygonConverter implements Converter<Document, GeoJsonPolygon> {
INSTANCE; INSTANCE;
@@ -654,7 +680,7 @@ abstract class GeoConverters {
String.format("Cannot convert type '%s' to MultiPolygon.", source.get("type"))); String.format("Cannot convert type '%s' to MultiPolygon.", source.get("type")));
List dbl = (List) source.get("coordinates"); List dbl = (List) source.get("coordinates");
List<GeoJsonPolygon> polygones = new ArrayList<GeoJsonPolygon>(); List<GeoJsonPolygon> polygones = new ArrayList<>();
for (Object polygon : dbl) { for (Object polygon : dbl) {
polygones.add(toGeoJsonPolygon((List) polygon)); polygones.add(toGeoJsonPolygon((List) polygon));
@@ -668,7 +694,7 @@ abstract class GeoConverters {
* @author Christoph Strobl * @author Christoph Strobl
* @since 1.7 * @since 1.7
*/ */
static enum DocumentToGeoJsonLineStringConverter implements Converter<Document, GeoJsonLineString> { enum DocumentToGeoJsonLineStringConverter implements Converter<Document, GeoJsonLineString> {
INSTANCE; INSTANCE;
@@ -696,7 +722,7 @@ abstract class GeoConverters {
* @author Christoph Strobl * @author Christoph Strobl
* @since 1.7 * @since 1.7
*/ */
static enum DocumentToGeoJsonMultiPointConverter implements Converter<Document, GeoJsonMultiPoint> { enum DocumentToGeoJsonMultiPointConverter implements Converter<Document, GeoJsonMultiPoint> {
INSTANCE; INSTANCE;
@@ -724,7 +750,7 @@ abstract class GeoConverters {
* @author Christoph Strobl * @author Christoph Strobl
* @since 1.7 * @since 1.7
*/ */
static enum DocumentToGeoJsonMultiLineStringConverter implements Converter<Document, GeoJsonMultiLineString> { enum DocumentToGeoJsonMultiLineStringConverter implements Converter<Document, GeoJsonMultiLineString> {
INSTANCE; INSTANCE;
@@ -756,7 +782,7 @@ abstract class GeoConverters {
* @author Christoph Strobl * @author Christoph Strobl
* @since 1.7 * @since 1.7
*/ */
static enum DocumentToGeoJsonGeometryCollectionConverter implements Converter<Document, GeoJsonGeometryCollection> { enum DocumentToGeoJsonGeometryCollectionConverter implements Converter<Document, GeoJsonGeometryCollection> {
INSTANCE; INSTANCE;
@@ -775,41 +801,12 @@ abstract class GeoConverters {
Assert.isTrue(ObjectUtils.nullSafeEquals(source.get("type"), "GeometryCollection"), Assert.isTrue(ObjectUtils.nullSafeEquals(source.get("type"), "GeometryCollection"),
String.format("Cannot convert type '%s' to GeometryCollection.", source.get("type"))); String.format("Cannot convert type '%s' to GeometryCollection.", source.get("type")));
List<GeoJson<?>> geometries = new ArrayList<GeoJson<?>>(); List<GeoJson<?>> geometries = new ArrayList<>();
for (Object o : (List) source.get("geometries")) { for (Object o : (List) source.get("geometries")) {
geometries.add(convertGeometries((Document) o)); geometries.add(toGenericGeoJson((Document) o));
} }
return new GeoJsonGeometryCollection(geometries); return new GeoJsonGeometryCollection(geometries);
}
private static GeoJson<?> convertGeometries(Document source) {
Object type = source.get("type");
if (ObjectUtils.nullSafeEquals(type, "Point")) {
return DocumentToGeoJsonPointConverter.INSTANCE.convert(source);
}
if (ObjectUtils.nullSafeEquals(type, "MultiPoint")) {
return DocumentToGeoJsonMultiPointConverter.INSTANCE.convert(source);
}
if (ObjectUtils.nullSafeEquals(type, "LineString")) {
return DocumentToGeoJsonLineStringConverter.INSTANCE.convert(source);
}
if (ObjectUtils.nullSafeEquals(type, "MultiLineString")) {
return DocumentToGeoJsonMultiLineStringConverter.INSTANCE.convert(source);
}
if (ObjectUtils.nullSafeEquals(type, "Polygon")) {
return DocumentToGeoJsonPolygonConverter.INSTANCE.convert(source);
}
if (ObjectUtils.nullSafeEquals(type, "MultiPolygon")) {
return DocumentToGeoJsonMultiPolygonConverter.INSTANCE.convert(source);
}
throw new IllegalArgumentException(String.format("Cannot convert unknown GeoJson type %s", type));
} }
} }
@@ -827,7 +824,7 @@ abstract class GeoConverters {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
static List<Point> toListOfPoint(List listOfCoordinatePairs) { static List<Point> toListOfPoint(List listOfCoordinatePairs) {
List<Point> points = new ArrayList<Point>(); List<Point> points = new ArrayList<>();
for (Object point : listOfCoordinatePairs) { for (Object point : listOfCoordinatePairs) {
@@ -852,6 +849,46 @@ abstract class GeoConverters {
return new GeoJsonPolygon(toListOfPoint((List) dbList.get(0))); return new GeoJsonPolygon(toListOfPoint((List) dbList.get(0)));
} }
/**
* Converter implementation transforming a {@link Document} into a concrete {@link GeoJson} based on the embedded
* {@literal type} information.
*
* @since 2.1
* @author Christoph Strobl
*/
@ReadingConverter
enum DocumentToGeoJsonConverter implements Converter<Document, GeoJson> {
INSTANCE;
/*
* (non-Javadoc)
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
*/
@Nullable
@Override
public GeoJson convert(Document source) {
return toGenericGeoJson(source);
}
}
private static GeoJson<?> toGenericGeoJson(Document source) {
String type = source.get("type", String.class);
if(type != null) {
Function<Document, GeoJson<?>> converter = converters.get(type);
if(converter != null){
return converter.apply(source);
}
}
throw new IllegalArgumentException(
String.format("No converter found capable of converting GeoJson type %s.", type));
}
private static double toPrimitiveDoubleValue(Object value) { private static double toPrimitiveDoubleValue(Object value) {
Assert.isInstanceOf(Number.class, value, "Argument must be a Number."); Assert.isInstanceOf(Number.class, value, "Argument must be a Number.");

View File

@@ -15,20 +15,12 @@
*/ */
package org.springframework.data.mongodb.core.convert; package org.springframework.data.mongodb.core.convert;
import java.util.ArrayList; import java.util.*;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import org.bson.Document; import org.bson.Document;
import org.bson.conversions.Bson; import org.bson.conversions.Bson;
import org.bson.types.ObjectId;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
@@ -42,6 +34,7 @@ import org.springframework.data.convert.TypeMapper;
import org.springframework.data.mapping.Association; import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PreferredConstructor;
import org.springframework.data.mapping.PreferredConstructor.Parameter; import org.springframework.data.mapping.PreferredConstructor.Parameter;
import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
@@ -147,7 +140,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
*/ */
public void setTypeMapper(@Nullable MongoTypeMapper typeMapper) { public void setTypeMapper(@Nullable MongoTypeMapper typeMapper) {
this.typeMapper = typeMapper == null this.typeMapper = typeMapper == null
? new DefaultMongoTypeMapper(DefaultMongoTypeMapper.DEFAULT_TYPE_KEY, mappingContext) : typeMapper; ? new DefaultMongoTypeMapper(DefaultMongoTypeMapper.DEFAULT_TYPE_KEY, mappingContext)
: typeMapper;
} }
/* /*
@@ -249,13 +243,14 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
throw new MappingException(String.format(INVALID_TYPE_TO_READ, target, typeToUse.getType())); throw new MappingException(String.format(INVALID_TYPE_TO_READ, target, typeToUse.getType()));
} }
return read((MongoPersistentEntity<S>) mappingContext.getRequiredPersistentEntity(typeToUse), target, path); return read((MongoPersistentEntity<S>) entity, target, path);
} }
private ParameterValueProvider<MongoPersistentProperty> getParameterProvider(MongoPersistentEntity<?> entity, private ParameterValueProvider<MongoPersistentProperty> getParameterProvider(MongoPersistentEntity<?> entity,
Bson source, DefaultSpELExpressionEvaluator evaluator, ObjectPath path) { DocumentAccessor source, SpELExpressionEvaluator evaluator, ObjectPath path) {
MongoDbPropertyValueProvider provider = new MongoDbPropertyValueProvider(source, evaluator, path); AssociationAwareMongoDbPropertyValueProvider provider = new AssociationAwareMongoDbPropertyValueProvider(source,
evaluator, path);
PersistentEntityParameterValueProvider<MongoPersistentProperty> parameterProvider = new PersistentEntityParameterValueProvider<>( PersistentEntityParameterValueProvider<MongoPersistentProperty> parameterProvider = new PersistentEntityParameterValueProvider<>(
entity, provider, path.getCurrentObject()); entity, provider, path.getCurrentObject());
@@ -265,60 +260,105 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
private <S extends Object> S read(final MongoPersistentEntity<S> entity, final Document bson, final ObjectPath path) { private <S extends Object> S read(final MongoPersistentEntity<S> entity, final Document bson, final ObjectPath path) {
DefaultSpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(bson, spELContext); SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(bson, spELContext);
DocumentAccessor documentAccessor = new DocumentAccessor(bson);
PreferredConstructor<S, MongoPersistentProperty> persistenceConstructor = entity.getPersistenceConstructor();
ParameterValueProvider<MongoPersistentProperty> provider = persistenceConstructor != null
&& persistenceConstructor.hasParameters() ? getParameterProvider(entity, documentAccessor, evaluator, path)
: NoOpParameterValueProvider.INSTANCE;
ParameterValueProvider<MongoPersistentProperty> provider = getParameterProvider(entity, bson, evaluator, path);
EntityInstantiator instantiator = instantiators.getInstantiatorFor(entity); EntityInstantiator instantiator = instantiators.getInstantiatorFor(entity);
S instance = instantiator.createInstance(entity, provider); S instance = instantiator.createInstance(entity, provider);
PersistentPropertyAccessor accessor = new ConvertingPropertyAccessor(entity.getPropertyAccessor(instance), if (entity.requiresPropertyPopulation()) {
conversionService); return populateProperties(entity, documentAccessor, path, evaluator, instance);
MongoPersistentProperty idProperty = entity.getIdProperty();
DocumentAccessor documentAccessor = new DocumentAccessor(bson);
// make sure id property is set before all other properties
Object idValue = null;
if (idProperty != null && documentAccessor.hasValue(idProperty)) {
idValue = readIdValue(path, evaluator, idProperty, documentAccessor);
accessor.setProperty(idProperty, idValue);
} }
ObjectPath currentPath = path.push(instance, entity, idValue != null ? bson.get(idProperty.getFieldName()) : null);
MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(documentAccessor, evaluator,
currentPath);
DbRefResolverCallback callback = new DefaultDbRefResolverCallback(bson, currentPath, evaluator,
MappingMongoConverter.this);
readProperties(entity, accessor, idProperty, documentAccessor, valueProvider, callback);
return instance; return instance;
} }
private Object readIdValue(ObjectPath path, DefaultSpELExpressionEvaluator evaluator, private <S> S populateProperties(MongoPersistentEntity<S> entity, DocumentAccessor documentAccessor, ObjectPath path,
MongoPersistentProperty idProperty, DocumentAccessor documentAccessor) { SpELExpressionEvaluator evaluator, S instance) {
PersistentPropertyAccessor<S> accessor = new ConvertingPropertyAccessor<>(entity.getPropertyAccessor(instance),
conversionService);
// Make sure id property is set before all other properties
Object rawId = readAndPopulateIdentifier(accessor, documentAccessor, entity, path, evaluator);
ObjectPath currentPath = path.push(accessor.getBean(), entity, rawId);
MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(documentAccessor, evaluator,
currentPath);
readProperties(entity, accessor, documentAccessor, valueProvider, currentPath, evaluator);
return accessor.getBean();
}
/**
* Reads the identifier from either the bean backing the {@link PersistentPropertyAccessor} or the source document in
* case the identifier has not be populated yet. In this case the identifier is set on the bean for further reference.
*
* @param accessor must not be {@literal null}.
* @param document must not be {@literal null}.
* @param entity must not be {@literal null}.
* @param path
* @param evaluator
* @return
*/
private Object readAndPopulateIdentifier(PersistentPropertyAccessor<?> accessor, DocumentAccessor document,
MongoPersistentEntity<?> entity, ObjectPath path, SpELExpressionEvaluator evaluator) {
Object rawId = document.getRawId(entity);
if (!entity.hasIdProperty() || rawId == null) {
return rawId;
}
MongoPersistentProperty idProperty = entity.getRequiredIdProperty();
if (idProperty.isImmutable() && entity.isConstructorArgument(idProperty)) {
return rawId;
}
accessor.setProperty(idProperty, readIdValue(path, evaluator, idProperty, rawId));
return rawId;
}
private Object readIdValue(ObjectPath path, SpELExpressionEvaluator evaluator, MongoPersistentProperty idProperty,
Object rawId) {
String expression = idProperty.getSpelExpression(); String expression = idProperty.getSpelExpression();
Object resolvedValue = expression != null ? evaluator.evaluate(expression) : documentAccessor.get(idProperty); Object resolvedValue = expression != null ? evaluator.evaluate(expression) : rawId;
return resolvedValue != null ? readValue(resolvedValue, idProperty.getTypeInformation(), path) : null; return resolvedValue != null ? readValue(resolvedValue, idProperty.getTypeInformation(), path) : null;
} }
private void readProperties(MongoPersistentEntity<?> entity, PersistentPropertyAccessor accessor, private void readProperties(MongoPersistentEntity<?> entity, PersistentPropertyAccessor<?> accessor,
@Nullable MongoPersistentProperty idProperty, DocumentAccessor documentAccessor, DocumentAccessor documentAccessor, MongoDbPropertyValueProvider valueProvider, ObjectPath currentPath,
MongoDbPropertyValueProvider valueProvider, DbRefResolverCallback callback) { SpELExpressionEvaluator evaluator) {
DbRefResolverCallback callback = null;
for (MongoPersistentProperty prop : entity) { for (MongoPersistentProperty prop : entity) {
if (prop.isAssociation() && !entity.isConstructorArgument(prop)) { if (prop.isAssociation() && !entity.isConstructorArgument(prop)) {
if (callback == null) {
callback = getDbRefResolverCallback(documentAccessor, currentPath, evaluator);
}
readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback); readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback);
continue; continue;
} }
// we skip the id property since it was already set
if (idProperty != null && idProperty.equals(prop)) { // We skip the id property since it was already set
if (entity.isIdProperty(prop)) {
continue; continue;
} }
@@ -327,6 +367,11 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
} }
if (prop.isAssociation()) { if (prop.isAssociation()) {
if (callback == null) {
callback = getDbRefResolverCallback(documentAccessor, currentPath, evaluator);
}
readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback); readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback);
continue; continue;
} }
@@ -335,7 +380,14 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
} }
} }
private void readAssociation(Association<MongoPersistentProperty> association, PersistentPropertyAccessor accessor, private DbRefResolverCallback getDbRefResolverCallback(DocumentAccessor documentAccessor, ObjectPath currentPath,
SpELExpressionEvaluator evaluator) {
return new DefaultDbRefResolverCallback(documentAccessor.getDocument(), currentPath, evaluator,
MappingMongoConverter.this);
}
private void readAssociation(Association<MongoPersistentProperty> association, PersistentPropertyAccessor<?> accessor,
DocumentAccessor documentAccessor, DbRefProxyHandler handler, DbRefResolverCallback callback) { DocumentAccessor documentAccessor, DbRefProxyHandler handler, DbRefResolverCallback callback) {
MongoPersistentProperty property = association.getInverse(); MongoPersistentProperty property = association.getInverse();
@@ -392,12 +444,23 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
removeFromMap(bson, "_id"); removeFromMap(bson, "_id");
} }
boolean handledByCustomConverter = conversions.hasCustomWriteTarget(entityType, Document.class); if (requiresTypeHint(entityType)) {
if (!handledByCustomConverter && !(bson instanceof Collection)) {
typeMapper.writeType(type, bson); typeMapper.writeType(type, bson);
} }
} }
/**
* Check if a given type requires a type hint (aka {@literal _class} attribute) when writing to the document.
*
* @param type must not be {@literal null}.
* @return {@literal true} if not a simple type, {@link Collection} or type with custom write target.
*/
private boolean requiresTypeHint(Class<?> type) {
return !conversions.isSimpleType(type) && !ClassUtils.isAssignable(Collection.class, type)
&& !conversions.hasCustomWriteTarget(type, Document.class);
}
/** /**
* Internal write conversion method which should be used for nested invocations. * Internal write conversion method which should be used for nested invocations.
* *
@@ -427,7 +490,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
} }
if (Collection.class.isAssignableFrom(entityType)) { if (Collection.class.isAssignableFrom(entityType)) {
writeCollectionInternal((Collection<?>) obj, ClassTypeInformation.LIST, (Collection) bson); writeCollectionInternal((Collection<?>) obj, ClassTypeInformation.LIST, (Collection<?>) bson);
return; return;
} }
@@ -446,22 +509,23 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
throw new MappingException("No mapping metadata found for entity of type " + obj.getClass().getName()); throw new MappingException("No mapping metadata found for entity of type " + obj.getClass().getName());
} }
PersistentPropertyAccessor accessor = entity.getPropertyAccessor(obj); PersistentPropertyAccessor<?> accessor = entity.getPropertyAccessor(obj);
DocumentAccessor dbObjectAccessor = new DocumentAccessor(bson); DocumentAccessor dbObjectAccessor = new DocumentAccessor(bson);
MongoPersistentProperty idProperty = entity.getIdProperty(); MongoPersistentProperty idProperty = entity.getIdProperty();
if (idProperty != null && !dbObjectAccessor.hasValue(idProperty)) { if (idProperty != null && !dbObjectAccessor.hasValue(idProperty)) {
Object value = idMapper.convertId(accessor.getProperty(idProperty)); Object value = idMapper.convertId(accessor.getProperty(idProperty), idProperty.getFieldType());
if (value != null) { if (value != null) {
dbObjectAccessor.put(idProperty, value); dbObjectAccessor.put(idProperty, value);
} }
} }
writeProperties(bson, entity, accessor, dbObjectAccessor, idProperty); writeProperties(bson, entity, accessor, dbObjectAccessor, idProperty);
} }
private void writeProperties(Bson bson, MongoPersistentEntity<?> entity, PersistentPropertyAccessor accessor, private void writeProperties(Bson bson, MongoPersistentEntity<?> entity, PersistentPropertyAccessor<?> accessor,
DocumentAccessor dbObjectAccessor, @Nullable MongoPersistentProperty idProperty) { DocumentAccessor dbObjectAccessor, @Nullable MongoPersistentProperty idProperty) {
// Write the properties // Write the properties
@@ -489,8 +553,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
} }
} }
private void writeAssociation(Association<MongoPersistentProperty> association, PersistentPropertyAccessor accessor, private void writeAssociation(Association<MongoPersistentProperty> association,
DocumentAccessor dbObjectAccessor) { PersistentPropertyAccessor<?> accessor, DocumentAccessor dbObjectAccessor) {
MongoPersistentProperty inverseProp = association.getInverse(); MongoPersistentProperty inverseProp = association.getInverse();
@@ -555,8 +619,9 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return; return;
} }
MongoPersistentEntity<?> entity = isSubtype(prop.getType(), obj.getClass()) MongoPersistentEntity<?> entity = valueType.isSubTypeOf(prop.getType())
? mappingContext.getRequiredPersistentEntity(obj.getClass()) : mappingContext.getRequiredPersistentEntity(type); ? mappingContext.getRequiredPersistentEntity(obj.getClass())
: mappingContext.getRequiredPersistentEntity(type);
Object existingValue = accessor.get(prop); Object existingValue = accessor.get(prop);
Document document = existingValue instanceof Document ? (Document) existingValue : new Document(); Document document = existingValue instanceof Document ? (Document) existingValue : new Document();
@@ -566,10 +631,6 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
accessor.put(prop, document); accessor.put(prop, document);
} }
private boolean isSubtype(Class<?> left, Class<?> right) {
return left.isAssignableFrom(right) && !left.equals(right);
}
/** /**
* Returns given object as {@link Collection}. Will return the {@link Collection} as is if the source is a * Returns given object as {@link Collection}. Will return the {@link Collection} as is if the source is a
* {@link Collection} already, will convert an array into a {@link Collection} or simply create a single element * {@link Collection} already, will convert an array into a {@link Collection} or simply create a single element
@@ -659,11 +720,13 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
* @param sink the {@link Collection} to write to. * @param sink the {@link Collection} to write to.
* @return * @return
*/ */
private List<Object> writeCollectionInternal(Collection<?> source, @Nullable TypeInformation<?> type, Collection<?> sink) { @SuppressWarnings("unchecked")
private List<Object> writeCollectionInternal(Collection<?> source, @Nullable TypeInformation<?> type,
Collection<?> sink) {
TypeInformation<?> componentType = null; TypeInformation<?> componentType = null;
List<Object> collection = sink instanceof List ? (List) sink : new ArrayList<>(sink); List<Object> collection = sink instanceof List ? (List<Object>) sink : new ArrayList<>(sink);
if (type != null) { if (type != null) {
componentType = type.getComponentType(); componentType = type.getComponentType();
@@ -777,7 +840,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
} }
return conversions.hasCustomWriteTarget(key.getClass(), String.class) return conversions.hasCustomWriteTarget(key.getClass(), String.class)
? (String) getPotentiallyConvertedSimpleWrite(key) : key.toString(); ? (String) getPotentiallyConvertedSimpleWrite(key)
: key.toString();
} }
/** /**
@@ -867,7 +931,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
*/ */
@Nullable @Nullable
@SuppressWarnings({ "rawtypes", "unchecked" }) @SuppressWarnings({ "rawtypes", "unchecked" })
private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, @Nullable Class<?> target) { private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, @Nullable Class<?> target) {
if (value == null || target == null || ClassUtils.isAssignableValue(target, value)) { if (value == null || target == null || ClassUtils.isAssignableValue(target, value)) {
return value; return value;
@@ -912,7 +976,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
throw new MappingException("Cannot create a reference to an object with a NULL id."); throw new MappingException("Cannot create a reference to an object with a NULL id.");
} }
return dbRefResolver.createDbRef(property == null ? null : property.getDBRef(), entity, idMapper.convertId(id)); return dbRefResolver.createDbRef(property == null ? null : property.getDBRef(), entity,
idMapper.convertId(id, idProperty != null ? idProperty.getFieldType() : ObjectId.class));
} }
throw new MappingException("No id property found on class " + entity.getType()); throw new MappingException("No id property found on class " + entity.getType());
@@ -942,9 +1007,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
Assert.notNull(targetType, "Target type must not be null!"); Assert.notNull(targetType, "Target type must not be null!");
Assert.notNull(path, "Object path must not be null!"); Assert.notNull(path, "Object path must not be null!");
Class<?> collectionType = targetType.getType(); Class<?> collectionType = targetType.isSubTypeOf(Collection.class) //
collectionType = Collection.class.isAssignableFrom(collectionType) // ? targetType.getType() //
? collectionType //
: List.class; : List.class;
TypeInformation<?> componentType = targetType.getComponentType() != null // TypeInformation<?> componentType = targetType.getComponentType() != null //
@@ -977,13 +1041,12 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
items.add(read(componentType, (BasicDBObject) element, path)); items.add(read(componentType, (BasicDBObject) element, path));
} else { } else {
if (element instanceof Collection) { if (!Object.class.equals(rawComponentType) && element instanceof Collection) {
if (!rawComponentType.isArray() && !ClassUtils.isAssignable(Iterable.class, rawComponentType)) { if (!rawComponentType.isArray() && !ClassUtils.isAssignable(Iterable.class, rawComponentType)) {
throw new MappingException( throw new MappingException(
String.format(INCOMPATIBLE_TYPES, element, element.getClass(), rawComponentType, path)); String.format(INCOMPATIBLE_TYPES, element, element.getClass(), rawComponentType, path));
} }
} }
if (element instanceof List) { if (element instanceof List) {
items.add(readCollectionOrArray(componentType, (Collection<Object>) element, path)); items.add(readCollectionOrArray(componentType, (Collection<Object>) element, path));
} else { } else {
@@ -1270,12 +1333,14 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
* of the configured source {@link Document}. * of the configured source {@link Document}.
* *
* @author Oliver Gierke * @author Oliver Gierke
* @author Mark Paluch
* @author Christoph Strobl
*/ */
class MongoDbPropertyValueProvider implements PropertyValueProvider<MongoPersistentProperty> { class MongoDbPropertyValueProvider implements PropertyValueProvider<MongoPersistentProperty> {
private final DocumentAccessor source; final DocumentAccessor accessor;
private final SpELExpressionEvaluator evaluator; final SpELExpressionEvaluator evaluator;
private final ObjectPath path; final ObjectPath path;
/** /**
* Creates a new {@link MongoDbPropertyValueProvider} for the given source, {@link SpELExpressionEvaluator} and * Creates a new {@link MongoDbPropertyValueProvider} for the given source, {@link SpELExpressionEvaluator} and
@@ -1285,15 +1350,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
* @param evaluator must not be {@literal null}. * @param evaluator must not be {@literal null}.
* @param path must not be {@literal null}. * @param path must not be {@literal null}.
*/ */
public MongoDbPropertyValueProvider(Bson source, SpELExpressionEvaluator evaluator, ObjectPath path) { MongoDbPropertyValueProvider(Bson source, SpELExpressionEvaluator evaluator, ObjectPath path) {
this(new DocumentAccessor(source), evaluator, path);
Assert.notNull(source, "Source document must no be null!");
Assert.notNull(evaluator, "SpELExpressionEvaluator must not be null!");
Assert.notNull(path, "ObjectPath must not be null!");
this.source = new DocumentAccessor(source);
this.evaluator = evaluator;
this.path = path;
} }
/** /**
@@ -1304,13 +1362,13 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
* @param evaluator must not be {@literal null}. * @param evaluator must not be {@literal null}.
* @param path must not be {@literal null}. * @param path must not be {@literal null}.
*/ */
public MongoDbPropertyValueProvider(DocumentAccessor accessor, SpELExpressionEvaluator evaluator, ObjectPath path) { MongoDbPropertyValueProvider(DocumentAccessor accessor, SpELExpressionEvaluator evaluator, ObjectPath path) {
Assert.notNull(accessor, "DocumentAccessor must no be null!"); Assert.notNull(accessor, "DocumentAccessor must no be null!");
Assert.notNull(evaluator, "SpELExpressionEvaluator must not be null!"); Assert.notNull(evaluator, "SpELExpressionEvaluator must not be null!");
Assert.notNull(path, "ObjectPath must not be null!"); Assert.notNull(path, "ObjectPath must not be null!");
this.source = accessor; this.accessor = accessor;
this.evaluator = evaluator; this.evaluator = evaluator;
this.path = path; this.path = path;
} }
@@ -1323,7 +1381,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
public <T> T getPropertyValue(MongoPersistentProperty property) { public <T> T getPropertyValue(MongoPersistentProperty property) {
String expression = property.getSpelExpression(); String expression = property.getSpelExpression();
Object value = expression != null ? evaluator.evaluate(expression) : source.get(property); Object value = expression != null ? evaluator.evaluate(expression) : accessor.get(property);
if (value == null) { if (value == null) {
return null; return null;
@@ -1333,6 +1391,55 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
} }
} }
/**
* {@link PropertyValueProvider} that is aware of {@link MongoPersistentProperty#isAssociation()} and that delegates
* resolution to {@link DbRefResolver}.
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.1
*/
class AssociationAwareMongoDbPropertyValueProvider extends MongoDbPropertyValueProvider {
/**
* Creates a new {@link AssociationAwareMongoDbPropertyValueProvider} for the given source,
* {@link SpELExpressionEvaluator} and {@link ObjectPath}.
*
* @param source must not be {@literal null}.
* @param evaluator must not be {@literal null}.
* @param path must not be {@literal null}.
*/
AssociationAwareMongoDbPropertyValueProvider(DocumentAccessor source, SpELExpressionEvaluator evaluator,
ObjectPath path) {
super(source, evaluator, path);
}
/*
* (non-Javadoc)
* @see org.springframework.data.convert.PropertyValueProvider#getPropertyValue(org.springframework.data.mapping.PersistentProperty)
*/
@Nullable
@SuppressWarnings("unchecked")
public <T> T getPropertyValue(MongoPersistentProperty property) {
if (property.isDbReference() && property.getDBRef().lazy()) {
Object rawRefValue = accessor.get(property);
if (rawRefValue == null) {
return null;
}
DbRefResolverCallback callback = new DefaultDbRefResolverCallback(accessor.getDocument(), path, evaluator,
MappingMongoConverter.this);
DBRef dbref = rawRefValue instanceof DBRef ? (DBRef) rawRefValue : null;
return (T) dbRefResolver.resolveDbRef(property, dbref, callback, dbRefProxyHandler);
}
return super.getPropertyValue(property);
}
}
/** /**
* Extension of {@link SpELExpressionParameterValueProvider} to recursively trigger value conversion on the raw * Extension of {@link SpELExpressionParameterValueProvider} to recursively trigger value conversion on the raw
* resolved SpEL value. * resolved SpEL value.
@@ -1435,7 +1542,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
} }
List<Document> referencedRawDocuments = dbrefs.size() == 1 List<Document> referencedRawDocuments = dbrefs.size() == 1
? Collections.singletonList(readRef(dbrefs.iterator().next())) : bulkReadRefs(dbrefs); ? Collections.singletonList(readRef(dbrefs.iterator().next()))
: bulkReadRefs(dbrefs);
String collectionName = dbrefs.iterator().next().getCollectionName(); String collectionName = dbrefs.iterator().next().getCollectionName();
List<T> targeList = new ArrayList<>(dbrefs.size()); List<T> targeList = new ArrayList<>(dbrefs.size());
@@ -1527,4 +1635,14 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
static class NestedDocument { static class NestedDocument {
} }
enum NoOpParameterValueProvider implements ParameterValueProvider<MongoPersistentProperty> {
INSTANCE;
@Override
public <T> T getParameterValue(Parameter<T, MongoPersistentProperty> parameter) {
return null;
}
}
} }

View File

@@ -18,6 +18,8 @@ package org.springframework.data.mongodb.core.convert;
import org.bson.BsonValue; import org.bson.BsonValue;
import org.bson.Document; import org.bson.Document;
import org.bson.conversions.Bson; import org.bson.conversions.Bson;
import org.bson.types.ObjectId;
import org.springframework.core.convert.ConversionException;
import org.springframework.data.convert.EntityConverter; import org.springframework.data.convert.EntityConverter;
import org.springframework.data.convert.EntityReader; import org.springframework.data.convert.EntityReader;
import org.springframework.data.convert.TypeMapper; import org.springframework.data.convert.TypeMapper;
@@ -83,7 +85,18 @@ public interface MongoConverter
if (sourceDocument.containsKey("$ref") && sourceDocument.containsKey("$id")) { if (sourceDocument.containsKey("$ref") && sourceDocument.containsKey("$id")) {
sourceDocument = dbRefResolver.fetch(new DBRef(sourceDocument.getString("$ref"), sourceDocument.get("$id"))); Object id = sourceDocument.get("$id");
String collection = sourceDocument.getString("$ref");
MongoPersistentEntity<?> entity = getMappingContext().getPersistentEntity(targetType);
if (entity != null && entity.hasIdProperty()) {
id = convertId(id, entity.getIdProperty().getFieldType());
}
DBRef ref = sourceDocument.containsKey("$db") ? new DBRef(sourceDocument.getString("$db"), collection, id)
: new DBRef(collection, id);
sourceDocument = dbRefResolver.fetch(ref);
if (sourceDocument == null) { if (sourceDocument == null) {
return null; return null;
} }
@@ -102,4 +115,38 @@ public interface MongoConverter
} }
return getConversionService().convert(source, targetType); return getConversionService().convert(source, targetType);
} }
/**
* Converts the given raw id value into either {@link ObjectId} or {@link String}.
*
* @param id
* @param targetType
* @return {@literal null} if source {@literal id} is already {@literal null}.
* @since 2.2
*/
@Nullable
default Object convertId(@Nullable Object id, Class<?> targetType) {
if (id == null) {
return null;
}
if (ClassUtils.isAssignable(ObjectId.class, targetType)) {
if (id instanceof String) {
if (ObjectId.isValid(id.toString())) {
return new ObjectId(id.toString());
}
}
}
try {
return getConversionService().canConvert(id.getClass(), targetType)
? getConversionService().convert(id, targetType)
: convertToMongoType(id, null);
} catch (ConversionException o_O) {
return convertToMongoType(id, null);
}
}
} }

View File

@@ -19,6 +19,7 @@ import java.math.BigDecimal;
import java.math.BigInteger; import java.math.BigInteger;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Currency; import java.util.Currency;
@@ -26,6 +27,7 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import org.bson.BsonTimestamp;
import org.bson.Document; import org.bson.Document;
import org.bson.types.Binary; import org.bson.types.Binary;
import org.bson.types.Code; import org.bson.types.Code;
@@ -86,6 +88,7 @@ abstract class MongoConverters {
converters.add(LongToAtomicLongConverter.INSTANCE); converters.add(LongToAtomicLongConverter.INSTANCE);
converters.add(IntegerToAtomicIntegerConverter.INSTANCE); converters.add(IntegerToAtomicIntegerConverter.INSTANCE);
converters.add(BinaryToByteArrayConverter.INSTANCE); converters.add(BinaryToByteArrayConverter.INSTANCE);
converters.add(BsonTimestampToInstantConverter.INSTANCE);
return converters; return converters;
} }
@@ -465,4 +468,22 @@ abstract class MongoConverters {
return source.getData(); return source.getData();
} }
} }
/**
* {@link Converter} implementation converting {@link BsonTimestamp} into {@link Instant}.
*
* @author Christoph Strobl
* @since 2.1.2
*/
@ReadingConverter
enum BsonTimestampToInstantConverter implements Converter<BsonTimestamp, Instant> {
INSTANCE;
@Nullable
@Override
public Instant convert(BsonTimestamp source) {
return Instant.ofEpochSecond(source.getTime(), 0);
}
}
} }

View File

@@ -164,13 +164,14 @@ public class MongoExampleMapper {
if (exampleSpecAccessor.hasPropertySpecifier(mappedPropertyPath)) { if (exampleSpecAccessor.hasPropertySpecifier(mappedPropertyPath)) {
PropertyValueTransformer valueTransformer = exampleSpecAccessor.getValueTransformerForPath(mappedPropertyPath); PropertyValueTransformer valueTransformer = exampleSpecAccessor.getValueTransformerForPath(mappedPropertyPath);
value = valueTransformer.convert(value); Optional converted = valueTransformer.apply(Optional.ofNullable(value));
if (value == null) {
if(!converted.isPresent()) {
iter.remove(); iter.remove();
continue; continue;
} }
entry.setValue(value); entry.setValue(converted.get());
} }
if (entry.getValue() instanceof String) { if (entry.getValue() instanceof String) {

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.convert;
import java.util.List;
import org.bson.Document;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.lang.Nullable;
import com.mongodb.DBRef;
/**
* No-Operation {@link org.springframework.data.mongodb.core.mapping.DBRef} resolver throwing
* {@link UnsupportedOperationException} when attempting to resolve database references.
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.1
*/
public enum NoOpDbRefResolver implements DbRefResolver {
INSTANCE;
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.DbRefResolver#resolveDbRef(org.springframework.data.mongodb.core.mapping.MongoPersistentProperty, org.springframework.data.mongodb.core.convert.DbRefResolverCallback, org.springframework.data.mongodb.core.convert.DbRefProxyHandler)
*/
@Override
@Nullable
public Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbref, DbRefResolverCallback callback,
DbRefProxyHandler proxyHandler) {
return handle();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.DbRefResolver#fetch(com.mongodb.DBRef)
*/
@Override
@Nullable
public Document fetch(DBRef dbRef) {
return handle();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.DbRefResolver#bulkFetch(java.util.List)
*/
@Override
public List<Document> bulkFetch(List<DBRef> dbRefs) {
return handle();
}
private <T> T handle() throws UnsupportedOperationException {
throw new UnsupportedOperationException("DBRef resolution is not supported!");
}
}

View File

@@ -15,8 +15,6 @@
*/ */
package org.springframework.data.mongodb.core.convert; package org.springframework.data.mongodb.core.convert;
import lombok.Value;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -45,26 +43,33 @@ class ObjectPath {
static final ObjectPath ROOT = new ObjectPath(); static final ObjectPath ROOT = new ObjectPath();
private final ObjectPathItem[] items; private final @Nullable ObjectPath parent;
private final @Nullable Object object;
private final @Nullable Object idValue;
private final String collection;
private ObjectPath() { private ObjectPath() {
this.items = new ObjectPathItem[0];
this.parent = null;
this.object = null;
this.idValue = null;
this.collection = "";
} }
/** /**
* Creates a new {@link ObjectPath} from the given parent {@link ObjectPath} by adding the provided * Creates a new {@link ObjectPath} from the given parent {@link ObjectPath} and adding the provided path values.
* {@link ObjectPathItem} to it.
* *
* @param parent must not be {@literal null}. * @param parent must not be {@literal null}.
* @param item * @param collection
* @param idValue
* @param collection
*/ */
private ObjectPath(ObjectPath parent, ObjectPath.ObjectPathItem item) { private ObjectPath(ObjectPath parent, Object object, @Nullable Object idValue, String collection) {
ObjectPathItem[] items = new ObjectPathItem[parent.items.length + 1]; this.parent = parent;
System.arraycopy(parent.items, 0, items, 0, parent.items.length); this.object = object;
items[parent.items.length] = item; this.idValue = idValue;
this.collection = collection;
this.items = items;
} }
/** /**
@@ -80,8 +85,7 @@ class ObjectPath {
Assert.notNull(object, "Object must not be null!"); Assert.notNull(object, "Object must not be null!");
Assert.notNull(entity, "MongoPersistentEntity must not be null!"); Assert.notNull(entity, "MongoPersistentEntity must not be null!");
ObjectPathItem item = new ObjectPathItem(object, id, entity.getCollection()); return new ObjectPath(this, object, id, entity.getCollection());
return new ObjectPath(this, item);
} }
/** /**
@@ -100,15 +104,15 @@ class ObjectPath {
Assert.notNull(id, "Id must not be null!"); Assert.notNull(id, "Id must not be null!");
Assert.hasText(collection, "Collection name must not be null!"); Assert.hasText(collection, "Collection name must not be null!");
for (ObjectPathItem item : items) { for (ObjectPath current = this; current != null; current = current.parent) {
Object object = item.getObject(); Object object = current.getObject();
if (object == null || item.getIdValue() == null) { if (object == null || current.getIdValue() == null) {
continue; continue;
} }
if (collection.equals(item.getCollection()) && id.equals(item.getIdValue())) { if (collection.equals(current.getCollection()) && id.equals(current.getIdValue())) {
return object; return object;
} }
} }
@@ -133,15 +137,15 @@ class ObjectPath {
Assert.hasText(collection, "Collection name must not be null!"); Assert.hasText(collection, "Collection name must not be null!");
Assert.notNull(type, "Type must not be null!"); Assert.notNull(type, "Type must not be null!");
for (ObjectPathItem item : items) { for (ObjectPath current = this; current != null; current = current.parent) {
Object object = item.getObject(); Object object = current.getObject();
if (object == null || item.getIdValue() == null) { if (object == null || current.getIdValue() == null) {
continue; continue;
} }
if (collection.equals(item.getCollection()) && id.equals(item.getIdValue()) if (collection.equals(current.getCollection()) && id.equals(current.getIdValue())
&& ClassUtils.isAssignable(type, object.getClass())) { && ClassUtils.isAssignable(type, object.getClass())) {
return type.cast(object); return type.cast(object);
} }
@@ -157,7 +161,21 @@ class ObjectPath {
*/ */
@Nullable @Nullable
Object getCurrentObject() { Object getCurrentObject() {
return items.length == 0 ? null : items[items.length - 1].getObject(); return getObject();
}
@Nullable
private Object getObject() {
return object;
}
@Nullable
private Object getIdValue() {
return idValue;
}
private String getCollection() {
return collection;
} }
/* /*
@@ -167,31 +185,16 @@ class ObjectPath {
@Override @Override
public String toString() { public String toString() {
if (items.length == 0) { if (parent == null) {
return "[empty]"; return "[empty]";
} }
List<String> strings = new ArrayList<>(items.length); List<String> strings = new ArrayList<>();
for (ObjectPathItem item : items) { for (ObjectPath current = this; current != null; current = current.parent) {
strings.add(ObjectUtils.nullSafeToString(item.object)); strings.add(ObjectUtils.nullSafeToString(current.getObject()));
} }
return StringUtils.collectionToDelimitedString(strings, " -> "); return StringUtils.collectionToDelimitedString(strings, " -> ");
} }
/**
* An item in an {@link ObjectPath}.
*
* @author Thomas Darimont
* @author Oliver Gierke
* @author Mark Paluch
*/
@Value
private static class ObjectPathItem {
Object object;
@Nullable Object idValue;
String collection;
}
} }

View File

@@ -18,6 +18,7 @@ package org.springframework.data.mongodb.core.convert;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map.Entry; import java.util.Map.Entry;
@@ -28,7 +29,6 @@ import org.bson.BsonValue;
import org.bson.Document; import org.bson.Document;
import org.bson.conversions.Bson; import org.bson.conversions.Bson;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import org.springframework.core.convert.ConversionException;
import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
import org.springframework.data.domain.Example; import org.springframework.data.domain.Example;
@@ -50,6 +50,7 @@ import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation; import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.mongodb.BasicDBList; import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject; import com.mongodb.BasicDBObject;
@@ -244,7 +245,16 @@ public class QueryMapper {
*/ */
protected Field createPropertyField(@Nullable MongoPersistentEntity<?> entity, String key, protected Field createPropertyField(@Nullable MongoPersistentEntity<?> entity, String key,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) { MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
return entity == null ? new Field(key) : new MetadataBackedField(key, entity, mappingContext);
if (entity == null) {
return new Field(key);
}
if (Field.ID_KEY.equals(key)) {
return new MetadataBackedField(key, entity, mappingContext, entity.getIdProperty());
}
return new MetadataBackedField(key, entity, mappingContext);
} }
/** /**
@@ -290,7 +300,7 @@ public class QueryMapper {
*/ */
protected Document getMappedKeyword(Field property, Keyword keyword) { protected Document getMappedKeyword(Field property, Keyword keyword) {
boolean needsAssociationConversion = property.isAssociation() && !keyword.isExists(); boolean needsAssociationConversion = property.isAssociation() && !keyword.isExists() && keyword.mayHoldDbRef();
Object value = keyword.getValue(); Object value = keyword.getValue();
Object convertedValue = needsAssociationConversion ? convertAssociation(value, property) Object convertedValue = needsAssociationConversion ? convertAssociation(value, property)
@@ -312,7 +322,7 @@ public class QueryMapper {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
protected Object getMappedValue(Field documentField, Object value) { protected Object getMappedValue(Field documentField, Object value) {
if (documentField.isIdField()) { if (documentField.isIdField() && !documentField.isAssociation()) {
if (isDBObject(value)) { if (isDBObject(value)) {
DBObject valueDbo = (DBObject) value; DBObject valueDbo = (DBObject) value;
@@ -322,11 +332,11 @@ public class QueryMapper {
String inKey = valueDbo.containsField("$in") ? "$in" : "$nin"; String inKey = valueDbo.containsField("$in") ? "$in" : "$nin";
List<Object> ids = new ArrayList<Object>(); List<Object> ids = new ArrayList<Object>();
for (Object id : (Iterable<?>) valueDbo.get(inKey)) { for (Object id : (Iterable<?>) valueDbo.get(inKey)) {
ids.add(convertId(id)); ids.add(convertId(id, getIdTypeForField(documentField)));
} }
resultDbo.put(inKey, ids); resultDbo.put(inKey, ids);
} else if (valueDbo.containsField("$ne")) { } else if (valueDbo.containsField("$ne")) {
resultDbo.put("$ne", convertId(valueDbo.get("$ne"))); resultDbo.put("$ne", convertId(valueDbo.get("$ne"), getIdTypeForField(documentField)));
} else { } else {
return getMappedObject(resultDbo, Optional.empty()); return getMappedObject(resultDbo, Optional.empty());
} }
@@ -341,18 +351,18 @@ public class QueryMapper {
String inKey = valueDbo.containsKey("$in") ? "$in" : "$nin"; String inKey = valueDbo.containsKey("$in") ? "$in" : "$nin";
List<Object> ids = new ArrayList<Object>(); List<Object> ids = new ArrayList<Object>();
for (Object id : (Iterable<?>) valueDbo.get(inKey)) { for (Object id : (Iterable<?>) valueDbo.get(inKey)) {
ids.add(convertId(id)); ids.add(convertId(id, getIdTypeForField(documentField)));
} }
resultDbo.put(inKey, ids); resultDbo.put(inKey, ids);
} else if (valueDbo.containsKey("$ne")) { } else if (valueDbo.containsKey("$ne")) {
resultDbo.put("$ne", convertId(valueDbo.get("$ne"))); resultDbo.put("$ne", convertId(valueDbo.get("$ne"), getIdTypeForField(documentField)));
} else { } else {
return getMappedObject(resultDbo, Optional.empty()); return getMappedObject(resultDbo, Optional.empty());
} }
return resultDbo; return resultDbo;
} else { } else {
return convertId(value); return convertId(value, getIdTypeForField(documentField));
} }
} }
@@ -367,6 +377,14 @@ public class QueryMapper {
return convertSimpleOrDocument(value, documentField.getPropertyEntity()); return convertSimpleOrDocument(value, documentField.getPropertyEntity());
} }
private boolean isIdField(Field documentField) {
return documentField.getProperty() != null && documentField.getProperty().isIdProperty();
}
private Class<?> getIdTypeForField(Field documentField) {
return isIdField(documentField) ? documentField.getProperty().getFieldType() : ObjectId.class;
}
/** /**
* Returns whether the given {@link Field} represents an association reference that together with the given value * Returns whether the given {@link Field} represents an association reference that together with the given value
* requires conversion to a {@link org.springframework.data.mongodb.core.mapping.DBRef} object. We check whether the * requires conversion to a {@link org.springframework.data.mongodb.core.mapping.DBRef} object. We check whether the
@@ -468,7 +486,14 @@ public class QueryMapper {
if (source instanceof DBRef) { if (source instanceof DBRef) {
DBRef ref = (DBRef) source; DBRef ref = (DBRef) source;
return new DBRef(ref.getCollectionName(), convertId(ref.getId())); Object id = convertId(ref.getId(),
property != null && property.isIdProperty() ? property.getFieldType() : ObjectId.class);
if (StringUtils.hasText(ref.getDatabaseName())) {
return new DBRef(ref.getDatabaseName(), ref.getCollectionName(), id);
} else {
return new DBRef(ref.getCollectionName(), id);
}
} }
if (source instanceof Iterable) { if (source instanceof Iterable) {
@@ -549,24 +574,24 @@ public class QueryMapper {
* *
* @param id * @param id
* @return * @return
* @since 2.2
*/ */
@Nullable @Nullable
public Object convertId(@Nullable Object id) { public Object convertId(@Nullable Object id) {
return convertId(id, ObjectId.class);
}
if (id == null) { /**
return null; * Converts the given raw id value into either {@link ObjectId} or {@link Class targetType}.
} *
* @param id can be {@literal null}.
if (id instanceof String) { * @param targetType
return ObjectId.isValid(id.toString()) ? conversionService.convert(id, ObjectId.class) : id; * @return the converted {@literal id} or {@literal null} if the source was already {@literal null}.
} * @since 2.2
*/
try { @Nullable
return conversionService.canConvert(id.getClass(), ObjectId.class) ? conversionService.convert(id, ObjectId.class) public Object convertId(@Nullable Object id, Class<?> targetType) {
: delegateConvertToMongoType(id, null); return converter.convertId(id, targetType);
} catch (ConversionException o_O) {
return delegateConvertToMongoType(id, null);
}
} }
/** /**
@@ -610,10 +635,12 @@ public class QueryMapper {
static class Keyword { static class Keyword {
private static final String N_OR_PATTERN = "\\$.*or"; private static final String N_OR_PATTERN = "\\$.*or";
private static final Set<String> NON_DBREF_CONVERTING_KEYWORDS = new HashSet<>(Arrays.asList("$", "$size", "$slice", "$gt", "$lt"));
private final String key; private final String key;
private final Object value; private final Object value;
public Keyword(Bson source, String key) { public Keyword(Bson source, String key) {
this.key = key; this.key = key;
this.value = BsonUtils.get(source, key); this.value = BsonUtils.get(source, key);
@@ -674,6 +701,15 @@ public class QueryMapper {
return (T) value; return (T) value;
} }
/**
*
* @return {@literal true} if key may hold a DbRef.
* @since 2.1.4
*/
public boolean mayHoldDbRef() {
return !NON_DBREF_CONVERTING_KEYWORDS.contains(key);
}
/** /**
* Returns whether the current keyword indicates a {@literal $jsonSchema} object. * Returns whether the current keyword indicates a {@literal $jsonSchema} object.
* *
@@ -850,15 +886,18 @@ public class QueryMapper {
/* /*
* (non-Javadoc) * (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.QueryMapper.Field#isIdKey() * @see org.springframework.data.mongodb.core.convert.QueryMapper.Field#isIdField()
*/ */
@Override @Override
public boolean isIdField() { public boolean isIdField() {
MongoPersistentProperty idProperty = entity.getIdProperty(); MongoPersistentProperty idProperty = (property != null && property.isIdProperty()) ? property
: entity.getIdProperty();
if (idProperty != null) { if (idProperty != null) {
return idProperty.getName().equals(name) || idProperty.getFieldName().equals(name);
return name.equals(idProperty.getName()) || name.equals(idProperty.getFieldName())
|| name.endsWith("." + idProperty.getName()) || name.endsWith("." + idProperty.getFieldName());
} }
return DEFAULT_ID_NAMES.contains(name); return DEFAULT_ID_NAMES.contains(name);

View File

@@ -70,7 +70,9 @@ public @interface CompoundIndex {
/** /**
* @return * @return
* @see <a href="https://docs.mongodb.org/manual/core/index-creation/#index-creation-duplicate-dropping">https://docs.mongodb.org/manual/core/index-creation/#index-creation-duplicate-dropping</a> * @see <a href="https://docs.mongodb.org/manual/core/index-creation/#index-creation-duplicate-dropping">https://docs.mongodb.org/manual/core/index-creation/#index-creation-duplicate-dropping</a>
* @deprecated since 2.1. No longer supported by MongoDB as of server version 3.0.
*/ */
@Deprecated
boolean dropDups() default false; boolean dropDups() default false;
/** /**

View File

@@ -36,6 +36,10 @@ import org.springframework.util.StringUtils;
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
public class Index implements IndexDefinition { public class Index implements IndexDefinition {
/**
* @deprecated since 2.1. No longer supported by MongoDB as of server version 3.0.
*/
@Deprecated
public enum Duplicates { public enum Duplicates {
RETAIN RETAIN
} }
@@ -43,7 +47,6 @@ public class Index implements IndexDefinition {
private final Map<String, Direction> fieldSpec = new LinkedHashMap<String, Direction>(); private final Map<String, Direction> fieldSpec = new LinkedHashMap<String, Direction>();
private @Nullable String name; private @Nullable String name;
private boolean unique = false; private boolean unique = false;
private boolean dropDuplicates = false;
private boolean sparse = false; private boolean sparse = false;
private boolean background = false; private boolean background = false;
private long expire = -1; private long expire = -1;
@@ -183,9 +186,6 @@ public class Index implements IndexDefinition {
if (unique) { if (unique) {
document.put("unique", true); document.put("unique", true);
} }
if (dropDuplicates) {
document.put("dropDups", true);
}
if (sparse) { if (sparse) {
document.put("sparse", true); document.put("sparse", true);
} }

View File

@@ -13,21 +13,22 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.data.mongodb.core.index; package org.springframework.data.mongodb.core.index;
/** /**
* TODO: Revisit for a better pattern. * Provider interface to obtain {@link IndexOperations} by MongoDB collection name.
* *
* @author Mark Paluch * @author Mark Paluch
* @author Jens Schauder * @author Jens Schauder
* @since 2.0 * @since 2.0
*/ */
@FunctionalInterface
public interface IndexOperationsProvider { public interface IndexOperationsProvider {
/** /**
* Returns the operations that can be performed on indexes * Returns the operations that can be performed on indexes
* *
* @param collectionName name of the MongoDB collection, must not be {@literal null}.
* @return index operations on the named collection * @return index operations on the named collection
*/ */
IndexOperations indexOps(String collectionName); IndexOperations indexOps(String collectionName);

View File

@@ -56,7 +56,9 @@ public @interface Indexed {
/** /**
* @return * @return
* @see <a href="https://docs.mongodb.org/manual/core/index-creation/#index-creation-duplicate-dropping">https://docs.mongodb.org/manual/core/index-creation/#index-creation-duplicate-dropping</a> * @see <a href="https://docs.mongodb.org/manual/core/index-creation/#index-creation-duplicate-dropping">https://docs.mongodb.org/manual/core/index-creation/#index-creation-duplicate-dropping</a>
* @deprecated since 2.1. No longer supported by MongoDB as of server version 3.0.
*/ */
@Deprecated
boolean dropDups() default false; boolean dropDups() default false;
/** /**

View File

@@ -18,6 +18,7 @@ package org.springframework.data.mongodb.core.index;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationListener;
import org.springframework.data.mapping.context.MappingContextEvent; import org.springframework.data.mapping.context.MappingContextEvent;
import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
@@ -38,7 +39,20 @@ import org.springframework.util.Assert;
*/ */
public class MongoMappingEventPublisher implements ApplicationEventPublisher { public class MongoMappingEventPublisher implements ApplicationEventPublisher {
private final MongoPersistentEntityIndexCreator indexCreator; private final ApplicationListener<MappingContextEvent<?, ?>> indexCreator;
/**
* Creates a new {@link MongoMappingEventPublisher} for the given {@link ApplicationListener}.
*
* @param indexCreator must not be {@literal null}.
* @since 2.1
*/
public MongoMappingEventPublisher(ApplicationListener<MappingContextEvent<?, ?>> indexCreator) {
Assert.notNull(indexCreator, "ApplicationListener must not be null!");
this.indexCreator = indexCreator;
}
/** /**
* Creates a new {@link MongoMappingEventPublisher} for the given {@link MongoPersistentEntityIndexCreator}. * Creates a new {@link MongoMappingEventPublisher} for the given {@link MongoPersistentEntityIndexCreator}.
@@ -48,6 +62,7 @@ public class MongoMappingEventPublisher implements ApplicationEventPublisher {
public MongoMappingEventPublisher(MongoPersistentEntityIndexCreator indexCreator) { public MongoMappingEventPublisher(MongoPersistentEntityIndexCreator indexCreator) {
Assert.notNull(indexCreator, "MongoPersistentEntityIndexCreator must not be null!"); Assert.notNull(indexCreator, "MongoPersistentEntityIndexCreator must not be null!");
this.indexCreator = indexCreator; this.indexCreator = indexCreator;
} }

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.index;
/**
* Provider interface to obtain {@link ReactiveIndexOperations} by MongoDB collection name.
*
* @author Mark Paluch
* @since 2.1
*/
@FunctionalInterface
public interface ReactiveIndexOperationsProvider {
/**
* Returns the operations that can be performed on indexes.
*
* @param collectionName name of the MongoDB collection, must not be {@literal null}.
* @return index operations on the named collection
*/
ReactiveIndexOperations indexOps(String collectionName);
}

View File

@@ -0,0 +1,186 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.index;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.UncategorizedMongoDbException;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolver.IndexDefinitionHolder;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.util.MongoDbErrorCodes;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import com.mongodb.MongoException;
/**
* Component that inspects {@link MongoPersistentEntity} instances contained in the given {@link MongoMappingContext}
* for indexing metadata and ensures the indexes to be available using reactive infrastructure.
*
* @author Mark Paluch
* @since 2.1
*/
public class ReactiveMongoPersistentEntityIndexCreator {
private static final Logger LOGGER = LoggerFactory.getLogger(ReactiveMongoPersistentEntityIndexCreator.class);
private final Map<Class<?>, Boolean> classesSeen = new ConcurrentHashMap<Class<?>, Boolean>();
private final MongoMappingContext mappingContext;
private final ReactiveIndexOperationsProvider operationsProvider;
private final IndexResolver indexResolver;
/**
* Creates a new {@link ReactiveMongoPersistentEntityIndexCreator} for the given {@link MongoMappingContext},
* {@link ReactiveIndexOperationsProvider}.
*
* @param mappingContext must not be {@literal null}.
* @param operationsProvider must not be {@literal null}.
*/
public ReactiveMongoPersistentEntityIndexCreator(MongoMappingContext mappingContext,
ReactiveIndexOperationsProvider operationsProvider) {
this(mappingContext, operationsProvider, new MongoPersistentEntityIndexResolver(mappingContext));
}
/**
* Creates a new {@link ReactiveMongoPersistentEntityIndexCreator} for the given {@link MongoMappingContext},
* {@link ReactiveIndexOperationsProvider}, and {@link IndexResolver}.
*
* @param mappingContext must not be {@literal null}.
* @param operationsProvider must not be {@literal null}.
* @param indexResolver must not be {@literal null}.
*/
public ReactiveMongoPersistentEntityIndexCreator(MongoMappingContext mappingContext,
ReactiveIndexOperationsProvider operationsProvider, IndexResolver indexResolver) {
Assert.notNull(mappingContext, "MongoMappingContext must not be null!");
Assert.notNull(operationsProvider, "ReactiveIndexOperations must not be null!");
Assert.notNull(indexResolver, "IndexResolver must not be null!");
this.mappingContext = mappingContext;
this.operationsProvider = operationsProvider;
this.indexResolver = indexResolver;
}
/**
* Returns whether the current index creator was registered for the given {@link MappingContext}.
*
* @param context
* @return
*/
public boolean isIndexCreatorFor(MappingContext<?, ?> context) {
return this.mappingContext.equals(context);
}
/**
* Inspect entities for index creation.
*
* @return a {@link Mono} that completes without value after indexes were created.
*/
public Mono<Void> checkForIndexes(MongoPersistentEntity<?> entity) {
Class<?> type = entity.getType();
if (!classesSeen.containsKey(type)) {
if (this.classesSeen.put(type, Boolean.TRUE) == null) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Analyzing class " + type + " for index information.");
}
return checkForAndCreateIndexes(entity);
}
}
return Mono.empty();
}
private Mono<Void> checkForAndCreateIndexes(MongoPersistentEntity<?> entity) {
List<Mono<?>> publishers = new ArrayList<>();
if (entity.isAnnotationPresent(Document.class)) {
for (IndexDefinitionHolder indexToCreate : indexResolver.resolveIndexFor(entity.getTypeInformation())) {
publishers.add(createIndex(indexToCreate));
}
}
return publishers.isEmpty() ? Mono.empty() : Flux.merge(publishers).then();
}
Mono<String> createIndex(IndexDefinitionHolder indexDefinition) {
return operationsProvider.indexOps(indexDefinition.getCollection()).ensureIndex(indexDefinition) //
.onErrorResume(ReactiveMongoPersistentEntityIndexCreator::isDataIntegrityViolation,
e -> translateException(e, indexDefinition));
}
private Mono<? extends String> translateException(Throwable e, IndexDefinitionHolder indexDefinition) {
Mono<IndexInfo> existingIndex = fetchIndexInformation(indexDefinition);
Mono<String> defaultError = Mono.error(new DataIntegrityViolationException(
String.format("Cannot create index for '%s' in collection '%s' with keys '%s' and options '%s'.",
indexDefinition.getPath(), indexDefinition.getCollection(), indexDefinition.getIndexKeys(),
indexDefinition.getIndexOptions()),
e.getCause()));
return existingIndex.flatMap(it -> {
return Mono.<String> error(new DataIntegrityViolationException(
String.format("Index already defined as '%s'.", indexDefinition.getPath()), e.getCause()));
}).switchIfEmpty(defaultError);
}
private Mono<IndexInfo> fetchIndexInformation(IndexDefinitionHolder indexDefinition) {
Object indexNameToLookUp = indexDefinition.getIndexOptions().get("name");
Flux<IndexInfo> existingIndexes = operationsProvider.indexOps(indexDefinition.getCollection()).getIndexInfo();
return existingIndexes //
.filter(indexInfo -> ObjectUtils.nullSafeEquals(indexNameToLookUp, indexInfo.getName())) //
.next() //
.doOnError(e -> {
LOGGER.debug(
String.format("Failed to load index information for collection '%s'.", indexDefinition.getCollection()),
e);
});
}
private static boolean isDataIntegrityViolation(Throwable t) {
if (t instanceof UncategorizedMongoDbException) {
return t.getCause() instanceof MongoException
&& MongoDbErrorCodes.isDataIntegrityViolationCode(((MongoException) t.getCause()).getCode());
}
return false;
}
}

View File

@@ -21,11 +21,6 @@ import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.expression.BeanFactoryAccessor;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.mapping.Association; import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.AssociationHandler; import org.springframework.data.mapping.AssociationHandler;
@@ -38,7 +33,6 @@ import org.springframework.expression.Expression;
import org.springframework.expression.ParserContext; import org.springframework.expression.ParserContext;
import org.springframework.expression.common.LiteralExpression; import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
@@ -55,7 +49,7 @@ import org.springframework.util.StringUtils;
* @author Mark Paluch * @author Mark Paluch
*/ */
public class BasicMongoPersistentEntity<T> extends BasicPersistentEntity<T, MongoPersistentProperty> public class BasicMongoPersistentEntity<T> extends BasicPersistentEntity<T, MongoPersistentProperty>
implements MongoPersistentEntity<T>, ApplicationContextAware { implements MongoPersistentEntity<T> {
private static final String AMBIGUOUS_FIELD_MAPPING = "Ambiguous field mapping detected! Both %s and %s map to the same field name %s! Disambiguate using @Field annotation!"; private static final String AMBIGUOUS_FIELD_MAPPING = "Ambiguous field mapping detected! Both %s and %s map to the same field name %s! Disambiguate using @Field annotation!";
private static final SpelExpressionParser PARSER = new SpelExpressionParser(); private static final SpelExpressionParser PARSER = new SpelExpressionParser();
@@ -63,7 +57,6 @@ public class BasicMongoPersistentEntity<T> extends BasicPersistentEntity<T, Mong
private final String collection; private final String collection;
private final String language; private final String language;
private final StandardEvaluationContext context;
private final @Nullable Expression expression; private final @Nullable Expression expression;
/** /**
@@ -79,8 +72,6 @@ public class BasicMongoPersistentEntity<T> extends BasicPersistentEntity<T, Mong
Class<?> rawType = typeInformation.getType(); Class<?> rawType = typeInformation.getType();
String fallback = MongoCollectionUtils.getPreferredCollectionName(rawType); String fallback = MongoCollectionUtils.getPreferredCollectionName(rawType);
this.context = new StandardEvaluationContext();
if (this.isAnnotationPresent(Document.class)) { if (this.isAnnotationPresent(Document.class)) {
Document document = this.getRequiredAnnotation(Document.class); Document document = this.getRequiredAnnotation(Document.class);
@@ -95,23 +86,15 @@ public class BasicMongoPersistentEntity<T> extends BasicPersistentEntity<T, Mong
} }
} }
/*
* (non-Javadoc)
* @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext)
*/
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context.addPropertyAccessor(new BeanFactoryAccessor());
context.setBeanResolver(new BeanFactoryResolver(applicationContext));
context.setRootObject(applicationContext);
}
/* /*
* (non-Javadoc) * (non-Javadoc)
* @see org.springframework.data.mongodb.core.mapping.MongoPersistentEntity#getCollection() * @see org.springframework.data.mongodb.core.mapping.MongoPersistentEntity#getCollection()
*/ */
public String getCollection() { public String getCollection() {
return expression == null ? collection : expression.getValue(context, String.class);
return expression == null //
? collection //
: expression.getValue(getEvaluationContext(null), String.class);
} }
/* /*

View File

@@ -67,8 +67,7 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope
/** /**
* Creates a new {@link BasicMongoPersistentProperty}. * Creates a new {@link BasicMongoPersistentProperty}.
* *
* @param field * @param property
* @param propertyDescriptor
* @param owner * @param owner
* @param simpleTypeHolder * @param simpleTypeHolder
* @param fieldNamingStrategy * @param fieldNamingStrategy
@@ -144,6 +143,32 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope
return fieldName; return fieldName;
} }
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.mapping.MongoPersistentProperty#getFieldType()
*/
@Override
public Class<?> getFieldType() {
if (!isIdProperty()) {
return getType();
}
MongoId idAnnotation = findAnnotation(MongoId.class);
if (idAnnotation == null) {
return FieldType.OBJECT_ID.getJavaClass();
}
FieldType fieldType = idAnnotation.targetType();
if (fieldType == FieldType.IMPLICIT) {
return getType();
}
return fieldType.getJavaClass();
}
/** /**
* @return true if {@link org.springframework.data.mongodb.core.mapping.Field} having non blank * @return true if {@link org.springframework.data.mongodb.core.mapping.Field} having non blank
* {@link org.springframework.data.mongodb.core.mapping.Field#value()} present. * {@link org.springframework.data.mongodb.core.mapping.Field#value()} present.

View File

@@ -24,20 +24,23 @@ import org.springframework.lang.Nullable;
* {@link MongoPersistentProperty} caching access to {@link #isIdProperty()} and {@link #getFieldName()}. * {@link MongoPersistentProperty} caching access to {@link #isIdProperty()} and {@link #getFieldName()}.
* *
* @author Oliver Gierke * @author Oliver Gierke
* @author Mark Paluch
*/ */
public class CachingMongoPersistentProperty extends BasicMongoPersistentProperty { public class CachingMongoPersistentProperty extends BasicMongoPersistentProperty {
private @Nullable Boolean isIdProperty; private @Nullable Boolean isIdProperty;
private @Nullable Boolean isAssociation; private @Nullable Boolean isAssociation;
private @Nullable boolean dbRefResolved;
private @Nullable DBRef dbref;
private @Nullable String fieldName; private @Nullable String fieldName;
private @Nullable Class<?> fieldType;
private @Nullable Boolean usePropertyAccess; private @Nullable Boolean usePropertyAccess;
private @Nullable Boolean isTransient; private @Nullable Boolean isTransient;
/** /**
* Creates a new {@link CachingMongoPersistentProperty}. * Creates a new {@link CachingMongoPersistentProperty}.
* *
* @param field * @param property
* @param propertyDescriptor
* @param owner * @param owner
* @param simpleTypeHolder * @param simpleTypeHolder
* @param fieldNamingStrategy * @param fieldNamingStrategy
@@ -87,6 +90,20 @@ public class CachingMongoPersistentProperty extends BasicMongoPersistentProperty
return this.fieldName; return this.fieldName;
} }
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.mapping.BasicMongoPersistentProperty#getFieldType()
*/
@Override
public Class<?> getFieldType() {
if (this.fieldType == null) {
this.fieldType = super.getFieldType();
}
return this.fieldType;
}
/* /*
* (non-Javadoc) * (non-Javadoc)
* @see org.springframework.data.mapping.model.AnnotationBasedPersistentProperty#usePropertyAccess() * @see org.springframework.data.mapping.model.AnnotationBasedPersistentProperty#usePropertyAccess()
@@ -114,4 +131,28 @@ public class CachingMongoPersistentProperty extends BasicMongoPersistentProperty
return this.isTransient; return this.isTransient;
} }
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.mapping.BasicMongoPersistentProperty#isDbReference()
*/
@Override
public boolean isDbReference() {
return getDBRef() != null;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.mapping.BasicMongoPersistentProperty#getDBRef()
*/
@Override
public DBRef getDBRef() {
if (!dbRefResolved) {
this.dbref = super.getDBRef();
this.dbRefResolved = true;
}
return this.dbref;
}
} }

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.mapping;
import org.bson.types.ObjectId;
/**
* Enumeration of field value types that can be used to represent a {@link org.bson.Document} field value. This
* enumeration contains a subset of {@link org.bson.BsonType} that is supported by the mapping and conversion
* components.
* <p/>
* Bson types are identified by a {@code byte} {@link #getBsonType() value}. This enumeration typically returns the
* according bson type value except for {@link #IMPLICIT} which is a marker to derive the field type from a property.
*
* @author Mark Paluch
* @since 2.2
* @see org.bson.BsonType
*/
public enum FieldType {
/**
* Implicit type that is derived from the property value.
*/
IMPLICIT(-1, Object.class), STRING(2, String.class), OBJECT_ID(7, ObjectId.class);
private final int bsonType;
private final Class<?> javaClass;
FieldType(int bsonType, Class<?> javaClass) {
this.bsonType = bsonType;
this.javaClass = javaClass;
}
/**
* Returns the BSON type identifier. Can be {@code -1} if {@link FieldType} maps to a synthetic Bson type.
*
* @return the BSON type identifier. Can be {@code -1} if {@link FieldType} maps to a synthetic Bson type.
*/
public int getBsonType() {
return bsonType;
}
/**
* Returns the Java class used to represent the type.
*
* @return the Java class used to represent the type.
*/
public Class<?> getJavaClass() {
return javaClass;
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.mapping;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.annotation.Id;
/**
* {@link MongoId} represents a MongoDB specific {@link Id} annotation that allows customizing {@literal id} conversion.
* Id properties use {@link org.springframework.data.mongodb.core.mapping.FieldType#IMPLICIT} as the default
* {@literal id's} target type. This means that the actual property value is used. No conversion attempts to any other
* type are made. <br />
* In contrast to {@link Id &#64;Id}, {@link String} {@literal id's} are stored as the such even when the actual value
* represents a valid {@link org.bson.types.ObjectId#isValid(String) ObjectId hex String}. To trigger {@link String} to
* {@link org.bson.types.ObjectId} conversion use {@link MongoId#targetType() &#64;MongoId(FieldType.OBJECT_ID)}.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.2
*/
@Id
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
public @interface MongoId {
/**
* @return the preferred id type.
* @see #targetType()
*/
@AliasFor("targetType")
FieldType value() default FieldType.IMPLICIT;
/**
* Get the preferred {@literal _id} type to be used. Defaults to {@link FieldType#IMPLICIT} which uses the property's
* type. If defined different, the given value is attempted to be converted into the desired target type via
* {@link org.springframework.data.mongodb.core.convert.MongoConverter#convertId(Object, Class)}.
*
* @return the preferred {@literal id} type. {@link FieldType#IMPLICIT} by default.
*/
@AliasFor("value")
FieldType targetType() default FieldType.IMPLICIT;
}

View File

@@ -87,14 +87,7 @@ public class MongoMappingContext extends AbstractMappingContext<BasicMongoPersis
*/ */
@Override @Override
protected <T> BasicMongoPersistentEntity<T> createPersistentEntity(TypeInformation<T> typeInformation) { protected <T> BasicMongoPersistentEntity<T> createPersistentEntity(TypeInformation<T> typeInformation) {
return new BasicMongoPersistentEntity<T>(typeInformation);
BasicMongoPersistentEntity<T> entity = new BasicMongoPersistentEntity<T>(typeInformation);
if (context != null) {
entity.setApplicationContext(context);
}
return entity;
} }
/* /*
@@ -103,6 +96,9 @@ public class MongoMappingContext extends AbstractMappingContext<BasicMongoPersis
*/ */
@Override @Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
super.setApplicationContext(applicationContext);
this.context = applicationContext; this.context = applicationContext;
} }
} }

View File

@@ -38,6 +38,15 @@ public interface MongoPersistentProperty extends PersistentProperty<MongoPersist
*/ */
String getFieldName(); String getFieldName();
/**
* Returns the {@link Class Java FieldType} of the field a property is persisted to.
*
* @return
* @since 2.2
* @see FieldType
*/
Class<?> getFieldType();
/** /**
* Returns the order of the field if defined. Will return -1 if undefined. * Returns the order of the field if defined. Will return -1 if undefined.
* *

View File

@@ -22,13 +22,13 @@ import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.bson.BsonObjectId; import org.bson.*;
import org.bson.types.Binary; import org.bson.types.Binary;
import org.bson.types.CodeWScope; import org.bson.types.CodeWScope;
import org.bson.types.CodeWithScope;
import org.bson.types.Decimal128;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.mongodb.util.MongoClientVersion;
import org.springframework.util.ClassUtils;
import com.mongodb.DBRef; import com.mongodb.DBRef;
@@ -54,15 +54,29 @@ public abstract class MongoSimpleTypes {
simpleTypes.add(ObjectId.class); simpleTypes.add(ObjectId.class);
simpleTypes.add(BsonObjectId.class); simpleTypes.add(BsonObjectId.class);
simpleTypes.add(CodeWScope.class); simpleTypes.add(CodeWScope.class);
simpleTypes.add(CodeWithScope.class);
simpleTypes.add(org.bson.Document.class); simpleTypes.add(org.bson.Document.class);
simpleTypes.add(Pattern.class); simpleTypes.add(Pattern.class);
simpleTypes.add(Binary.class); simpleTypes.add(Binary.class);
simpleTypes.add(UUID.class); simpleTypes.add(UUID.class);
simpleTypes.add(Decimal128.class);
if (MongoClientVersion.isMongo34Driver()) { simpleTypes.add(BsonBinary.class);
simpleTypes simpleTypes.add(BsonBoolean.class);
.add(ClassUtils.resolveClassName("org.bson.types.Decimal128", MongoSimpleTypes.class.getClassLoader())); simpleTypes.add(BsonDateTime.class);
} simpleTypes.add(BsonDbPointer.class);
simpleTypes.add(BsonDecimal128.class);
simpleTypes.add(BsonDocument.class);
simpleTypes.add(BsonDocument.class);
simpleTypes.add(BsonDouble.class);
simpleTypes.add(BsonInt32.class);
simpleTypes.add(BsonInt64.class);
simpleTypes.add(BsonJavaScript.class);
simpleTypes.add(BsonJavaScriptWithScope.class);
simpleTypes.add(BsonObjectId.class);
simpleTypes.add(BsonRegularExpression.class);
simpleTypes.add(BsonString.class);
simpleTypes.add(BsonTimestamp.class);
MONGO_SIMPLE_TYPES = Collections.unmodifiableSet(simpleTypes); MONGO_SIMPLE_TYPES = Collections.unmodifiableSet(simpleTypes);
} }

View File

@@ -15,8 +15,6 @@
*/ */
package org.springframework.data.mongodb.core.mapping.event; package org.springframework.data.mongodb.core.mapping.event;
import java.util.Optional;
import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.ObjectFactory;
import org.springframework.context.ApplicationListener; import org.springframework.context.ApplicationListener;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
@@ -53,9 +51,7 @@ public class AuditingEventListener implements ApplicationListener<BeforeConvertE
*/ */
@Override @Override
public void onApplicationEvent(BeforeConvertEvent<Object> event) { public void onApplicationEvent(BeforeConvertEvent<Object> event) {
event.mapSource(it -> auditingHandlerFactory.getObject().markAudited(it));
Optional.ofNullable(event.getSource())//
.ifPresent(it -> auditingHandlerFactory.getObject().markAudited(it));
} }
/* /*

Some files were not shown because too many files have changed in this diff Show More