Compare commits

..

154 Commits

Author SHA1 Message Date
Mark Paluch
bd8bd4f568 DATAMONGO-2121 - Release version 2.1.3 (Lovelace SR3). 2018-11-27 13:43:24 +01:00
Mark Paluch
c75f29dc42 DATAMONGO-2121 - Prepare 2.1.3 (Lovelace SR3). 2018-11-27 13:42:17 +01:00
Mark Paluch
e493af7266 DATAMONGO-2121 - Updated changelog. 2018-11-27 13:42:07 +01:00
Mark Paluch
8d892e5924 DATAMONGO-2109 - Updated changelog. 2018-11-27 12:36:47 +01:00
Mark Paluch
053299f243 DATAMONGO-2110 - Updated changelog. 2018-11-27 11:27:21 +01:00
Mark Paluch
872659cc00 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:26 +01:00
Christoph Strobl
96978a6194 DATAMONGO-2119 - Allow SpEL usage for annotated $regex query.
Original pull request: #621.
2018-11-23 09:48:26 +01:00
Oliver Drotbohm
2253d3e301 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:31 +01:00
Mark Paluch
5982ee84f7 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:40 +01:00
Christoph Strobl
dd2af6462d DATAMONGO-2130 - Polishing.
Set timeout for InetAdress host lookup to reduce test execution time.

Original pull request: #618.
2018-11-16 12:58:40 +01:00
Christoph Strobl
622643bf24 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:40 +01:00
Oliver Drotbohm
51cc55baac 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-15 15:26:36 +01:00
Mark Paluch
0b106e5649 DATAMONGO-2107 - After release cleanups. 2018-10-29 13:59:17 +01:00
Mark Paluch
8975d93ab3 DATAMONGO-2107 - Prepare next development iteration. 2018-10-29 13:59:15 +01:00
Mark Paluch
e25b6c49f5 DATAMONGO-2107 - Release version 2.1.2 (Lovelace SR2). 2018-10-29 12:53:51 +01:00
Mark Paluch
7a70c205de DATAMONGO-2107 - Prepare 2.1.2 (Lovelace SR2). 2018-10-29 12:52:54 +01:00
Mark Paluch
6045efa450 DATAMONGO-2107 - Updated changelog. 2018-10-29 12:52:45 +01:00
Mark Paluch
7b0816b3ee DATAMONGO-2118 - Polishing.
Fix typo in reactive repositories reference documentation.

Original pull request: #611.
2018-10-26 10:08:03 +02:00
Mona Mohamadinia
14e4ea736d DATAMONGO-2118 - Fix typo in repositories reference documentation.
Original pull request: #611.
2018-10-26 10:08:03 +02:00
Mark Paluch
32e7d9ab7f DATAMONGO-2098 - Polishing.
Annotate methods and parameters with Nullable. Use diamond syntax where appropriate.

Original pull request: #612.
2018-10-25 15:35:26 +02:00
Zied Yaich
7f35ad9e45 DATAMONGO-2098 - Fix typo in MappingMongoConverterParser method.
Original pull request: #612.
2018-10-25 15:35:26 +02:00
Mark Paluch
60228f6e5a 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:28 +02:00
Christoph Strobl
7604492b7f DATAMONGO-2113 - Polishing.
Use AssertJ in tests.

Original pull request: #615.
2018-10-25 14:33:28 +02:00
Christoph Strobl
4680fe0e77 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:28 +02:00
Mark Paluch
b4228c88d3 DATAMONGO-2083 - Updated changelog. 2018-10-15 14:19:03 +02:00
Mark Paluch
f6ef8c94c8 DATAMONGO-2084 - Updated changelog. 2018-10-15 12:46:24 +02:00
Mark Paluch
0d0dafa85e DATAMONGO-2094 - After release cleanups. 2018-10-15 11:12:14 +02:00
Mark Paluch
29aa34619f DATAMONGO-2094 - Prepare next development iteration. 2018-10-15 11:12:12 +02:00
Mark Paluch
7f19f769c4 DATAMONGO-2094 - Release version 2.1.1 (Lovelace SR1). 2018-10-15 10:42:04 +02:00
Mark Paluch
a40e89d90a DATAMONGO-2094 - Prepare 2.1.1 (Lovelace SR1). 2018-10-15 10:40:57 +02:00
Mark Paluch
6b2350200a DATAMONGO-2094 - Updated changelog. 2018-10-15 10:40:53 +02:00
Mark Paluch
fb50b0f6e7 DATAMONGO-2096 - Polishing.
Migrate assertions to AssertJ.

Original pull request: #613.
2018-10-05 15:02:38 +02:00
Christoph Strobl
ab568229b5 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:38 +02:00
Mark Paluch
7f9c1bd774 DATAMONGO-2061 - After release cleanups. 2018-09-21 07:46:17 -04:00
Mark Paluch
670a0978da DATAMONGO-2061 - Prepare next development iteration. 2018-09-21 07:46:16 -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
203 changed files with 11932 additions and 2834 deletions

View File

@@ -13,12 +13,12 @@ before_install:
- |-
downloads/mongodb-linux-x86_64-ubuntu1604-${MONGO_VERSION}/bin/mongo --eval "rs.initiate({_id: 'rs0', members:[{_id: 0, host: '127.0.0.1:27017'}]});"
sleep 15
env:
matrix:
- PROFILE=ci
global:
- MONGO_VERSION=3.7.9
- MONGO_VERSION=4.0.0
addons:
apt:

32
pom.xml
View File

@@ -5,7 +5,7 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>2.1.0.M3</version>
<version>2.1.3.RELEASE</version>
<packaging>pom</packaging>
<name>Spring Data MongoDB</name>
@@ -15,7 +15,7 @@
<parent>
<groupId>org.springframework.data.build</groupId>
<artifactId>spring-data-parent</artifactId>
<version>2.1.0.M3</version>
<version>2.1.3.RELEASE</version>
</parent>
<modules>
@@ -27,9 +27,9 @@
<properties>
<project.type>multi</project.type>
<dist.id>spring-data-mongodb</dist.id>
<springdata.commons>2.1.0.M3</springdata.commons>
<mongo>3.8.0-beta2</mongo>
<mongo.reactivestreams>1.9.0-beta1</mongo.reactivestreams>
<springdata.commons>2.1.3.RELEASE</springdata.commons>
<mongo>3.8.2</mongo>
<mongo.reactivestreams>1.9.2</mongo.reactivestreams>
<jmh.version>1.19</jmh.version>
</properties>
@@ -138,6 +138,24 @@
</modules>
</profile>
<profile>
<id>distribute</id>
<build>
<plugins>
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<configuration>
<attributes>
<mongo-reactivestreams>${mongo.reactivestreams}</mongo-reactivestreams>
<reactor>${reactor}</reactor>
</attributes>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<dependencies>
@@ -151,8 +169,8 @@
<repositories>
<repository>
<id>spring-libs-milestone</id>
<url>https://repo.spring.io/libs-milestone</url>
<id>spring-libs-release</id>
<url>https://repo.spring.io/libs-release</url>
</repository>
</repositories>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>2.1.0.M3</version>
<version>2.1.3.RELEASE</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

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

View File

@@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>2.1.0.M3</version>
<version>2.1.3.RELEASE</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -11,7 +11,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>2.1.0.M3</version>
<version>2.1.3.RELEASE</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -110,7 +110,7 @@ public class MongoDatabaseUtils {
ClientSession session = doGetSession(factory, sessionSynchronization);
if(session == null) {
if (session == null) {
return StringUtils.hasText(dbName) ? factory.getDb(dbName) : factory.getDb();
}
@@ -118,6 +118,25 @@ public class MongoDatabaseUtils {
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) {
@@ -140,7 +159,7 @@ public class MongoDatabaseUtils {
// init a non native MongoDB transaction by registering a MongoSessionSynchronization
resourceHolder = new MongoResourceHolder(createClientSession(dbFactory), dbFactory);
resourceHolder.getSession().startTransaction();
resourceHolder.getRequiredSession().startTransaction();
TransactionSynchronizationManager
.registerSynchronization(new MongoSessionSynchronization(resourceHolder, dbFactory));
@@ -187,8 +206,8 @@ public class MongoDatabaseUtils {
@Override
protected void processResourceAfterCommit(MongoResourceHolder resourceHolder) {
if (isTransactionActive(resourceHolder)) {
resourceHolder.getSession().commitTransaction();
if (resourceHolder.hasActiveTransaction()) {
resourceHolder.getRequiredSession().commitTransaction();
}
}
@@ -199,8 +218,8 @@ public class MongoDatabaseUtils {
@Override
public void afterCompletion(int status) {
if (status == TransactionSynchronization.STATUS_ROLLED_BACK && isTransactionActive(this.resourceHolder)) {
resourceHolder.getSession().abortTransaction();
if (status == TransactionSynchronization.STATUS_ROLLED_BACK && this.resourceHolder.hasActiveTransaction()) {
resourceHolder.getRequiredSession().abortTransaction();
}
super.afterCompletion(status);
@@ -214,17 +233,8 @@ public class MongoDatabaseUtils {
protected void releaseResource(MongoResourceHolder resourceHolder, Object resourceKey) {
if (resourceHolder.hasActiveSession()) {
resourceHolder.getSession().close();
resourceHolder.getRequiredSession().close();
}
}
private boolean isTransactionActive(MongoResourceHolder resourceHolder) {
if (!resourceHolder.hasSession()) {
return false;
}
return resourceHolder.getSession().hasActiveTransaction();
}
}
}

View File

@@ -62,7 +62,10 @@ public interface MongoDbFactory extends CodecRegistryProvider, MongoSessionProvi
* 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();
/**
@@ -105,4 +108,15 @@ public interface MongoDbFactory extends CodecRegistryProvider, MongoSessionProvi
* @since 2.1
*/
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

@@ -28,6 +28,7 @@ import com.mongodb.client.ClientSession;
* <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
@@ -57,6 +58,22 @@ class MongoResourceHolder extends ResourceHolderSupport {
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}.
*/
@@ -101,7 +118,21 @@ class MongoResourceHolder extends ResourceHolderSupport {
return false;
}
return hasServerSession() && !getSession().getServerSession().isClosed();
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();
}
/**
@@ -111,7 +142,7 @@ class MongoResourceHolder extends ResourceHolderSupport {
boolean hasServerSession() {
try {
return getSession().getServerSession() != null;
return getRequiredSession().getServerSession() != null;
} catch (IllegalStateException serverSessionClosed) {
// ignore
}

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

@@ -47,13 +47,18 @@ import com.mongodb.client.ClientSession;
* 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)
* @see MongoDatabaseUtils#getDatabase(MongoDbFactory, SessionSynchronization)
*/
public class MongoTransactionManager extends AbstractPlatformTransactionManager
implements ResourceTransactionManager, InitializingBean {
@@ -70,7 +75,7 @@ public class MongoTransactionManager extends AbstractPlatformTransactionManager
* <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)
*/
@@ -181,7 +186,7 @@ public class MongoTransactionManager extends AbstractPlatformTransactionManager
* org.springframework.transaction.support.AbstractPlatformTransactionManager#doCommit(org.springframework.transaction.support.DefaultTransactionStatus)
*/
@Override
protected void doCommit(DefaultTransactionStatus status) throws TransactionException {
protected final void doCommit(DefaultTransactionStatus status) throws TransactionException {
MongoTransactionObject mongoTransactionObject = extractMongoTransaction(status);
@@ -191,14 +196,45 @@ public class MongoTransactionManager extends AbstractPlatformTransactionManager
}
try {
mongoTransactionObject.commitTransaction();
} catch (MongoException ex) {
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)
@@ -386,7 +422,7 @@ public class MongoTransactionManager extends AbstractPlatformTransactionManager
* @since 2.1
* @see MongoResourceHolder
*/
static class MongoTransactionObject implements SmartTransactionObject {
protected static class MongoTransactionObject implements SmartTransactionObject {
private @Nullable MongoResourceHolder resourceHolder;
@@ -406,7 +442,7 @@ public class MongoTransactionManager extends AbstractPlatformTransactionManager
/**
* @return {@literal true} if a {@link MongoResourceHolder} is set.
*/
boolean hasResourceHolder() {
final boolean hasResourceHolder() {
return resourceHolder != null;
}
@@ -428,14 +464,14 @@ public class MongoTransactionManager extends AbstractPlatformTransactionManager
/**
* Commit the transaction.
*/
void commitTransaction() {
public void commitTransaction() {
getRequiredSession().commitTransaction();
}
/**
* Rollback (abort) the transaction.
*/
void abortTransaction() {
public void abortTransaction() {
getRequiredSession().abortTransaction();
}
@@ -451,7 +487,7 @@ public class MongoTransactionManager extends AbstractPlatformTransactionManager
}
@Nullable
ClientSession getSession() {
public ClientSession getSession() {
return resourceHolder != null ? resourceHolder.getSession() : null;
}

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.SimpleReactiveMongoDatabaseFactory;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
import com.mongodb.reactivestreams.client.MongoClient;
@@ -80,8 +81,7 @@ public abstract class AbstractReactiveMongoConfiguration extends MongoConfigurat
@Bean
public MappingMongoConverter mappingMongoConverter() throws Exception {
MappingMongoConverter converter = new MappingMongoConverter(ReactiveMongoTemplate.NO_OP_REF_RESOLVER,
mongoMappingContext());
MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mongoMappingContext());
converter.setCustomConversions(customConversions());
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.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.event.ValidatingMongoEventListener;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
@@ -75,6 +76,7 @@ import org.w3c.dom.Element;
* @author Thomas Darimont
* @author Christoph Strobl
* @author Mark Paluch
* @author Zied Yaich
*/
public class MappingMongoConverterParser implements BeanDefinitionParser {
@@ -159,6 +161,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
return null;
}
@Nullable
private BeanDefinition potentiallyCreateValidatingMongoEventListener(Element element, ParserContext parserContext) {
String disableValidation = element.getAttribute("disable-validation");
@@ -180,6 +183,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
return null;
}
@Nullable
private RuntimeBeanReference getValidator(Object source, ParserContext parserContext) {
if (!JSR_303_PRESENT) {
@@ -197,7 +201,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
}
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");
@@ -211,7 +215,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
BeanDefinitionBuilder mappingContextBuilder = BeanDefinitionBuilder
.genericBeanDefinition(MongoMappingContext.class);
Set<String> classesToAdd = getInititalEntityClasses(element);
Set<String> classesToAdd = getInitialEntityClasses(element);
if (classesToAdd != null) {
mappingContextBuilder.addPropertyValue("initialEntitySet", classesToAdd);
@@ -262,6 +266,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
}
}
@Nullable
private BeanDefinition getCustomConversions(Element element, ParserContext parserContext) {
List<Element> customConvertersElements = DomUtils.getChildElementsByTagName(element, "custom-converters");
@@ -269,7 +274,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
if (customConvertersElements.size() == 1) {
Element customerConvertersElement = customConvertersElements.get(0);
ManagedList<BeanMetadataElement> converterBeans = new ManagedList<BeanMetadataElement>();
ManagedList<BeanMetadataElement> converterBeans = new ManagedList<>();
List<Element> converterElements = DomUtils.getChildElementsByTagName(customerConvertersElement, "converter");
if (converterElements != null) {
@@ -285,9 +290,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
provider.addExcludeFilter(new NegatingFilter(new AssignableTypeFilter(Converter.class),
new AssignableTypeFilter(GenericConverter.class)));
for (BeanDefinition candidate : provider.findCandidateComponents(packageToScan)) {
converterBeans.add(candidate);
}
converterBeans.addAll(provider.findCandidateComponents(packageToScan));
}
BeanDefinitionBuilder conversionsBuilder = BeanDefinitionBuilder.rootBeanDefinition(MongoCustomConversions.class);
@@ -304,7 +307,8 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
return null;
}
private static Set<String> getInititalEntityClasses(Element element) {
@Nullable
private static Set<String> getInitialEntityClasses(Element element) {
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(Persistent.class));
Set<String> classes = new ManagedSet<String>();
Set<String> classes = new ManagedSet<>();
for (BeanDefinition candidate : componentProvider.findCandidateComponents(basePackage)) {
classes.add(candidate.getBeanClassName());
}
@@ -325,6 +329,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
return classes;
}
@Nullable
public BeanMetadataElement parseConverter(Element element, ParserContext parserContext) {
String converterRef = element.getAttribute("ref");
@@ -375,7 +380,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
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 Oliver Gierke
* @author Stephen Tyler Conrad
* @author Mark Paluch
* @since 1.7
*/
public class MongoCredentialPropertyEditor extends PropertyEditorSupport {
@@ -98,6 +100,12 @@ public class MongoCredentialPropertyEditor extends PropertyEditorSupport {
verifyDatabasePresent(database);
credentials.add(MongoCredential.createScramSha1Credential(userNameAndPassword[0], database,
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 {
throw new IllegalArgumentException(
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) {
int optionsSeparationIndex = text.lastIndexOf(OPTIONS_DELIMITER);
int dbSeparationIndex = text.lastIndexOf(OPTIONS_DELIMITER);
int dbSeparationIndex = text.lastIndexOf(DATABASE_DELIMITER);
if (optionsSeparationIndex == -1 || dbSeparationIndex > optionsSeparationIndex) {
return new Properties();
@@ -173,7 +181,13 @@ public class MongoCredentialPropertyEditor extends PropertyEditorSupport {
Properties properties = new Properties();
for (String option : text.substring(optionsSeparationIndex + 1).split(OPTION_VALUE_DELIMITER)) {
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]);
}

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,10 @@ package org.springframework.data.mongodb.core;
import lombok.EqualsAndHashCode;
import java.util.concurrent.atomic.AtomicReference;
import java.time.Instant;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import org.bson.BsonValue;
import org.bson.Document;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.messaging.Message;
@@ -26,6 +28,7 @@ import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
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
@@ -38,11 +41,17 @@ import com.mongodb.client.model.changestream.ChangeStreamDocument;
@EqualsAndHashCode
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 Class<T> targetType;
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}.
@@ -67,6 +76,58 @@ public class ChangeStreamEvent<T> {
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 raw != null && raw.getClusterTime() != null
? converter.getConversionService().convert(raw.getClusterTime(), Instant.class) : 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()}.
*
@@ -80,36 +141,48 @@ public class ChangeStreamEvent<T> {
return null;
}
if (raw.getFullDocument() == null) {
return targetType.cast(raw.getFullDocument());
Document fullDocument = 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) {
return result;
}
if (ClassUtils.isAssignable(Document.class, raw.getFullDocument().getClass())) {
if (ClassUtils.isAssignable(Document.class, fullDocument.getClass())) {
result = converter.read(targetType, raw.getFullDocument());
return converted.compareAndSet(null, result) ? result : converted.get();
result = converter.read(targetType, fullDocument);
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);
return converted.compareAndSet(null, result) ? result : converted.get();
result = converter.getConversionService().convert(fullDocument, targetType);
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",
raw.getFullDocument().getClass(), targetType));
fullDocument.getClass(), targetType));
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "ChangeStreamEvent {" + "raw=" + raw + ", targetType=" + targetType + '}';

View File

@@ -17,6 +17,7 @@ package org.springframework.data.mongodb.core;
import lombok.EqualsAndHashCode;
import java.time.Instant;
import java.util.Arrays;
import java.util.Optional;
@@ -46,6 +47,7 @@ public class ChangeStreamOptions {
private @Nullable BsonValue resumeToken;
private @Nullable FullDocument fullDocumentLookup;
private @Nullable Collation collation;
private @Nullable Instant resumeTimestamp;
protected ChangeStreamOptions() {}
@@ -77,6 +79,13 @@ public class ChangeStreamOptions {
return Optional.ofNullable(collation);
}
/**
* @return {@link Optional#empty()} if not set.
*/
public Optional<Instant> getResumeTimestamp() {
return Optional.ofNullable(resumeTimestamp);
}
/**
* @return empty {@link ChangeStreamOptions}.
*/
@@ -106,6 +115,7 @@ public class ChangeStreamOptions {
private @Nullable BsonValue resumeToken;
private @Nullable FullDocument fullDocumentLookup;
private @Nullable Collation collation;
private @Nullable Instant resumeTimestamp;
private ChangeStreamOptionsBuilder() {}
@@ -200,6 +210,20 @@ public class ChangeStreamOptions {
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;
}
/**
* @return the built {@link ChangeStreamOptions}
*/
@@ -211,6 +235,7 @@ public class ChangeStreamOptions {
options.resumeToken = resumeToken;
options.fullDocumentLookup = fullDocumentLookup;
options.collation = collation;
options.resumeTimestamp = resumeTimestamp;
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

@@ -119,11 +119,11 @@ class ExecutableAggregationOperationSupport implements ExecutableAggregationOper
TypedAggregation<?> typedAggregation = (TypedAggregation<?>) aggregation;
if (typedAggregation.getInputType() != null) {
return template.determineCollectionName(typedAggregation.getInputType());
return template.getCollectionName(typedAggregation.getInputType());
}
}
return template.determineCollectionName(domainType);
return template.getCollectionName(domainType);
}
}
}

View File

@@ -230,7 +230,7 @@ class ExecutableFindOperationSupport implements ExecutableFindOperation {
}
private String getCollectionName() {
return StringUtils.hasText(collection) ? collection : template.determineCollectionName(domainType);
return StringUtils.hasText(collection) ? collection : template.getCollectionName(domainType);
}
private String asString() {

View File

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

View File

@@ -72,11 +72,11 @@ class ExecutableInsertOperationSupport implements ExecutableInsertOperation {
* @see org.springframework.data.mongodb.core.ExecutableInsertOperation.TerminatingInsert#insert(java.lang.Class)
*/
@Override
public void one(T object) {
public T one(T object) {
Assert.notNull(object, "Object must not be null!");
template.insert(object, getCollectionName());
return template.insert(object, getCollectionName());
}
/*
@@ -84,11 +84,11 @@ class ExecutableInsertOperationSupport implements ExecutableInsertOperation {
* @see org.springframework.data.mongodb.core.ExecutableInsertOperation.TerminatingInsert#all(java.util.Collection)
*/
@Override
public void all(Collection<? extends T> objects) {
public Collection<T> all(Collection<? extends T> objects) {
Assert.notNull(objects, "Objects must not be null!");
template.insert(objects, getCollectionName());
return template.insert(objects, getCollectionName());
}
/*
@@ -129,7 +129,7 @@ class ExecutableInsertOperationSupport implements ExecutableInsertOperation {
}
private String getCollectionName() {
return StringUtils.hasText(collection) ? collection : template.determineCollectionName(domainType);
return StringUtils.hasText(collection) ? collection : template.getCollectionName(domainType);
}
}
}

View File

@@ -15,10 +15,11 @@
*/
package org.springframework.data.mongodb.core;
import java.util.List;
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;
@@ -67,8 +68,9 @@ class ExecutableMapReduceOperationSupport implements ExecutableMapReduceOperatio
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) {
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;
@@ -169,7 +171,7 @@ class ExecutableMapReduceOperationSupport implements ExecutableMapReduceOperatio
}
private String getCollectionName() {
return StringUtils.hasText(collection) ? collection : template.determineCollectionName(domainType);
return StringUtils.hasText(collection) ? collection : template.getCollectionName(domainType);
}
}
}

View File

@@ -53,26 +53,6 @@ public interface ExecutableRemoveOperation {
*/
<T> ExecutableRemove<T> remove(Class<T> domainType);
/**
* Collection override (optional).
*
* @param <T>
* @author Christoph Strobl
* @since 2.0
*/
interface RemoveWithCollection<T> extends RemoveWithQuery<T> {
/**
* Explicitly set the name of the collection to perform the query 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 RemoveWithCollection}.
* @throws IllegalArgumentException if collection is {@literal null}.
*/
RemoveWithQuery<T> inCollection(String collection);
}
/**
* @author Christoph Strobl
* @since 2.0
@@ -104,6 +84,27 @@ public interface ExecutableRemoveOperation {
List<T> findAndRemove();
}
/**
* Collection override (optional).
*
* @param <T>
* @author Christoph Strobl
* @since 2.0
*/
interface RemoveWithCollection<T> extends RemoveWithQuery<T> {
/**
* Explicitly set the name of the collection to perform the query 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 RemoveWithCollection}.
* @throws IllegalArgumentException if collection is {@literal null}.
*/
RemoveWithQuery<T> inCollection(String collection);
}
/**
* @author Christoph Strobl
* @since 2.0

View File

@@ -123,7 +123,7 @@ class ExecutableRemoveOperationSupport implements ExecutableRemoveOperation {
}
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.Update;
import com.mongodb.client.result.UpdateResult;
import org.springframework.lang.Nullable;
import com.mongodb.client.result.UpdateResult;
/**
* {@link ExecutableUpdateOperation} allows creation and execution of MongoDB update / findAndModify operations in a
* fluent API style. <br />
* {@link ExecutableUpdateOperation} allows creation and execution of MongoDB update / findAndModify / 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 {@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
@@ -57,6 +57,91 @@ public interface ExecutableUpdateOperation {
*/
<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.
*
@@ -73,6 +158,16 @@ public interface ExecutableUpdateOperation {
* @throws IllegalArgumentException if update is {@literal null}.
*/
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() {
return Optional.ofNullable(findAndModifyValue());
}
/**
* Find, modify and return the first matching document.
*
* @return {@literal null} if nothing found.
*/
@Nullable
T findAndModifyValue();
FindAndReplaceWithProjection<T> withOptions(FindAndReplaceOptions options);
}
/**
* Trigger update execution by calling one of the terminating methods.
* Result type override (Optional).
*
* @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

@@ -51,7 +51,7 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
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);
}
/**
@@ -61,14 +61,18 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
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 Class<T> domainType;
@NonNull Class domainType;
Query query;
@Nullable Update update;
@Nullable String collection;
@Nullable FindAndModifyOptions options;
@Nullable FindAndModifyOptions findAndModifyOptions;
@Nullable FindAndReplaceOptions findAndReplaceOptions;
@Nullable Object replacement;
@NonNull Class<T> targetType;
/*
* (non-Javadoc)
@@ -79,7 +83,8 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
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);
}
/*
@@ -91,7 +96,8 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
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);
}
/*
@@ -103,7 +109,34 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
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);
}
/*
@@ -115,7 +148,21 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
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);
}
/*
@@ -151,7 +198,22 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
*/
@Override
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) {
@@ -159,7 +221,7 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
}
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
*
* @return a new instance
* @return new instance of {@link FindAndModifyOptions}.
*/
public static FindAndModifyOptions options() {
return new FindAndModifyOptions();
}
/**
* @param options
* @return
* Create new {@link FindAndModifyOptions} based on option of given {@litearl source}.
*
* @param source can be {@literal null}.
* @return new instance of {@link FindAndModifyOptions}.
* @since 2.0
*/
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

@@ -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

@@ -233,6 +233,15 @@ public abstract class MongoDbFactorySupport<C> implements MongoDbFactory {
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);
}
@@ -241,7 +250,8 @@ public abstract class MongoDbFactorySupport<C> implements MongoDbFactory {
return createProxyInstance(session, database, MongoDatabase.class);
}
private MongoCollection<?> proxyCollection(com.mongodb.session.ClientSession session, MongoCollection<?> collection) {
private MongoCollection<?> proxyCollection(com.mongodb.session.ClientSession session,
MongoCollection<?> collection) {
return createProxyInstance(session, collection, MongoCollection.class);
}

View File

@@ -16,6 +16,7 @@
package org.springframework.data.mongodb.core;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
@@ -30,6 +31,7 @@ import org.springframework.dao.PermissionDeniedDataAccessException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.mongodb.BulkOperationException;
import org.springframework.data.mongodb.ClientSessionException;
import org.springframework.data.mongodb.MongoTransactionException;
import org.springframework.data.mongodb.UncategorizedMongoDbException;
import org.springframework.data.mongodb.util.MongoDbErrorCodes;
import org.springframework.lang.Nullable;
@@ -52,17 +54,17 @@ import com.mongodb.bulk.BulkWriteError;
*/
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"));
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",
"MongoCursorNotFoundException", "MongoServerSelectionException", "MongoTimeoutException"));
private static final Set<String> RESOURCE_USAGE_EXCEPTIONS = new HashSet<String>(
Arrays.asList("MongoInternalException"));
private static final Set<String> RESOURCE_USAGE_EXCEPTIONS = new HashSet<>(
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"));
/*
@@ -80,7 +82,7 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
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);
}
@@ -92,7 +94,7 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
return new InvalidDataAccessResourceUsageException(ex.getMessage(), ex);
}
if (DATA_INTEGRETY_EXCEPTIONS.contains(exception)) {
if (DATA_INTEGRITY_EXCEPTIONS.contains(exception)) {
if (ex instanceof MongoServerException) {
if (((MongoServerException) ex).getCode() == 11000) {
@@ -128,6 +130,10 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
return new InvalidDataAccessApiUsageException(ex.getMessage(), ex);
} else if (MongoDbErrorCodes.isPermissionDeniedCode(code)) {
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);
}

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.AggregationResults;
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.index.IndexOperations;
import org.springframework.data.mongodb.core.mapreduce.GroupBy;
@@ -42,6 +43,7 @@ import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.util.CloseableIterator;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import com.mongodb.ClientSessionOptions;
import com.mongodb.Cursor;
@@ -381,7 +383,7 @@ public interface MongoOperations extends FluentMongoOperations {
* 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 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.
* @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
* 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 groupBy the conditions under which the group operation will be performed, e.g. keys, initial document,
* reduce function.
@@ -894,6 +894,167 @@ public interface MongoOperations extends FluentMongoOperations {
<T> T findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass,
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
* 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.
*
* @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.
@@ -993,32 +1155,36 @@ public interface MongoOperations extends FluentMongoOperations {
*
* @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}.
* @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.
*
* @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}.
* @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.
*
* @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}.
* @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
* class.
*
* @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
@@ -1034,8 +1200,9 @@ public interface MongoOperations extends FluentMongoOperations {
* Conversion"</a> for more details.
*
* @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
@@ -1052,8 +1219,9 @@ public interface MongoOperations extends FluentMongoOperations {
*
* @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}.
* @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

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> {
Publisher<T> doInCollection(MongoCollection<Document> collection) throws MongoException, DataAccessException;
}

View File

@@ -85,6 +85,23 @@ public interface ReactiveFindOperation {
*/
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.
*

View File

@@ -169,6 +169,15 @@ class ReactiveFindOperationSupport implements ReactiveFindOperation {
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)
* @see org.springframework.data.mongodb.core.ReactiveFindOperation.FindWithQuery#near(org.springframework.data.mongodb.core.query.NearQuery)

View File

@@ -19,7 +19,6 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
@@ -27,6 +26,7 @@ import org.bson.Document;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscription;
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.AggregationOptions;
import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
@@ -41,6 +41,7 @@ import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import com.mongodb.ClientSessionOptions;
import com.mongodb.ReadPreference;
@@ -688,6 +689,160 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
<T> Mono<T> findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass,
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
* specified type. The first document that matches the query is returned and also removed from the collection in the
@@ -772,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.
*
* @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);
@@ -786,7 +941,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
*
* @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}.
* @return the saved object.
* @return the inserted object.
*/
<T> Mono<T> insert(T objectToSave, String collectionName);
@@ -795,7 +950,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
*
* @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}.
* @return the saved objects.
* @return the inserted objects .
*/
<T> Flux<T> insert(Collection<? extends T> batchToSave, Class<?> entityClass);
@@ -804,7 +959,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
*
* @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}.
* @return the saved objects.
* @return the inserted objects.
*/
<T> Flux<T> insert(Collection<? extends T> batchToSave, String collectionName);
@@ -832,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.
*
* @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);
@@ -841,7 +996,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
*
* @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}.
* @return the saved objects.
* @return the inserted objects.
*/
<T> Flux<T> insertAll(Mono<? extends Collection<? extends T>> batchToSave, Class<?> entityClass);
@@ -850,7 +1005,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
*
* @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}.
* @return the saved objects.
* @return the inserted objects.
*/
<T> Flux<T> insertAll(Mono<? extends Collection<? extends T>> batchToSave, String collectionName);
@@ -859,7 +1014,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* class.
*
* @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);
@@ -1202,7 +1357,57 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
<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
* unless the {@link org.reactivestreams.Subscription} is {@link Subscription#cancel() canceled}.
* <p />
@@ -1212,38 +1417,17 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* Use {@link ChangeStreamOptions} to set arguments like {@link ChangeStreamOptions#getResumeToken() the resumseToken}
* for resuming change streams.
*
* @param filter can be {@literal null}.
* @param resultType must not be {@literal null}.
* @param options must not be {@literal null}.
* @param collectionName must not be {@literal null} nor empty.
* @param database the database to watch. Can be {@literal null}, uses configured default if so.
* @param collectionName the collection to watch. Can be {@literal null}, watches all collections if so.
* @param options must not be {@literal null}. Use {@link ChangeStreamOptions#empty()}.
* @param targetType the result type to use.
* @param <T>
* @return
* @return the {@link Flux} emitting {@link ChangeStreamEvent events} as they arrive.
* @since 2.1
* @see ChangeStreamOptions#getFilter()
*/
<T> Flux<ChangeStreamEvent<T>> changeStream(@Nullable Aggregation filter, Class<T> resultType,
ChangeStreamOptions options, String collectionName);
/**
* Subscribe to a MongoDB <a href="https://docs.mongodb.com/manual/changeStreams/">Change Streams</a> via the reactive
* infrastructure. Use the optional provided aggregation chain 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 resumeToken}
* for resuming change streams.
*
* @param filter can be empty, must not be {@literal null}.
* @param resultType must not be {@literal null}.
* @param options must not be {@literal null}.
* @param collectionName must not be {@literal null} nor empty.
* @param <T>
* @return
* @since 2.1
*/
<T> Flux<ChangeStreamEvent<T>> changeStream(List<Document> filter, Class<T> resultType, ChangeStreamOptions options,
String collectionName);
<T> Flux<ChangeStreamEvent<T>> changeStream(@Nullable String database, @Nullable String collectionName,
ChangeStreamOptions options, Class<T> targetType);
/**
* Execute a map-reduce operation. Use {@link MapReduceOptions} to optionally specify an output collection and other
@@ -1288,4 +1472,13 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
*/
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

@@ -18,12 +18,13 @@ package org.springframework.data.mongodb.core;
import reactor.core.publisher.Mono;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import com.mongodb.client.result.UpdateResult;
/**
* {@link ReactiveUpdateOperation} allows creation and execution of reactive MongoDB update / findAndModify operations
* in a fluent API style. <br />
* {@link ReactiveUpdateOperation} allows creation and execution of reactive MongoDB update / findAndModify /
* 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 {@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
@@ -68,6 +69,22 @@ public interface ReactiveUpdateOperation {
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.
*/
@@ -108,6 +125,16 @@ public interface ReactiveUpdateOperation {
* @throws IllegalArgumentException if update is {@literal null}.
*/
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);
}
/**
* 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> {}
}

View File

@@ -22,6 +22,7 @@ import lombok.experimental.FieldDefaults;
import reactor.core.publisher.Mono;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -50,20 +51,24 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
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
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
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 Class<T> domainType;
@NonNull Class<?> domainType;
Query query;
org.springframework.data.mongodb.core.query.Update update;
String collection;
FindAndModifyOptions options;
@Nullable String collection;
@Nullable FindAndModifyOptions findAndModifyOptions;
@Nullable FindAndReplaceOptions findAndReplaceOptions;
@Nullable Object replacement;
@NonNull Class<T> targetType;
/*
* (non-Javadoc)
@@ -74,7 +79,8 @@ class ReactiveUpdateOperationSupport implements ReactiveUpdateOperation {
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!");
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();
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!");
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!");
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) {

View File

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

View File

@@ -41,7 +41,7 @@ class AggregationOperationRenderer {
* {@link Document} representation.
*
* @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}.
*/
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);
}
/**
* 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
*/
@@ -1497,4 +1510,59 @@ public class ArrayOperators {
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.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* 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
* 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}.
*/
public static Timezone ofExpression(AggregationExpression expression) {
@@ -380,6 +381,17 @@ public class DateOperators {
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
* (for Monday) to 7 (for Sunday).
@@ -1352,6 +1364,11 @@ public class DateOperators {
Assert.notNull(format, "Format must not be null!");
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) {
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
@@ -1400,10 +1453,14 @@ public class DateOperators {
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);
if (!ObjectUtils.nullSafeEquals(timezone, Timezone.none())) {
@@ -1412,6 +1469,25 @@ public class DateOperators {
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 {
/**
@@ -1421,6 +1497,16 @@ public class DateOperators {
* @return
*/
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));
}
/**
* 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
protected String getMongoMethod() {
return "$dateFromString";

View File

@@ -17,7 +17,9 @@ package org.springframework.data.mongodb.core.aggregation;
import org.bson.Document;
import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Represents a {@code geoNear} aggregation operation.
@@ -28,26 +30,55 @@ import org.springframework.util.Assert;
* @author Thomas Darimont
* @author Christoph Strobl
* @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 {
private final NearQuery nearQuery;
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
* {@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}.
*/
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.hasLength(distanceField, "Distance field must not be null or empty.");
this.nearQuery = nearQuery;
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());
command.put("distanceField", distanceField);
if (StringUtils.hasText(indexKey)) {
command.put("key", indexKey);
}
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("connectFromField", connectFrom.getName());
graphLookup.put("connectToField", connectTo.getName());
graphLookup.put("connectFromField", connectFrom.getTarget());
graphLookup.put("connectToField", connectTo.getTarget());
graphLookup.put("as", as.getName());
if (maxDepth != null) {
@@ -112,7 +112,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
}
if (depthField != null) {
graphLookup.put("depthField", depthField.getName());
graphLookup.put("depthField", depthField.getTarget());
}
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) {
return (key.startsWith("$") || blacklist.contains(key)) ? key : (prefix + "." + key);
return (key.startsWith("$") || isBlacklisted(key)) ? key : (prefix + "." + key);
}
private Object prefixCollection(Collection<Object> sourceCollection) {
@@ -119,4 +119,23 @@ public class PrefixingDelegatingAggregationOperationContext implements Aggregati
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));
}
/**
* 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
* result of the expression.

View File

@@ -350,8 +350,7 @@ public class StringOperators {
* @return
*/
public StrLenBytes length() {
return usesFieldRef() ? StrLenBytes.stringLengthOf(fieldReference)
: StrLenBytes.stringLengthOf(expression);
return usesFieldRef() ? StrLenBytes.stringLengthOf(fieldReference) : StrLenBytes.stringLengthOf(expression);
}
/**
@@ -391,6 +390,132 @@ public class StringOperators {
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() {
return fieldReference != null;
}
@@ -1072,4 +1197,257 @@ public class StringOperators {
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.MongoPersistentProperty;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import com.mongodb.DBRef;
@@ -59,9 +60,15 @@ public interface DbRefResolver {
* @param id will never be {@literal null}.
* @return
*/
DBRef createDbRef(@Nullable org.springframework.data.mongodb.core.mapping.DBRef annotation,
MongoPersistentEntity<?> entity,
Object id);
default 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);
}
/**
* 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.LazyLoadingException;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.lang.Nullable;
import org.springframework.objenesis.ObjenesisStd;
@@ -104,21 +103,6 @@ public class DefaultDbRefResolver implements DbRefResolver {
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)
* @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.conversions.Bson;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.util.BsonUtils;
import org.springframework.lang.Nullable;
@@ -58,6 +59,14 @@ class DocumentAccessor {
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
* {@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) {
String fieldName = property.getFieldName();
Map<String, Object> map = BsonUtils.asMap(document);
if (!fieldName.contains(".")) {
return BsonUtils.asMap(this.document).get(fieldName);
return map.get(fieldName);
}
Iterator<String> parts = Arrays.asList(fieldName.split("\\.")).iterator();
Map<String, Object> source = BsonUtils.asMap(this.document);
Map<String, Object> source = map;
Object result = null;
while (source != null && parts.hasNext()) {
@@ -124,6 +134,17 @@ class DocumentAccessor {
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
* {@link MongoPersistentProperty}.
@@ -131,21 +152,27 @@ class DocumentAccessor {
* @param property must not be {@literal null}.
* @return
*/
@SuppressWarnings("unchecked")
public boolean hasValue(MongoPersistentProperty property) {
Assert.notNull(property, "Property must not be null!");
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 (this.document instanceof Document) {
return ((Document) this.document).containsKey(fieldName);
}
if (this.document instanceof DBObject) {
return ((DBObject) this.document).containsField(fieldName);
}
return false;
}
String[] parts = fieldName.split("\\.");

View File

@@ -15,17 +15,8 @@
*/
package org.springframework.data.mongodb.core.convert;
import java.util.ArrayList;
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.*;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import org.bson.Document;
import org.bson.conversions.Bson;
@@ -42,6 +33,7 @@ import org.springframework.data.convert.TypeMapper;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PreferredConstructor;
import org.springframework.data.mapping.PreferredConstructor.Parameter;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
@@ -147,7 +139,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
*/
public void setTypeMapper(@Nullable MongoTypeMapper typeMapper) {
this.typeMapper = typeMapper == null
? new DefaultMongoTypeMapper(DefaultMongoTypeMapper.DEFAULT_TYPE_KEY, mappingContext) : typeMapper;
? new DefaultMongoTypeMapper(DefaultMongoTypeMapper.DEFAULT_TYPE_KEY, mappingContext)
: typeMapper;
}
/*
@@ -249,13 +242,14 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
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,
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<>(
entity, provider, path.getCurrentObject());
@@ -265,60 +259,105 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
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);
S instance = instantiator.createInstance(entity, provider);
PersistentPropertyAccessor accessor = new ConvertingPropertyAccessor(entity.getPropertyAccessor(instance),
conversionService);
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);
if (entity.requiresPropertyPopulation()) {
return populateProperties(entity, documentAccessor, path, evaluator, instance);
}
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;
}
private Object readIdValue(ObjectPath path, DefaultSpELExpressionEvaluator evaluator,
MongoPersistentProperty idProperty, DocumentAccessor documentAccessor) {
private <S> S populateProperties(MongoPersistentEntity<S> entity, DocumentAccessor documentAccessor, ObjectPath path,
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();
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;
}
private void readProperties(MongoPersistentEntity<?> entity, PersistentPropertyAccessor accessor,
@Nullable MongoPersistentProperty idProperty, DocumentAccessor documentAccessor,
MongoDbPropertyValueProvider valueProvider, DbRefResolverCallback callback) {
private void readProperties(MongoPersistentEntity<?> entity, PersistentPropertyAccessor<?> accessor,
DocumentAccessor documentAccessor, MongoDbPropertyValueProvider valueProvider, ObjectPath currentPath,
SpELExpressionEvaluator evaluator) {
DbRefResolverCallback callback = null;
for (MongoPersistentProperty prop : entity) {
if (prop.isAssociation() && !entity.isConstructorArgument(prop)) {
if (callback == null) {
callback = getDbRefResolverCallback(documentAccessor, currentPath, evaluator);
}
readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback);
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;
}
@@ -327,6 +366,11 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
if (prop.isAssociation()) {
if (callback == null) {
callback = getDbRefResolverCallback(documentAccessor, currentPath, evaluator);
}
readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback);
continue;
}
@@ -335,7 +379,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) {
MongoPersistentProperty property = association.getInverse();
@@ -392,12 +443,23 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
removeFromMap(bson, "_id");
}
boolean handledByCustomConverter = conversions.hasCustomWriteTarget(entityType, Document.class);
if (!handledByCustomConverter && !(bson instanceof Collection)) {
if (requiresTypeHint(entityType)) {
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.
*
@@ -427,7 +489,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
if (Collection.class.isAssignableFrom(entityType)) {
writeCollectionInternal((Collection<?>) obj, ClassTypeInformation.LIST, (Collection) bson);
writeCollectionInternal((Collection<?>) obj, ClassTypeInformation.LIST, (Collection<?>) bson);
return;
}
@@ -446,10 +508,10 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
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);
MongoPersistentProperty idProperty = entity.getIdProperty();
if (idProperty != null && !dbObjectAccessor.hasValue(idProperty)) {
Object value = idMapper.convertId(accessor.getProperty(idProperty));
@@ -458,10 +520,11 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
dbObjectAccessor.put(idProperty, value);
}
}
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) {
// Write the properties
@@ -489,8 +552,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
}
private void writeAssociation(Association<MongoPersistentProperty> association, PersistentPropertyAccessor accessor,
DocumentAccessor dbObjectAccessor) {
private void writeAssociation(Association<MongoPersistentProperty> association,
PersistentPropertyAccessor<?> accessor, DocumentAccessor dbObjectAccessor) {
MongoPersistentProperty inverseProp = association.getInverse();
@@ -555,8 +618,9 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return;
}
MongoPersistentEntity<?> entity = isSubtype(prop.getType(), obj.getClass())
? mappingContext.getRequiredPersistentEntity(obj.getClass()) : mappingContext.getRequiredPersistentEntity(type);
MongoPersistentEntity<?> entity = isSubTypeOf(obj.getClass(), prop.getType())
? mappingContext.getRequiredPersistentEntity(obj.getClass())
: mappingContext.getRequiredPersistentEntity(type);
Object existingValue = accessor.get(prop);
Document document = existingValue instanceof Document ? (Document) existingValue : new Document();
@@ -566,10 +630,6 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
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
* {@link Collection} already, will convert an array into a {@link Collection} or simply create a single element
@@ -659,11 +719,13 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
* @param sink the {@link Collection} to write to.
* @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;
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) {
componentType = type.getComponentType();
@@ -777,7 +839,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
return conversions.hasCustomWriteTarget(key.getClass(), String.class)
? (String) getPotentiallyConvertedSimpleWrite(key) : key.toString();
? (String) getPotentiallyConvertedSimpleWrite(key)
: key.toString();
}
/**
@@ -867,7 +930,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
*/
@Nullable
@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)) {
return value;
@@ -943,7 +1006,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
Assert.notNull(path, "Object path must not be null!");
Class<?> collectionType = targetType.getType();
collectionType = Collection.class.isAssignableFrom(collectionType) //
collectionType = isSubTypeOf(collectionType, Collection.class) //
? collectionType //
: List.class;
@@ -977,13 +1040,12 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
items.add(read(componentType, (BasicDBObject) element, path));
} else {
if (element instanceof Collection) {
if (!Object.class.equals(rawComponentType) && element instanceof Collection) {
if (!rawComponentType.isArray() && !ClassUtils.isAssignable(Iterable.class, rawComponentType)) {
throw new MappingException(
String.format(INCOMPATIBLE_TYPES, element, element.getClass(), rawComponentType, path));
}
}
if (element instanceof List) {
items.add(readCollectionOrArray(componentType, (Collection<Object>) element, path));
} else {
@@ -1270,12 +1332,14 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
* of the configured source {@link Document}.
*
* @author Oliver Gierke
* @author Mark Paluch
* @author Christoph Strobl
*/
class MongoDbPropertyValueProvider implements PropertyValueProvider<MongoPersistentProperty> {
private final DocumentAccessor source;
private final SpELExpressionEvaluator evaluator;
private final ObjectPath path;
final DocumentAccessor accessor;
final SpELExpressionEvaluator evaluator;
final ObjectPath path;
/**
* Creates a new {@link MongoDbPropertyValueProvider} for the given source, {@link SpELExpressionEvaluator} and
@@ -1285,15 +1349,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
* @param evaluator must not be {@literal null}.
* @param path must not be {@literal null}.
*/
public MongoDbPropertyValueProvider(Bson source, SpELExpressionEvaluator evaluator, ObjectPath 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;
MongoDbPropertyValueProvider(Bson source, SpELExpressionEvaluator evaluator, ObjectPath path) {
this(new DocumentAccessor(source), evaluator, path);
}
/**
@@ -1304,13 +1361,13 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
* @param evaluator 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(evaluator, "SpELExpressionEvaluator must not be null!");
Assert.notNull(path, "ObjectPath must not be null!");
this.source = accessor;
this.accessor = accessor;
this.evaluator = evaluator;
this.path = path;
}
@@ -1323,7 +1380,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
public <T> T getPropertyValue(MongoPersistentProperty property) {
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) {
return null;
@@ -1333,6 +1390,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
* resolved SpEL value.
@@ -1435,7 +1541,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
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();
List<T> targeList = new ArrayList<>(dbrefs.size());
@@ -1517,6 +1624,17 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return true;
}
/**
* Returns whether the given type is a sub type of the given reference, i.e. assignable but not the exact same type.
*
* @param type must not be {@literal null}.
* @param reference must not be {@literal null}.
* @return
*/
private static boolean isSubTypeOf(Class<?> type, Class<?> reference) {
return !type.equals(reference) && reference.isAssignableFrom(type);
}
/**
* Marker class used to indicate we have a non root document object here that might be used within an update - so we
* need to preserve type hints for potential nested elements but need to remove it on top level.
@@ -1527,4 +1645,14 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
static class NestedDocument {
}
enum NoOpParameterValueProvider implements ParameterValueProvider<MongoPersistentProperty> {
INSTANCE;
@Override
public <T> T getParameterValue(Parameter<T, MongoPersistentProperty> parameter) {
return null;
}
}
}

View File

@@ -19,6 +19,7 @@ import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Currency;
@@ -26,6 +27,7 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import org.bson.BsonTimestamp;
import org.bson.Document;
import org.bson.types.Binary;
import org.bson.types.Code;
@@ -86,6 +88,7 @@ abstract class MongoConverters {
converters.add(LongToAtomicLongConverter.INSTANCE);
converters.add(IntegerToAtomicIntegerConverter.INSTANCE);
converters.add(BinaryToByteArrayConverter.INSTANCE);
converters.add(BsonTimestampToInstantConverter.INSTANCE);
return converters;
}
@@ -465,4 +468,22 @@ abstract class MongoConverters {
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)) {
PropertyValueTransformer valueTransformer = exampleSpecAccessor.getValueTransformerForPath(mappedPropertyPath);
value = valueTransformer.convert(value);
if (value == null) {
Optional converted = valueTransformer.apply(Optional.ofNullable(value));
if(!converted.isPresent()) {
iter.remove();
continue;
}
entry.setValue(value);
entry.setValue(converted.get());
}
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;
import lombok.Value;
import java.util.ArrayList;
import java.util.List;
@@ -45,26 +43,33 @@ class 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() {
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
* {@link ObjectPathItem} to it.
* Creates a new {@link ObjectPath} from the given parent {@link ObjectPath} and adding the provided path values.
*
* @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];
System.arraycopy(parent.items, 0, items, 0, parent.items.length);
items[parent.items.length] = item;
this.items = items;
this.parent = parent;
this.object = object;
this.idValue = idValue;
this.collection = collection;
}
/**
@@ -80,8 +85,7 @@ class ObjectPath {
Assert.notNull(object, "Object must not be null!");
Assert.notNull(entity, "MongoPersistentEntity must not be null!");
ObjectPathItem item = new ObjectPathItem(object, id, entity.getCollection());
return new ObjectPath(this, item);
return new ObjectPath(this, object, id, entity.getCollection());
}
/**
@@ -100,15 +104,15 @@ class ObjectPath {
Assert.notNull(id, "Id 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;
}
if (collection.equals(item.getCollection()) && id.equals(item.getIdValue())) {
if (collection.equals(current.getCollection()) && id.equals(current.getIdValue())) {
return object;
}
}
@@ -133,15 +137,15 @@ class ObjectPath {
Assert.hasText(collection, "Collection name 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;
}
if (collection.equals(item.getCollection()) && id.equals(item.getIdValue())
if (collection.equals(current.getCollection()) && id.equals(current.getIdValue())
&& ClassUtils.isAssignable(type, object.getClass())) {
return type.cast(object);
}
@@ -157,7 +161,21 @@ class ObjectPath {
*/
@Nullable
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
public String toString() {
if (items.length == 0) {
if (parent == null) {
return "[empty]";
}
List<String> strings = new ArrayList<>(items.length);
List<String> strings = new ArrayList<>();
for (ObjectPathItem item : items) {
strings.add(ObjectUtils.nullSafeToString(item.object));
for (ObjectPath current = this; current != null; current = current.parent) {
strings.add(ObjectUtils.nullSafeToString(current.getObject()));
}
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

@@ -312,7 +312,7 @@ public class QueryMapper {
@SuppressWarnings("unchecked")
protected Object getMappedValue(Field documentField, Object value) {
if (documentField.isIdField()) {
if (documentField.isIdField() && !documentField.isAssociation()) {
if (isDBObject(value)) {
DBObject valueDbo = (DBObject) value;
@@ -850,15 +850,18 @@ public class QueryMapper {
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.QueryMapper.Field#isIdKey()
* @see org.springframework.data.mongodb.core.convert.QueryMapper.Field#isIdField()
*/
@Override
public boolean isIdField() {
MongoPersistentProperty idProperty = entity.getIdProperty();
MongoPersistentProperty idProperty = (property != null && property.isIdProperty()) ? property
: entity.getIdProperty();
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);

View File

@@ -70,7 +70,9 @@ public @interface CompoundIndex {
/**
* @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>
* @deprecated since 2.1. No longer supported by MongoDB as of server version 3.0.
*/
@Deprecated
boolean dropDups() default false;
/**

View File

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

View File

@@ -56,7 +56,9 @@ public @interface Indexed {
/**
* @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>
* @deprecated since 2.1. No longer supported by MongoDB as of server version 3.0.
*/
@Deprecated
boolean dropDups() default false;
/**

View File

@@ -24,11 +24,14 @@ import org.springframework.lang.Nullable;
* {@link MongoPersistentProperty} caching access to {@link #isIdProperty()} and {@link #getFieldName()}.
*
* @author Oliver Gierke
* @author Mark Paluch
*/
public class CachingMongoPersistentProperty extends BasicMongoPersistentProperty {
private @Nullable Boolean isIdProperty;
private @Nullable Boolean isAssociation;
private @Nullable boolean dbRefResolved;
private @Nullable DBRef dbref;
private @Nullable String fieldName;
private @Nullable Boolean usePropertyAccess;
private @Nullable Boolean isTransient;
@@ -36,8 +39,7 @@ public class CachingMongoPersistentProperty extends BasicMongoPersistentProperty
/**
* Creates a new {@link CachingMongoPersistentProperty}.
*
* @param field
* @param propertyDescriptor
* @param property
* @param owner
* @param simpleTypeHolder
* @param fieldNamingStrategy
@@ -114,4 +116,28 @@ public class CachingMongoPersistentProperty extends BasicMongoPersistentProperty
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

@@ -22,13 +22,13 @@ import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;
import org.bson.BsonObjectId;
import org.bson.*;
import org.bson.types.Binary;
import org.bson.types.CodeWScope;
import org.bson.types.CodeWithScope;
import org.bson.types.Decimal128;
import org.bson.types.ObjectId;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.mongodb.util.MongoClientVersion;
import org.springframework.util.ClassUtils;
import com.mongodb.DBRef;
@@ -54,15 +54,29 @@ public abstract class MongoSimpleTypes {
simpleTypes.add(ObjectId.class);
simpleTypes.add(BsonObjectId.class);
simpleTypes.add(CodeWScope.class);
simpleTypes.add(CodeWithScope.class);
simpleTypes.add(org.bson.Document.class);
simpleTypes.add(Pattern.class);
simpleTypes.add(Binary.class);
simpleTypes.add(UUID.class);
simpleTypes.add(Decimal128.class);
if (MongoClientVersion.isMongo34Driver()) {
simpleTypes
.add(ClassUtils.resolveClassName("org.bson.types.Decimal128", MongoSimpleTypes.class.getClassLoader()));
}
simpleTypes.add(BsonBinary.class);
simpleTypes.add(BsonBoolean.class);
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);
}

View File

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

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.mongodb.core.mapping.event;
import java.util.function.Function;
import org.bson.Document;
import org.springframework.context.ApplicationEvent;
import org.springframework.lang.Nullable;
@@ -72,4 +74,19 @@ public class MongoMappingEvent<T> extends ApplicationEvent {
public T getSource() {
return (T) super.getSource();
}
/**
* Allows client code to change the underlying source instance by applying the given {@link Function}.
*
* @param mapper the {@link Function} to apply, will only be applied if the source is not {@literal null}.
* @since 2.1
*/
final void mapSource(Function<T, T> mapper) {
if (source == null) {
return;
}
this.source = mapper.apply(getSource());
}
}

View File

@@ -24,6 +24,8 @@ import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.lang.Nullable;
import com.mongodb.MapReduceCommand;
import com.mongodb.MapReduceCommand.OutputType;
import com.mongodb.client.model.MapReduceAction;
/**
* @author Mark Pollack
@@ -295,6 +297,37 @@ public class MapReduceOptions {
return collation;
}
/**
* Return the {@link MapReduceAction} derived from {@link com.mongodb.MapReduceCommand.OutputType}.
*
* @return the mapped action or {@literal null} if the action maps to inline output.
* @since 2.0.10
*/
@Nullable
public MapReduceAction getMapReduceAction() {
switch (outputType) {
case MERGE:
return MapReduceAction.MERGE;
case REDUCE:
return MapReduceAction.REDUCE;
case REPLACE:
return MapReduceAction.REPLACE;
case INLINE:
return null;
default:
throw new IllegalStateException(String.format("Unknown output type %s for map reduce command.", outputType));
}
}
/**
* @return {@literal true} if {@link OutputType#INLINE} is used.
* @since 2.0.10
*/
public boolean usesInlineOutput() {
return OutputType.INLINE.equals(outputType);
}
public Document getOptionsObject() {
Document cmd = new Document();
@@ -328,7 +361,7 @@ public class MapReduceOptions {
Document out = new Document();
switch (outputType) {
switch (getOutputType()) {
case INLINE:
out.put("inline", 1);
break;

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.mongodb.core.messaging;
import java.time.Instant;
import org.bson.BsonValue;
import org.bson.Document;
import org.springframework.data.mongodb.core.ChangeStreamOptions;
@@ -34,7 +36,7 @@ import com.mongodb.client.model.changestream.FullDocument;
* using the synchronous MongoDB Java driver.
* <p/>
* The most trivial use case is subscribing to all events of a specific {@link com.mongodb.client.MongoCollection
* collection}.
* collection}
*
* <pre>
* <code>
@@ -42,6 +44,15 @@ import com.mongodb.client.model.changestream.FullDocument;
* </code>
* </pre>
*
* or {@link com.mongodb.client.MongoDatabase} which receives events from all {@link com.mongodb.client.MongoCollection
* collections} in that database.
*
* <pre>
* <code>
* ChangeStreamRequest<Document> request = new ChangeStreamRequest<>(System.out::println, RequestOptions.justDatabase("test"));
* </code>
* </pre>
*
* For more advanced scenarios {@link ChangeStreamOptions} offers abstractions for options like filtering, resuming,...
*
* <pre>
@@ -154,21 +165,23 @@ public class ChangeStreamRequest<T>
*/
public static class ChangeStreamRequestOptions implements SubscriptionRequest.RequestOptions {
private final String collectionName;
private final @Nullable String databaseName;
private final @Nullable String collectionName;
private final ChangeStreamOptions options;
/**
* Create new {@link ChangeStreamRequestOptions}.
*
* @param collectionName must not be {@literal null}.
* @param collectionName can be {@literal null}.
* @param options must not be {@literal null}.
*/
public ChangeStreamRequestOptions(String collectionName, ChangeStreamOptions options) {
public ChangeStreamRequestOptions(@Nullable String databaseName, @Nullable String collectionName,
ChangeStreamOptions options) {
Assert.notNull(collectionName, "CollectionName must not be null!");
Assert.notNull(options, "Options must not be null!");
this.collectionName = collectionName;
this.databaseName = databaseName;
this.options = options;
}
@@ -176,7 +189,8 @@ public class ChangeStreamRequest<T>
Assert.notNull(options, "Options must not be null!");
return new ChangeStreamRequestOptions(options.getCollectionName(), ChangeStreamOptions.builder().build());
return new ChangeStreamRequestOptions(options.getDatabaseName(), options.getCollectionName(),
ChangeStreamOptions.builder().build());
}
/**
@@ -196,6 +210,15 @@ public class ChangeStreamRequest<T>
public String getCollectionName() {
return collectionName;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.monitor.SubscriptionRequest.RequestOptions#getDatabaseName()
*/
@Override
public String getDatabaseName() {
return databaseName;
}
}
/**
@@ -207,12 +230,27 @@ public class ChangeStreamRequest<T>
*/
public static class ChangeStreamRequestBuilder<T> {
private @Nullable String databaseName;
private @Nullable String collectionName;
private @Nullable MessageListener<ChangeStreamDocument<Document>, ? super T> listener;
private ChangeStreamOptionsBuilder delegate = ChangeStreamOptions.builder();
private ChangeStreamRequestBuilder() {}
/**
* Set the name of the {@link com.mongodb.client.MongoDatabase} to listen to.
*
* @param databaseName must not be {@literal null} nor empty.
* @return this.
*/
public ChangeStreamRequestBuilder<T> database(String databaseName) {
Assert.hasText(databaseName, "DatabaseName must not be null!");
this.databaseName = databaseName;
return this;
}
/**
* Set the name of the {@link com.mongodb.client.MongoCollection} to listen to.
*
@@ -317,6 +355,22 @@ public class ChangeStreamRequest<T>
return this;
}
/**
* Set the cluster time at which to resume listening.
*
* @param clusterTime must not be {@literal null}.
* @return this.
* @see ChangeStreamOptions#getResumeTimestamp()
* @see ChangeStreamOptionsBuilder#resumeAt(java.time.Instant)
*/
public ChangeStreamRequestBuilder<T> resumeAt(Instant clusterTime) {
Assert.notNull(clusterTime, "ClusterTime must not be null!");
this.delegate.resumeAt(clusterTime);
return this;
}
/**
* Set the {@link FullDocument} lookup to {@link FullDocument#UPDATE_LOOKUP}.
*
@@ -339,9 +393,9 @@ public class ChangeStreamRequest<T>
public ChangeStreamRequest<T> build() {
Assert.notNull(listener, "MessageListener must not be null!");
Assert.hasText(collectionName, "CollectionName must not be null!");
return new ChangeStreamRequest<>(listener, new ChangeStreamRequestOptions(collectionName, delegate.build()));
return new ChangeStreamRequest<>(listener,
new ChangeStreamRequestOptions(databaseName, collectionName, delegate.build()));
}
}
}

View File

@@ -17,14 +17,16 @@ package org.springframework.data.mongodb.core.messaging;
import lombok.AllArgsConstructor;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.bson.BsonDocument;
import org.bson.BsonTimestamp;
import org.bson.BsonValue;
import org.bson.Document;
import org.springframework.data.mongodb.core.ChangeStreamEvent;
import org.springframework.data.mongodb.core.ChangeStreamOptions;
@@ -42,10 +44,12 @@ import org.springframework.data.mongodb.core.messaging.SubscriptionRequest.Reque
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.ErrorHandler;
import org.springframework.util.StringUtils;
import com.mongodb.MongoNamespace;
import com.mongodb.client.ChangeStreamIterable;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Collation;
import com.mongodb.client.model.changestream.ChangeStreamDocument;
import com.mongodb.client.model.changestream.FullDocument;
@@ -54,6 +58,7 @@ import com.mongodb.client.model.changestream.FullDocument;
* {@link Task} implementation for obtaining {@link ChangeStreamDocument ChangeStreamDocuments} from MongoDB.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.1
*/
class ChangeStreamTask extends CursorReadingTask<ChangeStreamDocument<Document>, Object> {
@@ -73,7 +78,7 @@ class ChangeStreamTask extends CursorReadingTask<ChangeStreamDocument<Document>,
mongoConverter = template.getConverter();
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.messaging.CursorReadingTask#initCursor(org.springframework.data.mongodb.core.MongoTemplate, org.springframework.data.mongodb.core.messaging.SubscriptionRequest.RequestOptions, java.lang.Class)
*/
@@ -84,7 +89,9 @@ class ChangeStreamTask extends CursorReadingTask<ChangeStreamDocument<Document>,
List<Document> filter = Collections.emptyList();
BsonDocument resumeToken = new BsonDocument();
Collation collation = null;
FullDocument fullDocument = FullDocument.DEFAULT;
FullDocument fullDocument = ClassUtils.isAssignable(Document.class, targetType) ? FullDocument.DEFAULT
: FullDocument.UPDATE_LOOKUP;
BsonTimestamp startAt = null;
if (options instanceof ChangeStreamRequest.ChangeStreamRequestOptions) {
@@ -107,16 +114,32 @@ class ChangeStreamTask extends CursorReadingTask<ChangeStreamDocument<Document>,
fullDocument = changeStreamOptions.getFullDocumentLookup()
.orElseGet(() -> ClassUtils.isAssignable(Document.class, targetType) ? FullDocument.DEFAULT
: FullDocument.UPDATE_LOOKUP);
startAt = changeStreamOptions.getResumeTimestamp().map(it -> new BsonTimestamp((int) it.getEpochSecond(), 0))
.orElse(null);
}
ChangeStreamIterable<Document> iterable = filter.isEmpty()
? template.getCollection(options.getCollectionName()).watch(Document.class)
: template.getCollection(options.getCollectionName()).watch(filter, Document.class);
MongoDatabase db = StringUtils.hasText(options.getDatabaseName())
? template.getMongoDbFactory().getDb(options.getDatabaseName()) : template.getDb();
ChangeStreamIterable<Document> iterable;
if (StringUtils.hasText(options.getCollectionName())) {
iterable = filter.isEmpty() ? db.getCollection(options.getCollectionName()).watch(Document.class)
: db.getCollection(options.getCollectionName()).watch(filter, Document.class);
} else {
iterable = filter.isEmpty() ? db.watch(Document.class) : db.watch(filter, Document.class);
}
if (!resumeToken.isEmpty()) {
iterable = iterable.resumeAfter(resumeToken);
}
if (startAt != null) {
iterable.startAtOperationTime(startAt);
}
if (collation != null) {
iterable = iterable.collation(collation);
}
@@ -126,13 +149,15 @@ class ChangeStreamTask extends CursorReadingTask<ChangeStreamDocument<Document>,
return iterable.iterator();
}
@SuppressWarnings("unchecked")
List<Document> prepareFilter(MongoTemplate template, ChangeStreamOptions options) {
if (!options.getFilter().isPresent()) {
return Collections.emptyList();
}
Object filter = options.getFilter().get();
Object filter = options.getFilter().orElse(null);
if (filter instanceof Aggregation) {
Aggregation agg = (Aggregation) filter;
AggregationOperationContext context = agg instanceof TypedAggregation
@@ -141,26 +166,39 @@ class ChangeStreamTask extends CursorReadingTask<ChangeStreamDocument<Document>,
: Aggregation.DEFAULT_CONTEXT;
return agg.toPipeline(new PrefixingDelegatingAggregationOperationContext(context, "fullDocument", blacklist));
} else if (filter instanceof List) {
return (List<Document>) filter;
} else {
throw new IllegalArgumentException(
"ChangeStreamRequestOptions.filter mut be either an Aggregation or a plain list of Documents");
}
if (filter instanceof List) {
return (List<Document>) filter;
}
throw new IllegalArgumentException(
"ChangeStreamRequestOptions.filter mut be either an Aggregation or a plain list of Documents");
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.messaging.CursorReadingTask#createMessage(java.lang.Object, java.lang.Class, org.springframework.data.mongodb.core.messaging.SubscriptionRequest.RequestOptions)
*/
@Override
protected Message<ChangeStreamDocument<Document>, Object> createMessage(ChangeStreamDocument<Document> source,
Class<Object> targetType, RequestOptions options) {
// namespace might be null for eg. OperationType.INVALIDATE
MongoNamespace namespace = Optional.ofNullable(source.getNamespace())
.orElse(new MongoNamespace("unknown", options.getCollectionName()));
MongoNamespace namespace = source.getNamespace() != null ? source.getNamespace()
: createNamespaceFromOptions(options);
return new ChangeStreamEventMessage<>(new ChangeStreamEvent<>(source, targetType, mongoConverter), MessageProperties
.builder().databaseName(namespace.getDatabaseName()).collectionName(namespace.getCollectionName()).build());
}
MongoNamespace createNamespaceFromOptions(RequestOptions options) {
String collectionName = StringUtils.hasText(options.getCollectionName()) ? options.getCollectionName() : "unknown";
String databaseName = StringUtils.hasText(options.getDatabaseName()) ? options.getDatabaseName() : "unknown";
return new MongoNamespace(databaseName, collectionName);
}
/**
* {@link Message} implementation for ChangeStreams
*
@@ -172,7 +210,7 @@ class ChangeStreamTask extends CursorReadingTask<ChangeStreamDocument<Document>,
private final ChangeStreamEvent<T> delegate;
private final MessageProperties messageProperties;
/*
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.messaging.Message#getRaw()
*/
@@ -182,7 +220,7 @@ class ChangeStreamTask extends CursorReadingTask<ChangeStreamDocument<Document>,
return delegate.getRaw();
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.messaging.Message#getBody()
*/
@@ -192,7 +230,7 @@ class ChangeStreamTask extends CursorReadingTask<ChangeStreamDocument<Document>,
return delegate.getBody();
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.messaging.Message#getProperties()
*/
@@ -200,5 +238,32 @@ class ChangeStreamTask extends CursorReadingTask<ChangeStreamDocument<Document>,
public MessageProperties getProperties() {
return this.messageProperties;
}
/**
* @return the resume token or {@literal null} if not set.
* @see ChangeStreamEvent#getResumeToken()
*/
@Nullable
BsonValue getResumeToken() {
return delegate.getResumeToken();
}
/**
* @return the cluster time of the event or {@literal null}.
* @see ChangeStreamEvent#getTimestamp()
*/
@Nullable
Instant getTimestamp() {
return delegate.getTimestamp();
}
/**
* Get the {@link ChangeStreamEvent} from the message.
*
* @return never {@literal null}.
*/
ChangeStreamEvent<T> getChangeStreamEvent() {
return delegate;
}
}
}

View File

@@ -24,10 +24,10 @@ import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.messaging.Message.MessageProperties;
import org.springframework.data.mongodb.core.messaging.SubscriptionRequest.RequestOptions;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ErrorHandler;
import com.mongodb.client.MongoCursor;
import com.mysema.commons.lang.Assert;
/**
* @author Christoph Strobl
@@ -202,7 +202,7 @@ abstract class CursorReadingTask<T, R> implements Task {
public boolean awaitStart(Duration timeout) throws InterruptedException {
Assert.notNull(timeout, "Timeout must not be null!");
Assert.isFalse(timeout.isNegative(), "Timeout must not be negative!");
Assert.isTrue(!timeout.isNegative(), "Timeout must not be negative!");
return awaitStart.await(timeout.toNanos(), TimeUnit.NANOSECONDS);
}

View File

@@ -15,7 +15,10 @@
*/
package org.springframework.data.mongodb.core.messaging;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.messaging.SubscriptionRequest.RequestOptions;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* The actual {@link SubscriptionRequest} sent to the {@link MessageListenerContainer}. This wrapper type allows passing
@@ -51,8 +54,93 @@ public interface SubscriptionRequest<S, T, O extends RequestOptions> {
interface RequestOptions {
/**
* @return the name of the collection to subscribe to. Never {@literal null}.
* Get the database name of the db.
*
* @return the name of the database to subscribe to. Can be {@literal null} in which case the default
* {@link MongoDbFactory#getDb() database} is used.
*/
@Nullable
default String getDatabaseName() {
return null;
}
/**
* Get the collection name.
*
* @return the name of the collection to subscribe to. Can be {@literal null}.
*/
@Nullable
String getCollectionName();
/**
* Create empty options.
*
* @return new instance of empty {@link RequestOptions}.
*/
static RequestOptions none() {
return () -> null;
}
/**
* Create options with the provided database.
*
* @param database must not be {@literal null}.
* @return new instance of empty {@link RequestOptions}.
*/
static RequestOptions justDatabase(String database) {
Assert.notNull(database, "Database must not be null!");
return new RequestOptions() {
@Override
public String getCollectionName() {
return null;
}
@Override
public String getDatabaseName() {
return database;
}
};
}
/**
* Create options with the provided collection.
*
* @param collection must not be {@literal null}.
* @return new instance of empty {@link RequestOptions}.
*/
static RequestOptions justCollection(String collection) {
Assert.notNull(collection, "Collection must not be null!");
return () -> collection;
}
/**
* Create options with the provided database and collection.
*
* @param database must not be {@literal null}.
* @param collection must not be {@literal null}.
* @return new instance of empty {@link RequestOptions}.
*/
static RequestOptions of(String database, String collection) {
Assert.notNull(database, "Database must not be null!");
Assert.notNull(collection, "Collection must not be null!");
return new RequestOptions() {
@Override
public String getCollectionName() {
return collection;
}
@Override
public String getDatabaseName() {
return database;
}
};
}
}
}

View File

@@ -868,8 +868,17 @@ public class Criteria implements CriteriaDefinition {
return right == null;
}
if (left instanceof Pattern) {
return right instanceof Pattern ? ((Pattern) left).pattern().equals(((Pattern) right).pattern()) : false;
if (Pattern.class.isInstance(left)) {
if (!Pattern.class.isInstance(right)) {
return false;
}
Pattern leftPattern = (Pattern) left;
Pattern rightPattern = (Pattern) right;
return leftPattern.pattern().equals(rightPattern.pattern()) //
&& leftPattern.flags() == rightPattern.flags();
}
return ObjectUtils.nullSafeEquals(left, right);

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.mongodb.core.query;
import java.time.Duration;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
@@ -50,6 +51,7 @@ public class Meta {
private final Map<String, Object> values = new LinkedHashMap<String, Object>(2);
private final Set<CursorOption> flags = new LinkedHashSet<CursorOption>();
private Integer cursorBatchSize;
/**
* @return {@literal null} if not set.
@@ -65,7 +67,7 @@ public class Meta {
* @param maxTimeMsec
*/
public void setMaxTimeMsec(long maxTimeMsec) {
setMaxTime(maxTimeMsec, TimeUnit.MILLISECONDS);
setMaxTime(Duration.ofMillis(maxTimeMsec));
}
/**
@@ -73,11 +75,25 @@ public class Meta {
*
* @param timeout
* @param timeUnit
* @deprecated since 2.1. Use {@link #setMaxTime(Duration)} instead.
*/
@Deprecated
public void setMaxTime(long timeout, @Nullable TimeUnit timeUnit) {
setValue(MetaKey.MAX_TIME_MS.key, (timeUnit != null ? timeUnit : TimeUnit.MILLISECONDS).toMillis(timeout));
}
/**
* Set the maximum time limit for processing operations.
*
* @param timeout must not be {@literal null}.
* @since 2.1
*/
public void setMaxTime(Duration timeout) {
Assert.notNull(timeout, "Timeout must not be null!");
setValue(MetaKey.MAX_TIME_MS.key, timeout.toMillis());
}
/**
* @return {@literal null} if not set.
*/
@@ -90,13 +106,15 @@ public class Meta {
* Only scan the specified number of documents.
*
* @param maxScan
* @deprecated since 2.1 due to deprecation in MongoDB 4.0.
*/
@Deprecated
public void setMaxScan(long maxScan) {
setValue(MetaKey.MAX_SCAN.key, maxScan);
}
/**
* Add a comment to the query.
* Add a comment to the query that is propagated to the profile log.
*
* @param comment
*/
@@ -116,7 +134,9 @@ public class Meta {
* Using snapshot prevents the cursor from returning a document more than once.
*
* @param useSnapshot
* @deprecated since 2.1 due to deprecation as of MongoDB 3.6
*/
@Deprecated
public void setSnapshot(boolean useSnapshot) {
setValue(MetaKey.SNAPSHOT.key, useSnapshot);
}
@@ -128,6 +148,27 @@ public class Meta {
return getValue(MetaKey.SNAPSHOT.key, false);
}
/**
* @return {@literal null} if not set.
* @since 2.1
*/
@Nullable
public Integer getCursorBatchSize() {
return cursorBatchSize;
}
/**
* Apply the batch size (number of documents to return in each response) for a query. <br />
* Use {@literal 0 (zero)} for no limit. A <strong>negative limit</strong> closes the cursor after returning a single
* batch indicating to the server that the client will not ask for a subsequent one.
*
* @param cursorBatchSize The number of documents to return per batch.
* @since 2.1
*/
public void setCursorBatchSize(int cursorBatchSize) {
this.cursorBatchSize = cursorBatchSize;
}
/**
* Add {@link CursorOption} influencing behavior of the {@link com.mongodb.DBCursor}.
*
@@ -153,7 +194,7 @@ public class Meta {
* @return
*/
public boolean hasValues() {
return !this.values.isEmpty() || !this.flags.isEmpty();
return !this.values.isEmpty() || !this.flags.isEmpty() || this.cursorBatchSize != null;
}
/**

View File

@@ -18,6 +18,7 @@ package org.springframework.data.mongodb.core.query;
import static org.springframework.data.mongodb.core.query.SerializationUtils.*;
import static org.springframework.util.ObjectUtils.*;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
@@ -289,7 +290,7 @@ public class Query {
/**
* @param maxTimeMsec
* @return
* @return this.
* @see Meta#setMaxTimeMsec(long)
* @since 1.6
*/
@@ -302,22 +303,38 @@ public class Query {
/**
* @param timeout
* @param timeUnit
* @return
* @return this.
* @see Meta#setMaxTime(long, TimeUnit)
* @since 1.6
* @deprecated since 2.1. Use {@link #maxTime(Duration)} instead.
*/
@Deprecated
public Query maxTime(long timeout, TimeUnit timeUnit) {
meta.setMaxTime(timeout, timeUnit);
return this;
}
/**
* @param timeout
* @return this.
* @see Meta#setMaxTime(Duration)
* @since 2.1
*/
public Query maxTime(Duration timeout) {
meta.setMaxTime(timeout);
return this;
}
/**
* @param maxScan
* @return
* @return this.
* @see Meta#setMaxScan(long)
* @since 1.6
* @deprecated since 2.1 due to deprecation in MongoDB 4.0.
*/
@Deprecated
public Query maxScan(long maxScan) {
meta.setMaxScan(maxScan);
@@ -325,8 +342,10 @@ public class Query {
}
/**
* Add a comment to the query that is propagated to the profile log.
*
* @param comment
* @return
* @return this.
* @see Meta#setComment(String)
* @since 1.6
*/
@@ -337,10 +356,12 @@ public class Query {
}
/**
* @return
* @return this.
* @see Meta#setSnapshot(boolean)
* @since 1.6
* @deprecated since 2.1 due to deprecation as of MongoDB 3.6
*/
@Deprecated
public Query useSnapshot() {
meta.setSnapshot(true);
@@ -348,7 +369,23 @@ public class Query {
}
/**
* @return
* Set the number of documents to return in each response batch. <br />
* Use {@literal 0 (zero)} for no limit. A <strong>negative limit</strong> closes the cursor after returning a single
* batch indicating to the server that the client will not ask for a subsequent one.
*
* @param batchSize The number of documents to return per batch.
* @return this.
* @see Meta#setCursorBatchSize(int)
* @since 2.1
*/
public Query cursorBatchSize(int batchSize) {
meta.setCursorBatchSize(batchSize);
return this;
}
/**
* @return this.
* @see org.springframework.data.mongodb.core.query.Meta.CursorOption#NO_TIMEOUT
* @since 1.10
*/
@@ -359,7 +396,7 @@ public class Query {
}
/**
* @return
* @return this.
* @see org.springframework.data.mongodb.core.query.Meta.CursorOption#EXHAUST
* @since 1.10
*/
@@ -370,7 +407,9 @@ public class Query {
}
/**
* @return
* Allows querying of a replica slave.
*
* @return this.
* @see org.springframework.data.mongodb.core.query.Meta.CursorOption#SLAVE_OK
* @since 1.10
*/
@@ -381,7 +420,7 @@ public class Query {
}
/**
* @return
* @return this.
* @see org.springframework.data.mongodb.core.query.Meta.CursorOption#PARTIAL
* @since 1.10
*/
@@ -392,7 +431,7 @@ public class Query {
}
/**
* @return never {@literal null}.
* @return never {@literal null}.ø
* @since 1.6
*/
public Meta getMeta() {

View File

@@ -893,18 +893,14 @@ public class Update {
/**
* Forces values to be added at the given {@literal position}.
*
* @param position needs to be greater than or equal to zero.
* @param position the position offset. As of MongoDB 3.6 use a negative value to indicate starting from the end,
* counting (but not including) the last element of the array.
* @return never {@literal null}.
* @since 1.7
*/
public PushOperatorBuilder atPosition(int position) {
if (position < 0) {
throw new IllegalArgumentException("Position must be greater than or equal to zero.");
}
this.modifiers.addModifier(new PositionModifier(position));
return this;
}

View File

@@ -52,6 +52,7 @@ import com.mongodb.client.gridfs.model.GridFSUploadOptions;
* @author Christoph Strobl
* @author Mark Paluch
* @author Hartmut Lang
* @author Niklas Helge Hanft
*/
public class GridFsTemplate implements GridFsOperations, ResourcePatternResolver {
@@ -228,7 +229,8 @@ public class GridFsTemplate implements GridFsOperations, ResourcePatternResolver
*/
public GridFsResource getResource(String location) {
return Optional.ofNullable(findOne(query(whereFilename().is(location)))).map(this::getResource)
return Optional.ofNullable(findOne(query(whereFilename().is(location)))) //
.map(this::getResource) //
.orElseGet(() -> GridFsResource.absent(location));
}
@@ -240,7 +242,7 @@ public class GridFsTemplate implements GridFsOperations, ResourcePatternResolver
Assert.notNull(file, "GridFSFile must not be null!");
return new GridFsResource(file, getGridFs().openDownloadStream(file.getFilename()));
return new GridFsResource(file, getGridFs().openDownloadStream(file.getObjectId()));
}
/*
@@ -261,7 +263,7 @@ public class GridFsTemplate implements GridFsOperations, ResourcePatternResolver
List<GridFsResource> resources = new ArrayList<>();
for (GridFSFile file : files) {
resources.add(new GridFsResource(file, getGridFs().openDownloadStream(file.getFilename())));
resources.add(getResource(file));
}
return resources.toArray(new GridFsResource[0]);

View File

@@ -21,7 +21,6 @@ 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.QueryAnnotation;
/**
@@ -50,6 +49,16 @@ public @interface Meta {
*/
long maxScanDocuments() default -1;
/**
* Sets the number of documents to return per batch. <br />
* Use {@literal 0 (zero)} for no limit. A <strong>negative limit</strong> closes the cursor after returning a single
* batch indicating to the server that the client will not ask for a subsequent one.
*
* @return {@literal 0 (zero)} by default.
* @since 2.1
*/
int cursorBatchSize() default 0;
/**
* Add a comment to the query.
*

View File

@@ -30,6 +30,7 @@ import org.springframework.data.repository.query.QueryByExampleExecutor;
* @author Christoph Strobl
* @author Thomas Darimont
* @author Mark Paluch
* @author Khaled Baklouti
*/
@NoRepositoryBean
public interface MongoRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
@@ -39,7 +40,7 @@ public interface MongoRepository<T, ID> extends PagingAndSortingRepository<T, ID
* @see org.springframework.data.repository.CrudRepository#saveAll(java.lang.Iterable)
*/
@Override
<S extends T> List<S> saveAll(Iterable<S> entites);
<S extends T> List<S> saveAll(Iterable<S> entities);
/*
* (non-Javadoc)

View File

@@ -77,4 +77,22 @@ public @interface Query {
* @return
*/
boolean delete() default false;
/**
* Defines a default sort order for the given query.<br />
* <strong>NOTE:</strong> The so set defaults can be altered / overwritten using an explicit
* {@link org.springframework.data.domain.Sort} argument of the query method.
*
* <pre>
* <code>
*
* &#64;Query(sort = "{ age : -1 }") // order by age descending
* List<Person> findByFirstname(String firstname);
* </code>
* </pre>
*
* @return
* @since 2.1
*/
String sort() default "";
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.mongodb.repository.query;
import org.bson.Document;
import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind;
import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery;
import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind;
@@ -84,6 +85,7 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
Query query = createQuery(accessor);
applyQueryMetaAttributesWhenPresent(query);
query = applyAnnotatedDefaultSortIfPresent(query);
ResultProcessor processor = method.getResultProcessor().withDynamicProjection(accessor);
Class<?> typeToRead = processor.getReturnedType().getTypeToRead();
@@ -110,7 +112,7 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
} else if (method.isStreamQuery()) {
return q -> operation.matching(q).stream();
} else if (method.isCollectionQuery()) {
return q -> operation.matching(q.with(accessor.getPageable())).all();
return q -> operation.matching(q.with(accessor.getPageable()).with(accessor.getSort())).all();
} else if (method.isPageQuery()) {
return new PagedExecution(operation, accessor.getPageable());
} else if (isCountQuery()) {
@@ -135,6 +137,23 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
return query;
}
/**
* Add a default sort derived from {@link org.springframework.data.mongodb.repository.Query#sort()} to the given
* {@link Query} if present.
*
* @param query the {@link Query} to potentially apply the sort to.
* @return the query with potential default sort applied.
* @since 2.1
*/
Query applyAnnotatedDefaultSortIfPresent(Query query) {
if (!method.hasAnnotatedSort()) {
return query;
}
return QueryUtils.decorateSort(query, Document.parse(method.getAnnotatedSort()));
}
/**
* Creates a {@link Query} instance using the given {@link ConvertingParameterAccessor}. Will delegate to
* {@link #createQuery(ConvertingParameterAccessor)} by default but allows customization of the count query to be

View File

@@ -18,6 +18,7 @@ package org.springframework.data.mongodb.repository.query;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.bson.Document;
import org.reactivestreams.Publisher;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.EntityInstantiators;
@@ -31,11 +32,9 @@ import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecu
import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.GeoNearExecution;
import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.ResultProcessingConverter;
import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.ResultProcessingExecution;
import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.TailExecution;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.util.Assert;
/**
@@ -109,6 +108,7 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
Query query = createQuery(new ConvertingParameterAccessor(operations.getConverter(), parameterAccessor));
applyQueryMetaAttributesWhenPresent(query);
query = applyAnnotatedDefaultSortIfPresent(query);
ResultProcessor processor = method.getResultProcessor().withDynamicProjection(parameterAccessor);
Class<?> typeToRead = processor.getReturnedType().getTypeToRead();
@@ -119,7 +119,7 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
String collection = method.getEntityInformation().getCollectionName();
ReactiveMongoQueryExecution execution = getExecution(query, parameterAccessor,
ReactiveMongoQueryExecution execution = getExecution(parameterAccessor,
new ResultProcessingConverter(processor, operations, instantiators), find);
return execution.execute(query, processor.getReturnedType().getDomainType(), collection);
@@ -128,12 +128,11 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
/**
* Returns the execution instance to use.
*
* @param query must not be {@literal null}.
* @param accessor must not be {@literal null}.
* @param resultProcessing must not be {@literal null}.
* @return
*/
private ReactiveMongoQueryExecution getExecution(Query query, MongoParameterAccessor accessor,
private ReactiveMongoQueryExecution getExecution(MongoParameterAccessor accessor,
Converter<Object, Object> resultProcessing, FindWithQuery<?> operation) {
return new ResultProcessingExecution(getExecutionToWrap(accessor, operation), resultProcessing);
}
@@ -145,11 +144,13 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
} else if (method.isGeoNearQuery()) {
return new GeoNearExecution(operations, accessor, method.getReturnType());
} else if (isTailable(method)) {
return new TailExecution(operations, accessor.getPageable());
return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).tail();
} else if (method.isCollectionQuery()) {
return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).all();
} else if (isCountQuery()) {
return (q, t, c) -> operation.matching(q).count();
} else if (isExistsQuery()) {
return (q, t, c) -> operation.matching(q).exists();
} else {
return (q, t, c) -> {
@@ -177,6 +178,23 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
return query;
}
/**
* Add a default sort derived from {@link org.springframework.data.mongodb.repository.Query#sort()} to the given
* {@link Query} if present.
*
* @param query the {@link Query} to potentially apply the sort to.
* @return the query with potential default sort applied.
* @since 2.1
*/
Query applyAnnotatedDefaultSortIfPresent(Query query) {
if (!method.hasAnnotatedSort()) {
return query;
}
return QueryUtils.decorateSort(query, Document.parse(method.getAnnotatedSort()));
}
/**
* Creates a {@link Query} instance using the given {@link ConvertingParameterAccessor}. Will delegate to
* {@link #createQuery(ConvertingParameterAccessor)} by default but allows customization of the count query to be
@@ -204,6 +222,14 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
*/
protected abstract boolean isCountQuery();
/**
* Returns whether the query should get an exists projection applied.
*
* @return
* @since 2.0.9
*/
protected abstract boolean isExistsQuery();
/**
* Return weather the query should delete matching documents.
*

View File

@@ -0,0 +1,48 @@
/*
* 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.repository.query;
import lombok.experimental.UtilityClass;
/**
* Utility class containing methods to interact with boolean values.
*
* @author Mark Paluch
* @since 2.0.9
*/
@UtilityClass
class BooleanUtil {
/**
* Count the number of {@literal true} values.
*
* @param values
* @return the number of values that are {@literal true}.
*/
static int countBooleanTrueValues(boolean... values) {
int count = 0;
for (boolean value : values) {
if (value) {
count++;
}
}
return count;
}
}

View File

@@ -16,16 +16,19 @@
package org.springframework.data.mongodb.repository.query;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.experimental.UtilityClass;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.UUID;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -205,6 +208,7 @@ class ExpressionEvaluatingParameterBinder {
* @param binding must not be {@literal null}.
* @return
*/
@SuppressWarnings("unchecked")
private String getParameterValueForBinding(MongoParameterAccessor accessor, MongoParameters parameters,
ParameterBinding binding) {
@@ -221,37 +225,7 @@ class ExpressionEvaluatingParameterBinder {
return binding.isExpression() ? JSON.serialize(value) : QuotedString.unquote(JSON.serialize(value));
}
if (value instanceof byte[]) {
if (binding.isQuoted()) {
return DatatypeConverter.printBase64Binary((byte[]) value);
}
return encode(new Binary((byte[]) value), BinaryCodec::new);
}
if (value instanceof UUID) {
if (binding.isQuoted()) {
return value.toString();
}
return encode((UUID) value, UuidCodec::new);
}
return JSON.serialize(value);
}
private <T> String encode(T value, Supplier<Codec<T>> defaultCodec) {
Codec<T> codec = codecRegistryProvider.getCodecFor((Class<T>) value.getClass()).orElseGet(defaultCodec);
StringWriter writer = new StringWriter();
codec.encode(new JsonWriter(writer), value, null);
writer.flush();
return writer.toString();
return EncodableValue.create(value).encode(codecRegistryProvider, binding.isQuoted());
}
/**
@@ -473,4 +447,221 @@ class ExpressionEvaluatingParameterBinder {
return quoted.substring(1, quoted.length() - 1);
}
}
/**
* Value object encapsulating a bindable value, that can be encoded to be represented as JSON (BSON).
*
* @author Mark Paluch
*/
abstract static class EncodableValue {
/**
* Obtain a {@link EncodableValue} given {@code value}.
*
* @param value the value to encode, may be {@literal null}.
* @return the {@link EncodableValue} for {@code value}.
*/
@SuppressWarnings("unchecked")
public static EncodableValue create(@Nullable Object value) {
if (value instanceof byte[]) {
return new BinaryValue((byte[]) value);
}
if (value instanceof UUID) {
return new UuidValue((UUID) value);
}
if (value instanceof Collection) {
Collection<?> collection = (Collection<?>) value;
Class<?> commonElement = CollectionUtils.findCommonElementType(collection);
if (commonElement != null) {
if (UUID.class.isAssignableFrom(commonElement)) {
return new UuidCollection((Collection<UUID>) value);
}
if (byte[].class.isAssignableFrom(commonElement)) {
return new BinaryCollectionValue((Collection<byte[]>) value);
}
}
}
return new ObjectValue(value);
}
/**
* Encode the encapsulated value.
*
* @param provider
* @param quoted
* @return
*/
public abstract String encode(CodecRegistryProvider provider, boolean quoted);
/**
* Encode a {@code value} to JSON.
*
* @param provider
* @param value
* @param defaultCodec
* @param <V>
* @return
*/
protected <V> String encode(CodecRegistryProvider provider, V value, Supplier<Codec<V>> defaultCodec) {
StringWriter writer = new StringWriter();
doEncode(provider, writer, value, defaultCodec);
return writer.toString();
}
/**
* Encode a {@link Collection} to JSON and potentially apply a {@link Function mapping function} before encoding.
*
* @param provider
* @param value
* @param mappingFunction
* @param defaultCodec
* @param <I> Input value type.
* @param <V> Target type.
* @return
*/
protected <I, V> String encodeCollection(CodecRegistryProvider provider, Iterable<I> value,
Function<I, V> mappingFunction, Supplier<Codec<V>> defaultCodec) {
StringWriter writer = new StringWriter();
writer.append("[");
value.forEach(it -> {
if (writer.getBuffer().length() > 1) {
writer.append(", ");
}
doEncode(provider, writer, mappingFunction.apply(it), defaultCodec);
});
writer.append("]");
writer.flush();
return writer.toString();
}
@SuppressWarnings("unchecked")
private <V> void doEncode(CodecRegistryProvider provider, StringWriter writer, V value,
Supplier<Codec<V>> defaultCodec) {
Codec<V> codec = provider.getCodecFor((Class<V>) value.getClass()).orElseGet(defaultCodec);
JsonWriter jsonWriter = new JsonWriter(writer);
codec.encode(jsonWriter, value, null);
jsonWriter.flush();
}
}
/**
* {@link EncodableValue} for {@code byte[]} to render to {@literal $binary}.
*/
@RequiredArgsConstructor
static class BinaryValue extends EncodableValue {
private final byte[] value;
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.ExpressionEvaluatingParameterBinder.EncodableValue#encode(org.springframework.data.mongodb.CodecRegistryProvider, boolean)
*/
@Override
public String encode(CodecRegistryProvider provider, boolean quoted) {
if (quoted) {
return DatatypeConverter.printBase64Binary(this.value);
}
return encode(provider, new Binary(this.value), BinaryCodec::new);
}
}
/**
* {@link EncodableValue} for {@link Collection} containing only {@code byte[]} items to render to a BSON list
* containing {@literal $binary}.
*/
@RequiredArgsConstructor
static class BinaryCollectionValue extends EncodableValue {
private final Collection<byte[]> value;
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.ExpressionEvaluatingParameterBinder.EncodableValue#encode(org.springframework.data.mongodb.CodecRegistryProvider, boolean)
*/
@Override
public String encode(CodecRegistryProvider provider, boolean quoted) {
return encodeCollection(provider, this.value, Binary::new, BinaryCodec::new);
}
}
/**
* {@link EncodableValue} for {@link UUID} to render to {@literal $binary}.
*/
@RequiredArgsConstructor
static class UuidValue extends EncodableValue {
private final UUID value;
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.ExpressionEvaluatingParameterBinder.EncodableValue#encode(org.springframework.data.mongodb.CodecRegistryProvider, boolean)
*/
@Override
public String encode(CodecRegistryProvider provider, boolean quoted) {
if (quoted) {
return this.value.toString();
}
return encode(provider, this.value, UuidCodec::new);
}
}
/**
* {@link EncodableValue} for {@link Collection} containing only {@link UUID} items to render to a BSON list
* containing {@literal $binary}.
*/
@RequiredArgsConstructor
static class UuidCollection extends EncodableValue {
private final Collection<UUID> value;
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.ExpressionEvaluatingParameterBinder.EncodableValue#encode(org.springframework.data.mongodb.CodecRegistryProvider, boolean)
*/
@Override
public String encode(CodecRegistryProvider provider, boolean quoted) {
return encodeCollection(provider, this.value, Function.identity(), UuidCodec::new);
}
}
/**
* Fallback-{@link EncodableValue} for {@link Object}-typed values.
*/
@RequiredArgsConstructor
static class ObjectValue extends EncodableValue {
private final @Nullable Object value;
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.ExpressionEvaluatingParameterBinder.EncodableValue#encode(org.springframework.data.mongodb.CodecRegistryProvider, boolean)
*/
@Override
public String encode(CodecRegistryProvider provider, boolean quoted) {
return JSON.serialize(this.value);
}
}
}

View File

@@ -45,6 +45,7 @@ public interface MongoParameterAccessor extends ParameterAccessor {
*
* @return
*/
@Nullable
Point getGeoNearLocation();
/**

View File

@@ -19,6 +19,7 @@ import java.util.Arrays;
import java.util.List;
import org.springframework.data.domain.Range;
import org.springframework.data.domain.Range.Bound;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Point;
import org.springframework.data.mongodb.core.query.Term;
@@ -66,9 +67,9 @@ public class MongoParametersParameterAccessor extends ParametersParameterAccesso
}
int maxDistanceIndex = mongoParameters.getMaxDistanceIndex();
Distance maxDistance = maxDistanceIndex == -1 ? null : (Distance) getValue(maxDistanceIndex);
Bound<Distance> maxDistance = maxDistanceIndex == -1 ? Bound.unbounded() : Bound.inclusive((Distance) getValue(maxDistanceIndex));
return new Range<Distance>(null, maxDistance);
return Range.of(Bound.unbounded(), maxDistance);
}
/*

View File

@@ -21,6 +21,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.Optional;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -113,9 +114,8 @@ class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
PersistentPropertyPath<MongoPersistentProperty> path = context.getPersistentPropertyPath(part.getProperty());
MongoPersistentProperty property = path.getLeafProperty();
Criteria criteria = from(part, property, where(path.toDotPath()), (PotentiallyConvertingIterator) iterator);
return criteria;
return from(part, property, where(path.toDotPath()), iterator);
}
/*
@@ -132,7 +132,7 @@ class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
PersistentPropertyPath<MongoPersistentProperty> path = context.getPersistentPropertyPath(part.getProperty());
MongoPersistentProperty property = path.getLeafProperty();
return from(part, property, base.and(path.toDotPath()), (PotentiallyConvertingIterator) iterator);
return from(part, property, base.and(path.toDotPath()), iterator);
}
/*
@@ -206,7 +206,9 @@ class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
case NOT_CONTAINING:
return createContainingCriteria(part, property, criteria.not(), parameters);
case REGEX:
return criteria.regex(parameters.next().toString());
Object param = parameters.next();
return param instanceof Pattern ? criteria.regex((Pattern) param) : criteria.regex(param.toString());
case EXISTS:
return criteria.exists((Boolean) parameters.next());
case TRUE:

View File

@@ -16,13 +16,14 @@
package org.springframework.data.mongodb.repository.query;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.geo.GeoPage;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
@@ -40,6 +41,7 @@ import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -57,6 +59,7 @@ public class MongoQueryMethod extends QueryMethod {
private final Method method;
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
private final Map<Class<? extends Annotation>, Optional<Annotation>> annotationCache;
private @Nullable MongoEntityMetadata<?> metadata;
@@ -77,6 +80,7 @@ public class MongoQueryMethod extends QueryMethod {
this.method = method;
this.mappingContext = mappingContext;
this.annotationCache = new ConcurrentReferenceHashMap<>();
}
/*
@@ -110,9 +114,8 @@ public class MongoQueryMethod extends QueryMethod {
private Optional<String> findAnnotatedQuery() {
return Optional.ofNullable(getQueryAnnotation()) //
.map(AnnotationUtils::getValue) //
.map(it -> (String) it) //
return lookupQueryAnnotation() //
.map(Query::value) //
.filter(StringUtils::hasText);
}
@@ -121,10 +124,11 @@ public class MongoQueryMethod extends QueryMethod {
*
* @return
*/
@Nullable
String getFieldSpecification() {
return Optional.ofNullable(getQueryAnnotation()) //
.map(it -> (String) AnnotationUtils.getValue(it, "fields")) //
return lookupQueryAnnotation() //
.map(Query::fields) //
.filter(StringUtils::hasText) //
.orElse(null);
}
@@ -156,8 +160,7 @@ public class MongoQueryMethod extends QueryMethod {
MongoPersistentEntity<?> collectionEntity = domainClass.isAssignableFrom(returnedObjectType) ? returnedEntity
: managedEntity;
this.metadata = new SimpleMongoEntityMetadata<Object>((Class<Object>) returnedEntity.getType(),
collectionEntity);
this.metadata = new SimpleMongoEntityMetadata<>((Class<Object>) returnedEntity.getType(), collectionEntity);
}
}
@@ -207,7 +210,11 @@ public class MongoQueryMethod extends QueryMethod {
*/
@Nullable
Query getQueryAnnotation() {
return AnnotatedElementUtils.findMergedAnnotation(method, Query.class);
return lookupQueryAnnotation().orElse(null);
}
Optional<Query> lookupQueryAnnotation() {
return doFindAnnotation(Query.class);
}
TypeInformation<?> getReturnType() {
@@ -230,7 +237,7 @@ public class MongoQueryMethod extends QueryMethod {
*/
@Nullable
Meta getMetaAnnotation() {
return AnnotatedElementUtils.findMergedAnnotation(method, Meta.class);
return doFindAnnotation(Meta.class).orElse(null);
}
/**
@@ -241,7 +248,7 @@ public class MongoQueryMethod extends QueryMethod {
*/
@Nullable
Tailable getTailableAnnotation() {
return AnnotatedElementUtils.findMergedAnnotation(method, Tailable.class);
return doFindAnnotation(Tailable.class).orElse(null);
}
/**
@@ -266,6 +273,10 @@ public class MongoQueryMethod extends QueryMethod {
metaAttributes.setMaxScan(meta.maxScanDocuments());
}
if (meta.cursorBatchSize() != 0) {
metaAttributes.setCursorBatchSize(meta.cursorBatchSize());
}
if (StringUtils.hasText(meta.comment())) {
metaAttributes.setComment(meta.comment());
}
@@ -283,4 +294,35 @@ public class MongoQueryMethod extends QueryMethod {
return metaAttributes;
}
/**
* Check if the query method is decorated with an non empty {@link Query#sort()}.
*
* @return true if method annotated with {@link Query} having an non empty sort attribute.
* @since 2.1
*/
public boolean hasAnnotatedSort() {
return lookupQueryAnnotation().map(it -> !it.sort().isEmpty()).orElse(false);
}
/**
* Get the sort value, used as default, extracted from the {@link Query} annotation.
*
* @return the {@link Query#sort()} value.
* @throws IllegalStateException if method not annotated with {@link Query}. Make sure to check
* {@link #hasAnnotatedQuery()} first.
* @since 2.1
*/
public String getAnnotatedSort() {
return lookupQueryAnnotation().map(Query::sort).orElseThrow(() -> new IllegalStateException(
"Expected to find @Query annotation but did not. Make sure to check hasAnnotatedSort() before."));
}
@SuppressWarnings("unchecked")
private <A extends Annotation> Optional<A> doFindAnnotation(Class<A> annotationType) {
return (Optional<A>) this.annotationCache.computeIfAbsent(annotationType,
it -> Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, it)));
}
}

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.repository.query;
import org.aopalliance.intercept.MethodInterceptor;
import org.bson.Document;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.data.mongodb.core.query.Query;
/**
* Internal utility class to help avoid duplicate code required in both the reactive and the sync {@link Query} support
* offered by repositories.
*
* @author Christoph Strobl
* @since 2.1
* @currentRead Assassin's Apprentice - Robin Hobb
*/
class QueryUtils {
/**
* Decorate {@link Query} and add a default sort expression to the given {@link Query}. Attributes of the given
* {@code sort} may be overwritten by the sort explicitly defined by the {@link Query} itself.
*
* @param query the {@link Query} to decorate.
* @param defaultSort the default sort expression to apply to the query.
* @return the query having the given {@code sort} applied.
*/
static Query decorateSort(Query query, Document defaultSort) {
if (defaultSort.isEmpty()) {
return query;
}
ProxyFactory factory = new ProxyFactory(query);
factory.addAdvice((MethodInterceptor) invocation -> {
if (!invocation.getMethod().getName().equals("getSortObject")) {
return invocation.proceed();
}
Document combinedSort = new Document(defaultSort);
combinedSort.putAll((Document) invocation.proceed());
return combinedSort;
});
return (Query) factory.getProxy();
}
}

View File

@@ -51,23 +51,6 @@ interface ReactiveMongoQueryExecution {
Object execute(Query query, Class<?> type, String collection);
/**
* {@link ReactiveMongoQueryExecution} for collection returning queries using tailable cursors.
*
* @author Mark Paluch
*/
@RequiredArgsConstructor
final class TailExecution implements ReactiveMongoQueryExecution {
private final @NonNull ReactiveMongoOperations operations;
private final Pageable pageable;
@Override
public Object execute(Query query, Class<?> type, String collection) {
return operations.tail(query.with(pageable), type, collection);
}
}
/**
* {@link MongoQueryExecution} to execute geo-near queries.
*

View File

@@ -79,7 +79,7 @@ public class ReactiveMongoQueryMethod extends MongoQueryMethod {
ClassUtils.getShortName(method.getDeclaringClass()), method.getName()));
}
if (!multiWrapper && !singleWrapperWithWrappedPageableResult) {
if (!multiWrapper) {
throw new IllegalStateException(String.format(
"Method has to use a either multi-item reactive wrapper return type or a wrapped Page/Slice type. Offending method: %s",
method.toString()));

View File

@@ -134,6 +134,15 @@ public class ReactivePartTreeMongoQuery extends AbstractReactiveMongoQuery {
return tree.isCountProjection();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.AbstractReactiveMongoQuery#isExistsQuery()
*/
@Override
protected boolean isExistsQuery() {
return tree.isExistsProjection();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.AbstractReactiveMongoQuery#isDeleteQuery()

View File

@@ -28,7 +28,6 @@ import org.springframework.data.mongodb.repository.query.ExpressionEvaluatingPar
import org.springframework.data.mongodb.repository.query.StringBasedMongoQuery.ParameterBinding;
import org.springframework.data.mongodb.repository.query.StringBasedMongoQuery.ParameterBindingParser;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert;
@@ -41,13 +40,14 @@ import org.springframework.util.Assert;
*/
public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery {
private static final String COUND_AND_DELETE = "Manually defined query for %s cannot be both a count and delete query at the same time!";
private static final String COUNT_EXISTS_AND_DELETE = "Manually defined query for %s cannot be a count and exists or delete query at the same time!";
private static final Logger LOG = LoggerFactory.getLogger(ReactiveStringBasedMongoQuery.class);
private static final ParameterBindingParser BINDING_PARSER = ParameterBindingParser.INSTANCE;
private final String query;
private final String fieldSpec;
private final boolean isCountQuery;
private final boolean isExistsQuery;
private final boolean isDeleteQuery;
private final List<ParameterBinding> queryParameterBindings;
private final List<ParameterBinding> fieldSpecParameterBindings;
@@ -93,11 +93,23 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery {
this.fieldSpec = BINDING_PARSER.parseAndCollectParameterBindingsFromQueryIntoBindings(
method.getFieldSpecification(), this.fieldSpecParameterBindings);
this.isCountQuery = method.hasAnnotatedQuery() ? method.getQueryAnnotation().count() : false;
this.isDeleteQuery = method.hasAnnotatedQuery() ? method.getQueryAnnotation().delete() : false;
if (method.hasAnnotatedQuery()) {
if (isCountQuery && isDeleteQuery) {
throw new IllegalArgumentException(String.format(COUND_AND_DELETE, method));
org.springframework.data.mongodb.repository.Query queryAnnotation = method.getQueryAnnotation();
this.isCountQuery = queryAnnotation.count();
this.isExistsQuery = queryAnnotation.exists();
this.isDeleteQuery = queryAnnotation.delete();
if (hasAmbiguousProjectionFlags(this.isCountQuery, this.isExistsQuery, this.isDeleteQuery)) {
throw new IllegalArgumentException(String.format(COUNT_EXISTS_AND_DELETE, method));
}
} else {
this.isCountQuery = false;
this.isExistsQuery = false;
this.isDeleteQuery = false;
}
this.parameterBinder = new ExpressionEvaluatingParameterBinder(expressionParser, evaluationContextProvider);
@@ -133,6 +145,15 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery {
return isCountQuery;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.AbstractReactiveMongoQuery#isExistsQuery()
*/
@Override
protected boolean isExistsQuery() {
return isExistsQuery;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.AbstractReactiveMongoQuery#isDeleteQuery()
@@ -151,4 +172,9 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery {
return false;
}
private static boolean hasAmbiguousProjectionFlags(boolean isCountQuery, boolean isExistsQuery,
boolean isDeleteQuery) {
return BooleanUtil.countBooleanTrueValues(isCountQuery, isExistsQuery, isDeleteQuery) > 1;
}
}

View File

@@ -20,7 +20,9 @@ import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.bson.BSON;
import org.bson.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -37,6 +39,7 @@ import org.springframework.util.StringUtils;
import com.mongodb.DBObject;
import com.mongodb.DBRef;
import com.mongodb.util.JSON;
import com.mongodb.util.JSONCallback;
/**
* Query to use a plain JSON String to create the {@link Query} to actually execute.
@@ -170,11 +173,6 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
return this.isDeleteQuery;
}
private static boolean hasAmbiguousProjectionFlags(boolean isCountQuery, boolean isExistsQuery,
boolean isDeleteQuery) {
return countBooleanValues(isCountQuery, isExistsQuery, isDeleteQuery) > 1;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery#isLimiting()
@@ -184,18 +182,9 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
return false;
}
private static int countBooleanValues(boolean... values) {
int count = 0;
for (boolean value : values) {
if (value) {
count++;
}
}
return count;
private static boolean hasAmbiguousProjectionFlags(boolean isCountQuery, boolean isExistsQuery,
boolean isDeleteQuery) {
return BooleanUtil.countBooleanTrueValues(isCountQuery, isExistsQuery, isDeleteQuery) > 1;
}
/**
@@ -239,7 +228,8 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
String transformedInput = transformQueryAndCollectExpressionParametersIntoBindings(input, bindings);
String parseableInput = makeParameterReferencesParseable(transformedInput);
collectParameterReferencesIntoBindings(bindings, JSON.parse(parseableInput));
collectParameterReferencesIntoBindings(bindings,
JSON.parse(parseableInput, new LenientPatternDecodingCallback()));
return transformedInput;
}
@@ -374,6 +364,43 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
}
}
/**
* {@link JSONCallback} with lenient handling for {@link PatternSyntaxException} falling back to a placeholder
* {@link Pattern} for intermediate query document rendering.
*/
private static class LenientPatternDecodingCallback extends JSONCallback {
private static final Pattern EMPTY_MARKER = Pattern.compile("__Spring_Data_MongoDB_Bind_Marker__");
/*
* (non-Javadoc)
* @see com.mongodb.util.JSONCallback#objectDone()
*/
@Override
public Object objectDone() {
return exceptionSwallowingStackReducingObjectDone();
}
private Object exceptionSwallowingStackReducingObjectDone/*CauseWeJustNeedTheStructureNotTheActualValue*/() {
Object value;
try {
return super.objectDone();
} catch (PatternSyntaxException e) {
value = EMPTY_MARKER;
}
if (!isStackEmpty()) {
_put(curName(), value);
} else {
value = !BSON.hasDecodeHooks() ? value : BSON.applyDecodingHooks(value);
setRoot(value);
}
return value;
}
}
/**
* A generic parameter binding with name or position information.
*

View File

@@ -15,12 +15,10 @@
*/
package org.springframework.data.mongodb.repository.support;
import org.springframework.data.domain.Persistable;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Support class responsible for creating {@link MongoEntityInformation} instances for a given
@@ -47,10 +45,6 @@ final class MongoEntityInformationSupport {
Assert.notNull(entity, "Entity must not be null!");
MappingMongoEntityInformation<T, ID> entityInformation = new MappingMongoEntityInformation<T, ID>(
(MongoPersistentEntity<T>) entity, (Class<ID>) idType);
return ClassUtils.isAssignable(Persistable.class, entity.getType())
? new PersistableMongoEntityInformation<T, ID>(entityInformation) : entityInformation;
return new MappingMongoEntityInformation<>((MongoPersistentEntity<T>) entity, (Class<ID>) idType);
}
}

View File

@@ -0,0 +1,437 @@
/*
* 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.repository.support;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import org.bson.BsonJavaScript;
import org.bson.BsonRegularExpression;
import org.bson.Document;
import org.bson.types.ObjectId;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import com.mongodb.DBRef;
import com.querydsl.core.types.*;
import com.querydsl.mongodb.MongodbOps;
/**
* <p>
* Serializes the given Querydsl query to a Document query for MongoDB.
* </p>
* <p>
* Original implementation source {@link com.querydsl.mongodb.MongodbSerializer} by {@literal The Querydsl Team}
* (<a href="http://www.querydsl.com/team">http://www.querydsl.com/team</a>) licensed under the Apache License, Version
* 2.0.
* </p>
* Modified to use {@link Document} instead of {@link com.mongodb.DBObject}, updated nullable types and code format. Use
* Bson specific types and add {@link QuerydslMongoOps#NO_MATCH}.
*
* @author laimw
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.1
*/
abstract class MongodbDocumentSerializer implements Visitor<Object, Void> {
@Nullable
Object handle(Expression<?> expression) {
return expression.accept(this, null);
}
/**
* Create the MongoDB specific query document.
*
* @param predicate must not be {@literal null}.
* @return empty {@link Document} by default.
*/
Document toQuery(Predicate predicate) {
Object value = handle(predicate);
if (value == null) {
return new Document();
}
Assert.isInstanceOf(Document.class, value,
() -> String.format("Invalid type. Expected Document but found %s", value.getClass()));
return (Document) value;
}
/**
* Create the MongoDB specific sort document.
*
* @param orderBys must not be {@literal null}.
* @return empty {@link Document} by default.
*/
Document toSort(List<OrderSpecifier<?>> orderBys) {
Document sort = new Document();
orderBys.forEach(orderSpecifier -> {
Object key = orderSpecifier.getTarget().accept(this, null);
Assert.notNull(key, () -> String.format("Mapped sort key for %s must not be null!", orderSpecifier));
sort.append(key.toString(), orderSpecifier.getOrder() == Order.ASC ? 1 : -1);
});
return sort;
}
/*
* (non-Javadoc)
* @see com.querydsl.core.types.Visitor#visit(com.querydsl.core.types.Constant, java.lang.Void)
*/
@Override
public Object visit(Constant<?> expr, Void context) {
if (!Enum.class.isAssignableFrom(expr.getType())) {
return expr.getConstant();
}
@SuppressWarnings("unchecked") // Guarded by previous check
Constant<? extends Enum<?>> expectedExpr = (Constant<? extends Enum<?>>) expr;
return expectedExpr.getConstant().name();
}
/*
* (non-Javadoc)
* @see com.querydsl.core.types.Visitor#visit(com.querydsl.core.types.TemplateExpression, java.lang.Void)
*/
@Override
public Object visit(TemplateExpression<?> expr, Void context) {
throw new UnsupportedOperationException();
}
/*
* (non-Javadoc)
* @see com.querydsl.core.types.Visitor#visit(com.querydsl.core.types.FactoryExpression, java.lang.Void)
*/
@Override
public Object visit(FactoryExpression<?> expr, Void context) {
throw new UnsupportedOperationException();
}
protected String asDBKey(Operation<?> expr, int index) {
String key = (String) asDBValue(expr, index);
Assert.hasText(key, () -> String.format("Mapped key must not be null nor empty for expression %s.", expr));
return key;
}
@Nullable
protected Object asDBValue(Operation<?> expr, int index) {
return expr.getArg(index).accept(this, null);
}
private String regexValue(Operation<?> expr, int index) {
Object value = expr.getArg(index).accept(this, null);
Assert.notNull(value, () -> String.format("Regex for %s must not be null.", expr));
return Pattern.quote(value.toString());
}
protected Document asDocument(String key, @Nullable Object value) {
return new Document(key, value);
}
@SuppressWarnings("unchecked")
@Override
public Object visit(Operation<?> expr, Void context) {
Operator op = expr.getOperator();
if (op == Ops.EQ) {
if (expr.getArg(0) instanceof Operation) {
Operation<?> lhs = (Operation<?>) expr.getArg(0);
if (lhs.getOperator() == Ops.COL_SIZE || lhs.getOperator() == Ops.ARRAY_SIZE) {
return asDocument(asDBKey(lhs, 0), asDocument("$size", asDBValue(expr, 1)));
} else {
throw new UnsupportedOperationException("Illegal operation " + expr);
}
} else if (expr.getArg(0) instanceof Path) {
Path<?> path = (Path<?>) expr.getArg(0);
Constant<?> constant = (Constant<?>) expr.getArg(1);
return asDocument(asDBKey(expr, 0), convert(path, constant));
}
} else if (op == Ops.STRING_IS_EMPTY) {
return asDocument(asDBKey(expr, 0), "");
} else if (op == Ops.AND) {
Map<Object, Object> lhs = (Map<Object, Object>) handle(expr.getArg(0));
Map<Object, Object> rhs = (Map<Object, Object>) handle(expr.getArg(1));
LinkedHashSet<Entry<Object, Object>> lhs2 = new LinkedHashSet<>(lhs.entrySet());
lhs2.retainAll(rhs.entrySet());
if (lhs2.isEmpty()) {
lhs.putAll(rhs);
return lhs;
} else {
List<Object> list = new ArrayList<>(2);
list.add(handle(expr.getArg(0)));
list.add(handle(expr.getArg(1)));
return asDocument("$and", list);
}
} else if (op == Ops.NOT) {
// Handle the not's child
Operation<?> subOperation = (Operation<?>) expr.getArg(0);
Operator subOp = subOperation.getOperator();
if (subOp == Ops.IN) {
return visit(
ExpressionUtils.operation(Boolean.class, Ops.NOT_IN, subOperation.getArg(0), subOperation.getArg(1)),
context);
} else {
Document arg = (Document) handle(expr.getArg(0));
return negate(arg);
}
} else if (op == Ops.OR) {
List<Object> list = new ArrayList<>(2);
list.add(handle(expr.getArg(0)));
list.add(handle(expr.getArg(1)));
return asDocument("$or", list);
} else if (op == Ops.NE) {
Path<?> path = (Path<?>) expr.getArg(0);
Constant<?> constant = (Constant<?>) expr.getArg(1);
return asDocument(asDBKey(expr, 0), asDocument("$ne", convert(path, constant)));
} else if (op == Ops.STARTS_WITH) {
return asDocument(asDBKey(expr, 0), new BsonRegularExpression("^" + regexValue(expr, 1)));
} else if (op == Ops.STARTS_WITH_IC) {
return asDocument(asDBKey(expr, 0), new BsonRegularExpression("^" + regexValue(expr, 1), "i"));
} else if (op == Ops.ENDS_WITH) {
return asDocument(asDBKey(expr, 0), new BsonRegularExpression(regexValue(expr, 1) + "$"));
} else if (op == Ops.ENDS_WITH_IC) {
return asDocument(asDBKey(expr, 0), new BsonRegularExpression(regexValue(expr, 1) + "$", "i"));
} else if (op == Ops.EQ_IGNORE_CASE) {
return asDocument(asDBKey(expr, 0), new BsonRegularExpression("^" + regexValue(expr, 1) + "$", "i"));
} else if (op == Ops.STRING_CONTAINS) {
return asDocument(asDBKey(expr, 0), new BsonRegularExpression(".*" + regexValue(expr, 1) + ".*"));
} else if (op == Ops.STRING_CONTAINS_IC) {
return asDocument(asDBKey(expr, 0), new BsonRegularExpression(".*" + regexValue(expr, 1) + ".*", "i"));
} else if (op == Ops.MATCHES) {
return asDocument(asDBKey(expr, 0), new BsonRegularExpression(asDBValue(expr, 1).toString()));
} else if (op == Ops.MATCHES_IC) {
return asDocument(asDBKey(expr, 0), new BsonRegularExpression(asDBValue(expr, 1).toString(), "i"));
} else if (op == Ops.LIKE) {
String regex = ExpressionUtils.likeToRegex((Expression) expr.getArg(1)).toString();
return asDocument(asDBKey(expr, 0), new BsonRegularExpression(regex));
} else if (op == Ops.BETWEEN) {
Document value = new Document("$gte", asDBValue(expr, 1));
value.append("$lte", asDBValue(expr, 2));
return asDocument(asDBKey(expr, 0), value);
} else if (op == Ops.IN) {
int constIndex = 0;
int exprIndex = 1;
if (expr.getArg(1) instanceof Constant<?>) {
constIndex = 1;
exprIndex = 0;
}
if (Collection.class.isAssignableFrom(expr.getArg(constIndex).getType())) {
@SuppressWarnings("unchecked") // guarded by previous check
Collection<?> values = ((Constant<? extends Collection<?>>) expr.getArg(constIndex)).getConstant();
return asDocument(asDBKey(expr, exprIndex), asDocument("$in", values));
} else {
Path<?> path = (Path<?>) expr.getArg(exprIndex);
Constant<?> constant = (Constant<?>) expr.getArg(constIndex);
return asDocument(asDBKey(expr, exprIndex), convert(path, constant));
}
} else if (op == Ops.NOT_IN) {
int constIndex = 0;
int exprIndex = 1;
if (expr.getArg(1) instanceof Constant<?>) {
constIndex = 1;
exprIndex = 0;
}
if (Collection.class.isAssignableFrom(expr.getArg(constIndex).getType())) {
@SuppressWarnings("unchecked") // guarded by previous check
Collection<?> values = ((Constant<? extends Collection<?>>) expr.getArg(constIndex)).getConstant();
return asDocument(asDBKey(expr, exprIndex), asDocument("$nin", values));
} else {
Path<?> path = (Path<?>) expr.getArg(exprIndex);
Constant<?> constant = (Constant<?>) expr.getArg(constIndex);
return asDocument(asDBKey(expr, exprIndex), asDocument("$ne", convert(path, constant)));
}
} else if (op == Ops.COL_IS_EMPTY) {
List<Object> list = new ArrayList<>(2);
list.add(asDocument(asDBKey(expr, 0), new ArrayList<Object>()));
list.add(asDocument(asDBKey(expr, 0), asDocument("$exists", false)));
return asDocument("$or", list);
} else if (op == Ops.LT) {
return asDocument(asDBKey(expr, 0), asDocument("$lt", asDBValue(expr, 1)));
} else if (op == Ops.GT) {
return asDocument(asDBKey(expr, 0), asDocument("$gt", asDBValue(expr, 1)));
} else if (op == Ops.LOE) {
return asDocument(asDBKey(expr, 0), asDocument("$lte", asDBValue(expr, 1)));
} else if (op == Ops.GOE) {
return asDocument(asDBKey(expr, 0), asDocument("$gte", asDBValue(expr, 1)));
} else if (op == Ops.IS_NULL) {
return asDocument(asDBKey(expr, 0), asDocument("$exists", false));
} else if (op == Ops.IS_NOT_NULL) {
return asDocument(asDBKey(expr, 0), asDocument("$exists", true));
} else if (op == Ops.CONTAINS_KEY) {
Path<?> path = (Path<?>) expr.getArg(0);
Expression<?> key = expr.getArg(1);
return asDocument(visit(path, context) + "." + key.toString(), asDocument("$exists", true));
} else if (op == MongodbOps.NEAR) {
return asDocument(asDBKey(expr, 0), asDocument("$near", asDBValue(expr, 1)));
} else if (op == MongodbOps.NEAR_SPHERE) {
return asDocument(asDBKey(expr, 0), asDocument("$nearSphere", asDBValue(expr, 1)));
} else if (op == MongodbOps.ELEM_MATCH) {
return asDocument(asDBKey(expr, 0), asDocument("$elemMatch", asDBValue(expr, 1)));
} else if (op == QuerydslMongoOps.NO_MATCH) {
return new Document("$where", new BsonJavaScript("function() { return false }"));
}
throw new UnsupportedOperationException("Illegal operation " + expr);
}
private Object negate(Document arg) {
List<Object> list = new ArrayList<>();
for (Map.Entry<String, Object> entry : arg.entrySet()) {
if (entry.getKey().equals("$or")) {
list.add(asDocument("$nor", entry.getValue()));
} else if (entry.getKey().equals("$and")) {
List<Object> list2 = new ArrayList<>();
for (Object o : ((Collection) entry.getValue())) {
list2.add(negate((Document) o));
}
list.add(asDocument("$or", list2));
} else if (entry.getValue() instanceof Pattern || entry.getValue() instanceof BsonRegularExpression) {
list.add(asDocument(entry.getKey(), asDocument("$not", entry.getValue())));
} else if (entry.getValue() instanceof Document) {
list.add(negate(entry.getKey(), (Document) entry.getValue()));
} else {
list.add(asDocument(entry.getKey(), asDocument("$ne", entry.getValue())));
}
}
return list.size() == 1 ? list.get(0) : asDocument("$or", list);
}
private Object negate(String key, Document value) {
if (value.size() == 1) {
return asDocument(key, asDocument("$not", value));
} else {
List<Object> list2 = new ArrayList<>();
for (Map.Entry<String, Object> entry2 : value.entrySet()) {
list2.add(asDocument(key, asDocument("$not", asDocument(entry2.getKey(), entry2.getValue()))));
}
return asDocument("$or", list2);
}
}
protected Object convert(Path<?> property, Constant<?> constant) {
if (isReference(property)) {
return asReference(constant.getConstant());
} else if (isId(property)) {
if (isReference(property.getMetadata().getParent())) {
return asReferenceKey(property.getMetadata().getParent().getType(), constant.getConstant());
} else if (constant.getType().equals(String.class) && isImplicitObjectIdConversion()) {
String id = (String) constant.getConstant();
return ObjectId.isValid(id) ? new ObjectId(id) : id;
}
}
return visit(constant, null);
}
protected boolean isImplicitObjectIdConversion() {
return true;
}
protected DBRef asReferenceKey(Class<?> entity, Object id) {
// TODO override in subclass
throw new UnsupportedOperationException();
}
protected abstract DBRef asReference(Object constant);
protected abstract boolean isReference(@Nullable Path<?> arg);
protected boolean isId(Path<?> arg) {
// TODO override in subclass
return false;
}
@Override
public String visit(Path<?> expr, Void context) {
PathMetadata metadata = expr.getMetadata();
if (metadata.getParent() != null) {
Path<?> parent = metadata.getParent();
if (parent.getMetadata().getPathType() == PathType.DELEGATE) {
parent = parent.getMetadata().getParent();
}
if (metadata.getPathType() == PathType.COLLECTION_ANY) {
return visit(parent, context);
} else if (parent.getMetadata().getPathType() != PathType.VARIABLE) {
String rv = getKeyForPath(expr, metadata);
String parentStr = visit(parent, context);
return rv != null ? parentStr + "." + rv : parentStr;
}
}
return getKeyForPath(expr, metadata);
}
protected String getKeyForPath(Path<?> expr, PathMetadata metadata) {
return metadata.getElement().toString();
}
@Override
public Object visit(SubQueryExpression<?> expr, Void context) {
throw new UnsupportedOperationException();
}
@Override
public Object visit(ParamExpression<?> expr, Void context) {
throw new UnsupportedOperationException();
}
}

View File

@@ -1,103 +0,0 @@
/*
* Copyright 2017-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.repository.support;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Persistable;
import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
/**
* {@link MongoEntityInformation} implementation wrapping an existing {@link MongoEntityInformation} considering
* {@link Persistable} types by delegating {@link #isNew(Object)} and {@link #getId(Object)} to the corresponding
* {@link Persistable#isNew()} and {@link Persistable#getId()} implementations.
*
* @author Christoph Strobl
* @author Oliver Gierke
* @since 1.10
*/
@RequiredArgsConstructor
class PersistableMongoEntityInformation<T, ID> implements MongoEntityInformation<T, ID> {
private final @NonNull MongoEntityInformation<T, ID> delegate;
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.MongoEntityInformation#getCollectionName()
*/
@Override
public String getCollectionName() {
return delegate.getCollectionName();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.MongoEntityInformation#getIdAttribute()
*/
@Override
public String getIdAttribute() {
return delegate.getIdAttribute();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.EntityInformation#isNew(java.lang.Object)
*/
@Override
@SuppressWarnings("unchecked")
public boolean isNew(T t) {
if (t instanceof Persistable) {
return ((Persistable<ID>) t).isNew();
}
return delegate.isNew(t);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.EntityInformation#getId(java.lang.Object)
*/
@Override
@SuppressWarnings("unchecked")
public ID getId(T t) {
if (t instanceof Persistable) {
return ((Persistable<ID>) t).getId();
}
return delegate.getId(t);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.PersistentEntityInformation#getIdType()
*/
@Override
public Class<ID> getIdType() {
return delegate.getIdType();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.EntityMetadata#getJavaType()
*/
@Override
public Class<T> getJavaType() {
return delegate.getJavaType();
}
}

View File

@@ -0,0 +1,202 @@
/*
* 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.repository.support;
import java.util.List;
import org.bson.Document;
import org.springframework.lang.Nullable;
import com.querydsl.core.DefaultQueryMetadata;
import com.querydsl.core.QueryModifiers;
import com.querydsl.core.SimpleQuery;
import com.querydsl.core.support.QueryMixin;
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.FactoryExpression;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.ParamExpression;
import com.querydsl.core.types.Predicate;
/**
* {@code QuerydslAbstractMongodbQuery} provides a base class for general Querydsl query implementation.
* <p>
* Original implementation source {@link com.querydsl.mongodb.AbstractMongodbQuery} by {@literal The Querydsl Team}
* (<a href="http://www.querydsl.com/team">http://www.querydsl.com/team</a>) licensed under the Apache License, Version
* 2.0.
* </p>
* Modified for usage with {@link MongodbDocumentSerializer}.
*
* @param <Q> concrete subtype
* @author laimw
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.1
*/
public abstract class QuerydslAbstractMongodbQuery<K, Q extends QuerydslAbstractMongodbQuery<K, Q>>
implements SimpleQuery<Q> {
private final MongodbDocumentSerializer serializer;
private final QueryMixin<Q> queryMixin;
/**
* Create a new MongodbQuery instance
*
* @param serializer serializer
*/
@SuppressWarnings("unchecked")
QuerydslAbstractMongodbQuery(MongodbDocumentSerializer serializer) {
this.queryMixin = new QueryMixin<>((Q) this, new DefaultQueryMetadata(), false);
this.serializer = serializer;
}
/*
* (non-Javadoc)
* @see com.querydsl.core.SimpleQuery#distinct()
*/
@Override
public Q distinct() {
return queryMixin.distinct();
}
/*
* (non-Javadoc)
* @see com.querydsl.core.FilteredClause#where(com.querydsl.core.types.Predicate[])
*/
@Override
public Q where(Predicate... e) {
return queryMixin.where(e);
}
/*
* (non-Javadoc)
* @see com.querydsl.core.SimpleQuery#limit(long)
*/
@Override
public Q limit(long limit) {
return queryMixin.limit(limit);
}
/*
* (non-Javadoc)
* @see com.querydsl.core.SimpleQuery#offset()
*/
@Override
public Q offset(long offset) {
return queryMixin.offset(offset);
}
/*
* (non-Javadoc)
* @see com.querydsl.core.SimpleQuery#restrict(com.querydsl.core.QueryModifiers)
*/
@Override
public Q restrict(QueryModifiers modifiers) {
return queryMixin.restrict(modifiers);
}
/*
* (non-Javadoc)
* @see com.querydsl.core.SimpleQuery#orderBy(com.querydsl.core.types.OrderSpecifier)
*/
@Override
public Q orderBy(OrderSpecifier<?>... o) {
return queryMixin.orderBy(o);
}
/*
* (non-Javadoc)
* @see com.querydsl.core.SimpleQuery#set(com.querydsl.core.types.ParamExpression, Object)
*/
@Override
public <T> Q set(ParamExpression<T> param, T value) {
return queryMixin.set(param, value);
}
/**
* Compute the actual projection {@link Document} from a given projectionExpression by serializing the contained
* {@link Expression expressions} individually.
*
* @param projectionExpression the computed projection {@link Document}.
* @return never {@literal null}. An empty {@link Document} by default.
* @see MongodbDocumentSerializer#handle(Expression)
*/
protected Document createProjection(@Nullable Expression<?> projectionExpression) {
if (!(projectionExpression instanceof FactoryExpression)) {
return new Document();
}
Document projection = new Document();
((FactoryExpression<?>) projectionExpression).getArgs().stream() //
.filter(Expression.class::isInstance) //
.map(Expression.class::cast) //
.map(serializer::handle) //
.forEach(it -> projection.append(it.toString(), 1));
return projection;
}
/**
* Compute the filer {@link Document} from the given {@link Predicate}.
*
* @param predicate can be {@literal null}.
* @return an empty {@link Document} if predicate is {@literal null}.
* @see MongodbDocumentSerializer#toQuery(Predicate)
*/
protected Document createQuery(@Nullable Predicate predicate) {
if (predicate == null) {
return new Document();
}
return serializer.toQuery(predicate);
}
/**
* Compute the sort {@link Document} from the given list of {@link OrderSpecifier order specifiers}.
*
* @param orderSpecifiers can be {@literal null}.
* @return an empty {@link Document} if predicate is {@literal null}.
* @see MongodbDocumentSerializer#toSort(List)
*/
protected Document createSort(List<OrderSpecifier<?>> orderSpecifiers) {
return serializer.toSort(orderSpecifiers);
}
/**
* Get the actual {@link QueryMixin} delegate.
*
* @return
*/
QueryMixin<Q> getQueryMixin() {
return queryMixin;
}
/**
* Get the where definition as a Document instance
*
* @return
*/
Document asDocument() {
return createQuery(queryMixin.getMetadata().getWhere());
}
@Override
public String toString() {
return asDocument().toString();
}
}

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