Compare commits

...

170 Commits
3.2.3 ... 3.3.0

Author SHA1 Message Date
Jens Schauder
b28da2eed3 Release version 3.3 GA (2021.1.0).
See #3865
2021-11-12 10:49:36 +01:00
Jens Schauder
02de914993 Prepare 3.3 GA (2021.1.0).
See #3865
2021-11-12 10:49:15 +01:00
Christoph Strobl
5b498f809e Upgrade to MongoDB 4.4.0 Drivers.
Closes: #3875
2021-11-12 07:23:57 +01:00
Mark Paluch
f94a7ee742 After release cleanups.
See #3829
2021-10-18 13:55:41 +02:00
Mark Paluch
ab0ffab488 Prepare next development iteration.
See #3829
2021-10-18 13:55:38 +02:00
Mark Paluch
85b47d66f1 Release version 3.3 RC1 (2021.1.0).
See #3829
2021-10-18 13:48:13 +02:00
Mark Paluch
0d8fe46f3b Prepare 3.3 RC1 (2021.1.0).
See #3829
2021-10-18 13:47:46 +02:00
Christoph Strobl
0bc78f99dd Upgrade to MongoDB 4.4.0-beta1 Drivers.
Closes: #3860
2021-10-14 16:21:32 +02:00
Mark Paluch
e4f450f667 Upgrade to Maven Wrapper 3.8.3.
See #3859
2021-10-11 14:30:24 +02:00
Mark Paluch
49cd44295c Add support for fluent Querydsl and Query by Example query definition.
We now support the functional fluent query definition API for imperative and reactive usage with Querydsl and Query by Example.

Page<PersonProjection> first = repository.findBy(Example.of(probe),
		it -> it.as(PersonProjection.class).project("firstname").page(PageRequest.of(0, 1, Sort.by("firstname"))));

Closes #3757
Original pull request: #3788.
2021-10-08 11:37:26 +02:00
Mark Paluch
767d97a831 Polishing.
Remove code duplications. Reuse target type computation for enum types. Refine method names.

See #3766
Closes #3837
2021-10-07 15:57:08 +02:00
Christoph Strobl
bbe8410979 Render items for collection-like properties when deriving $jsonSchema.
Closes #3766
Original pull request: #3837.
2021-10-07 15:57:08 +02:00
Mark Paluch
c0a4bdb548 Polishing.
Reformat code.

See #3853
Original pull request: #3856.
2021-10-07 15:23:44 +02:00
Christoph Strobl
673a81af0e Fix query/update document reference computation for non-id properties.
We now consider using the target keyword when computing document references. This fixes an issue where query/update statements had not been rendered correctly for references like { 'name' : ?#{#target} }.

Closes #3853
Original pull request: #3856.
2021-10-07 15:23:29 +02:00
Christoph Strobl
977032620e Upgrade to MongoDB 4.3.3 Drivers.
Closes: #3855
2021-10-06 10:51:12 +02:00
Mark Paluch
c7263e5b11 Polishing.
Reformat code.

See #3847
Original pull request: #3848.
2021-10-05 10:08:33 +02:00
Christoph Strobl
eed9b2470a Fix id conversion when storing document reference.
This commit makes sure that the string to ObjectId conversion when storing document references follows the general conversion rules.

Closes #3847
Original pull request: #3848.
2021-10-05 10:08:19 +02:00
Mark Paluch
c350be1f52 Polishing.
Reformat code.

See #3842
Original pull request: #3844.
2021-10-05 09:42:25 +02:00
Christoph Strobl
65b058ffd9 Fix SpEL evaluation in document reference lookup.
Closes #3842
Original pull request: #3844.
2021-10-05 09:42:19 +02:00
Christoph Strobl
4a5789d67e DocumentReference should consider Reference annotations.
Closes #3851
Original pull request: #3852.
2021-10-04 14:30:40 +02:00
Christoph Strobl
7b05cfad94 Update readme.
Shorten MongoDB setup section and add missing anchors.

Original Pull Request: #3833
2021-10-04 10:43:58 +02:00
John Blum
a16a9fe1fe Edit README to include complete steps for building from source.
Closes: #3833
2021-10-04 10:43:04 +02:00
Christoph Strobl
2f98a6656b Fix javadoc errors and warnings
Closes: #3835
2021-09-27 11:13:08 +02:00
Christoph Strobl
9e2f6055a3 Refine CI job triggers.
See #3696
Original pull request: #3753.
2021-09-21 15:20:36 +02:00
Mark Paluch
7f58538292 Use HTTPS in Dockerfiles for package download.
See #3696
Original pull request: #3753.
2021-09-21 15:20:31 +02:00
Christoph Strobl
2f208d712c Update CI to cover MongoDB Server 5.0.
MongoDB has alpha releases in a slightly different location on their distribution server. And they use different keys for signing these alpha releases compared to the overall package listing.

Closes #3696
Original pull request: #3753.
2021-09-21 15:20:26 +02:00
Christoph Strobl
63d9875576 Update test for MongoDB Server 5.0.
Update assertions for changed return types, add a bit of think time and disable tests for no longer supported features.

See #3696
Original pull request: #3753.
2021-09-21 15:20:01 +02:00
Mark Paluch
b7ffff4769 After release cleanups.
See #3771
2021-09-17 09:52:21 +02:00
Mark Paluch
715ae26f3c Prepare next development iteration.
See #3771
2021-09-17 09:52:18 +02:00
Mark Paluch
00350edd32 Release version 3.3 M3 (2021.1.0).
See #3771
2021-09-17 09:44:56 +02:00
Mark Paluch
38e1d0d92d Prepare 3.3 M3 (2021.1.0).
See #3771
2021-09-17 09:44:34 +02:00
Christoph Strobl
8f00ffd291 Change visibility of PersistentEntitiesFactoryBean.
Closes: #3825
2021-09-15 15:30:30 +02:00
Mark Paluch
0af8d6839e Polishing.
Reformat code, fix ticket references in tests.

See #3820
Original pull request: #3821.
2021-09-14 09:12:21 +02:00
Christoph Strobl
9b02897db5 Add configuration support for MongoDB ServerApiVersion.
Introduce FactoryBean and required options to set the ServerAPI.
Update namespace xsd and parsing.

Closes: #3820
Original pull request: #3821.
2021-09-14 09:12:09 +02:00
Christoph Strobl
99203b397a Add support for deriving json schema for encrypted properties.
This commit introduces support for creating a MongoJsonSchema containing encrypted fields for a given type based on mapping metadata.
Using the Encrypted annotation allows to derive required encryptMetadata and encrypt properties within a given (mapping)context.

@Document
@Encrypted(keyId = "...")
static class Patient {

    // ...

    @Encrypted(algorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic")
    private Integer ssn;

}

MongoJsonSchemaCreator schemaCreator = MongoJsonSchemaCreator.create(mappingContext);
MongoJsonSchema patientSchema = schemaCreator
    .filter(MongoJsonSchemaCreator.encryptedOnly())
    .createSchemaFor(Patient.class);

Closes: #3800
Original pull request: #3801.
2021-09-14 08:56:53 +02:00
Christoph Strobl
eda1c79315 Move and add tests to UpdateMapper.
Also update author information.

Original Pull Request: #3815
2021-09-13 14:26:46 +02:00
divyajnu08
e7150f525e Fix update mapping using nested integer keys on map structures.
Closes: #3775
Original Pull Request: #3815
2021-09-13 14:26:12 +02:00
Mark Paluch
7d6b5ae5fe Upgrade to Maven Wrapper 3.8.2.
See #3818
2021-09-10 15:37:59 +02:00
Christoph Strobl
d70e459ffe Upgrade to MongoDB Java Drivers 4.3.2
Closes: #3816
2021-09-10 10:48:22 +02:00
Mark Paluch
a26e780957 Reduce allocations in query and update mapping.
Introduce EmptyDocument and utility methods in BsonUtils. Avoid entrySet and iterator creation for document iterations/inspections.

Relates to: #3760
Original Pull Request: #3809
2021-09-09 08:00:07 +02:00
Mark Paluch
8fb0e1326b Introduce SessionSynchronization.NEVER to disable transactional participation.
SessionSynchronization.NEVER bypasses all transactional integration in cases where applications do not want to make use of transactions so that transaction inspection overhead is avoided.

Closes: #3760
Original Pull Request: #3809
2021-09-09 07:58:34 +02:00
Christoph Strobl
9014f770d8 Fix slice argument in query fields projection.
We now use a Collection instead of an Array to pass on $slice projection values for offset and limit.

Closes: #3811
Original pull request: #3812.
2021-09-08 14:46:31 +02:00
Christoph Strobl
f128e6df15 Fix @DocumentReference resolution for properties used in constructor.
This commit fixes an issue that prevented referenced entities from being used as constructor arguments.

Closes: #3806
Original pull request: #3810.
2021-09-08 14:35:43 +02:00
Mark Paluch
270456ed81 Polishing.
Extract query that yields no hits into constant. Guard Map-typed reference properties against empty $or.

See #3805
Original pull request: #3807.
2021-09-08 14:18:35 +02:00
Christoph Strobl
4e960a9682 Fix document reference on empty reference arrays.
This commit fixes an issue caused by empty reference arrays.

Closes #3805
Original pull request: #3807.
2021-09-08 14:18:30 +02:00
Mark Paluch
061c28f84a Polishing.
Add ticket reference to tests.

See #3803
2021-09-08 13:59:11 +02:00
Oliver Drotbohm
cba7eaba4c Polishing.
Formatting and indentation in parent project's pom.xml.

See #3803
2021-09-08 13:59:06 +02:00
Oliver Drotbohm
ada7e199a4 Properly detect all supported identifier annotations as explicitly annotated.
We now simply delegate to AnnotationBasedPersistentProperty.isIdProperty() for the detection of annotated identifiers. The previous, manual identifier check was preventing additional identifier annotations, supported by ABP, to be considered, too.

Fixes #3803.
2021-09-08 13:56:49 +02:00
Mark Paluch
977e5e4c5c Polishing.
Tweak reference documentation wording. Extract self/target source dereferencing into utility methods.

See: #3798
Original pull request: #3802.
2021-09-08 13:53:03 +02:00
Christoph Strobl
c8307d5a39 Allow one-to-many style lookups with via @DocumentReference.
This commit adds support for relational style One-To-Many references using a combination of ReadonlyProperty and @DocumentReference.
It allows to link types without explicitly storing the linking values within the document itself.

@Document
class Publisher {

  @Id
  ObjectId id;
  // ...

  @ReadOnlyProperty
  @DocumentReference(lookup="{'publisherId':?#{#self._id} }")
  List<Book> books;
}

Closes: #3798
Original pull request: #3802.
2021-09-08 13:52:46 +02:00
Mark Paluch
dcf184888e Polishing.
Add since and author tags. Update reference docs. Fix format of ticket references in tests.

See #3708
Original pull request: #3796.
2021-09-07 09:56:47 +02:00
divyajnu08
59d0042d13 Add support for $asin and $asinh aggregation operators.
Closes #3708
Original pull request: #3796.
2021-09-07 09:56:30 +02:00
Mark Paluch
8af904b81f Polishing.
Add author and since tags. Tweak Javadoc format.

See #3709
Original pull request: #3794.
2021-09-06 15:50:58 +02:00
divya srivastava
ffceed8da9 Add support for $atan, $atan2 and $atanh aggregation operators.
Closes #3709
Original pull request: #3794.
2021-09-06 15:50:44 +02:00
Mark Paluch
34d66a276a Polishing.
Add license headers. Update Javadoc, author, and since tags. Add tests. Add toCriteriaDefinition method.

See #3790
2021-09-06 15:08:18 +02:00
divyajnu08
e71ec874ab Add support for $expr operator.
Also, allow construction of $match with an AggregationExpression.

Closes #3790
2021-09-06 15:07:06 +02:00
Mark Paluch
f24e8e5361 Avoid nested Document conversion to primitive types for fields with an explicit write target.
We now no longer attempt to convert query Documents into primitive types to avoid e.g. Document to String conversion.

Closes: #3783
Original Pull Request: #3797
2021-09-02 09:50:37 +02:00
Christoph Strobl
bf86f39b2d Fix id field target type conversion for document references.
This commit fixes an issue where a defined custom target type conversion for the id field was not properly considered when writing a document reference.  Previously an eg. String was not being converted into an ObjectId correctly causing lookup queries to return empty results.
Converting the id property value on write solves the issue.
Includes a minor polish in the mapping centralizing pointer creation within the DocumentPointerFactory.

Closes: #3782
Original pull request: #3785.
2021-08-27 09:40:11 +02:00
Mark Paluch
f662d7ca0d Polishing.
Tweak Javadoc. Add since tag, reformat code. Simplify tests. Move documentation bits into the right place.

See #3726.
Original pull request: #3765.
2021-08-27 09:35:33 +02:00
James McNee
62eb719b1e Add support for $sampleRate criteria.
Closes #3726
Original pull request: #3765.
2021-08-27 09:35:12 +02:00
Mark Paluch
69b582823a Polishing.
Add support for Pattern. Extract Regex flags translation from Criteria into RegexFlags utility class. Add since and author tags. Simplify tests. Update reference documentation.

See #3725.
Original pull request: #3781.
2021-08-27 08:47:20 +02:00
divya srivastava
297ef98239 Add support for $regexFind, $regexFindAll, and $regexMatch aggregation operators.
Closes #3725
Original pull request: #3781.
2021-08-26 12:21:55 +02:00
Mark Paluch
f71f107445 Polishing.
Reorder methods. Add since tag. Simplify assertions. Use diamond syntax.

See: #3776
Original pull request: #3777.
2021-08-25 14:58:00 +02:00
Ivan Volzhev
36e2d80d71 Relax requirement for GeoJsonMultiPoint construction allowing creation using a single point.
Only 1 point is required per GeoJson RFC and Mongo works just fine with 1 point as well.

Closes #3776
Original pull request: #3777.
2021-08-25 14:57:08 +02:00
Mark Paluch
467536cb34 Polishing.
Update since version. Reformat code.

See: #3761.
2021-08-25 14:33:36 +02:00
sangyongchoi
302c8031f9 Add Criteria infix functions for maxDistance and minDistance.
Closes: #3761
2021-08-25 14:33:10 +02:00
Mark Paluch
7c6e951c7c Polishing.
Add author tags, tweak Javadoc style. Simplify tests. Document operator.

See #3724
Original pull request: #3759.
2021-08-25 11:13:02 +02:00
Mushtaq Ahmed
92cc2a582a Add support for $rand aggregation operator.
Closes #3724
Original pull request: #3759
2021-08-25 11:08:16 +02:00
Mark Paluch
24171b3ae2 Polishing.
Introduce factory methods to convert TimeZone/ZoneId/ZoneOffset into Mongo Timezone. Introduce TemporalUnit abstraction and converters to convert ChronoUnit and TimeUnit into TemporalUnit for date operators accepting a unit parameter.

See #3713
Original pull request: #3748.
2021-08-25 11:01:34 +02:00
Christoph Strobl
456c1ad26a Add shortcut for date aggregation operators working with timezone.
See: #3713
Original pull request: #3748.
2021-08-25 11:01:27 +02:00
Christoph Strobl
fc41793d5d Add support for $dateDiff aggregation operator.
Closes: #3713
Original pull request: #3748.
2021-08-25 11:01:17 +02:00
Christoph Strobl
afef243634 Add support for $dateAdd aggregation operator.
Closes: #3713
Original pull request: #3748.
2021-08-25 11:01:00 +02:00
Mark Paluch
869b88702d Polishing.
Fix typo in reference docs.

See #3758
2021-08-25 10:15:13 +02:00
Ryan Gibb
aca403c112 Fix a typo in MongoConverter javadoc.
Original pull request: #3758.
2021-08-25 10:14:20 +02:00
Mark Paluch
df0372eee1 Polishing.
Rename AngularDimension to AngularUnit. Tweak Javadoc. Simplify tests. Update reference docs.

See: #3710, #3714, #3728, #3730
Original pull request: #3755.
2021-08-24 16:18:17 +02:00
Christoph Strobl
c4c6267d91 Add support for $cos and $cosh aggregation operators.
Closes: #3710
Original pull request: #3755.
2021-08-24 16:18:17 +02:00
Christoph Strobl
73d5886aae Add support for $tan and $tanh aggregation operators.
Closes: #3730
Original pull request: #3755.
2021-08-24 16:18:17 +02:00
Christoph Strobl
0db47169cf Add support for $sin and $sinh aggregation operators.
Closes: #3728
Original pull request: #3755.
2021-08-24 16:18:14 +02:00
Christoph Strobl
ec16b873b7 Add support for $degreesToRadians aggregation operator.
Closes: #3714
Original pull request: #3755.
2021-08-24 16:16:01 +02:00
Mark Paluch
2a3a4cf030 Polishing.
Fix method order from earlier merges. Add missing Javadoc. Simplify tests. Update documentation.

See #3721
Original pull request: #3746.
2021-08-24 15:01:54 +02:00
Christoph Strobl
df2b2a2f68 Add support for $integral aggregation operator.
Closes: #3721
Original pull request: #3746.
2021-08-24 15:01:49 +02:00
Mark Paluch
fd0a402c99 Polishing.
See #3720
Original pull request: #3745.
2021-08-24 14:34:43 +02:00
Christoph Strobl
6bd0f758fe Extend support for $ifNull to cover multiple conditions.
Closes: #3720
Original pull request: #3745.
2021-08-24 14:34:43 +02:00
Mark Paluch
10c0203605 Polishing.
Accept window units in addition to plain strings. Document operator.

See: #3716
Original pull request: #3742.
2021-08-24 14:30:52 +02:00
Christoph Strobl
82b33331fc Add support for $derivative aggregation operator.
Closes: #3716
Original pull request: #3742.
2021-08-24 14:30:52 +02:00
Mark Paluch
75b5a548b6 Polishing.
Fix asterisk callouts.

See #3786
2021-08-24 11:23:42 +02:00
Mark Paluch
0c481feb72 Extract Aggregation Framework and GridFS docs in own source files.
Closes #3786
2021-08-24 11:09:06 +02:00
Mark Paluch
c8a791d367 Polishing.
Make fields final where possible. Update javadoc. Simplify assertions. Update reference docs.

See: #3715, See #3717, See #3727
Original pull request: #3741.
2021-08-24 11:07:40 +02:00
Christoph Strobl
510028a834 Add support for $shift aggregation Operator.
Closes: #3727
Original pull request: #3741.
2021-08-24 11:07:31 +02:00
Christoph Strobl
1a86761e2e Add support for $documentNumber aggregation operator.
Closes: #3717
Original pull request: #3741.
2021-08-24 11:07:22 +02:00
Christoph Strobl
30da62181f Add support for $rank and $denseRank aggregation operators.
Closes: #3715
Original pull request: #3741.
2021-08-24 11:06:59 +02:00
Christoph Strobl
a977b8a790 Change visibility of Reactive/MongoRepositoryFactoryBean setters.
Setters of the FactoryBean should be public.

Closes: #3779
Original pull request: #3780.
2021-08-24 10:26:44 +02:00
Christoph Strobl
f3e067f59f Add support for $expMovingAvg aggregation operator.
The SpEL support for this one is missing due to the differing argument map (N, alpha).

Closes: #3718
Original pull request: #3744.
2021-08-23 14:44:14 +02:00
Christoph Strobl
dbfd4e5c62 Polishing.
Reformat code.

See #3712
Original pull request: #3740.
2021-08-23 14:29:16 +02:00
Christoph Strobl
c574e5cf8a Add support for $covariancePop and $covarianceSamp aggregation expressions.
Closes: #3712
Original pull request: #3740.
2021-08-23 14:25:41 +02:00
Mark Paluch
f9f4c4621b Polishing.
Update javadoc and add assertions.

See #3711
Original pull request: #3739.
2021-08-23 11:21:35 +02:00
Christoph Strobl
23254c10dc Add support for $setWindowFields aggregation stage.
Add a SetWindowFieldsOperation to the aggregation framework.

The builder API allows fluent declaration of the aggregation stage as shown in the sample below.

SetWindowFieldsOperation.builder()
	.partitionByField("state")
	.sortBy(Sort.by(Direction.ASC, "date"))
	.output(AccumulatorOperators.valueOf("qty").sum())
	.within(Windows.documents().fromUnbounded().toCurrent().build())
	.as("cumulativeQuantityForState")
	.build();

Closes #3711
Original pull request: #3739.
2021-08-23 11:21:35 +02:00
Christoph Strobl
255491c446 Upgrade to MongoDB 4.3.1 Drivers.
Closes: #3778
2021-08-23 10:49:22 +02:00
Christoph Strobl
1d943d62a3 Fix build on Java 16.
Make sure to use an initialized MappingContext.

Closes: #3749
Original pull request: #3752.
2021-08-23 10:29:35 +02:00
Jens Schauder
7538b1a1a5 After release cleanups.
See #3736
2021-08-12 15:16:23 +02:00
Jens Schauder
828c074167 Prepare next development iteration.
See #3736
2021-08-12 15:16:21 +02:00
Jens Schauder
87ab1ac48c Release version 3.3 M2 (2021.1.0).
See #3736
2021-08-12 15:03:17 +02:00
Jens Schauder
454afd9877 Prepare 3.3 M2 (2021.1.0).
See #3736
2021-08-12 15:02:56 +02:00
Mark Paluch
45971b212c Polishing.
Move off deprecated classes. Add unpaged testcase for query by example.

Original Pull Request: #3754
2021-07-26 15:16:05 +02:00
Mark Paluch
68370c16fb Run unpaged query using Pageable.unpaged() through QuerydslMongoPredicateExecutor.findAll(…).
We now correctly consider unpaged queries if the Pageable is unpaged.

Closes: #3751
Original Pull Request: #3754
2021-07-26 15:15:37 +02:00
Christoph Strobl
d2c9b47366 Fix issues related to Querydsl 5.0 upgrade.
Remove overridden methods no longer available in public api.

Closes: #3738
2021-07-22 09:03:10 +02:00
Jens Schauder
4d7ee0e741 After release cleanups.
See #3631
2021-07-16 14:19:58 +02:00
Jens Schauder
e7f3a2436d Prepare next development iteration.
See #3631
2021-07-16 14:19:56 +02:00
Jens Schauder
4ef1ff6aff Release version 3.3 M1 (2021.1.0).
See #3631
2021-07-16 14:09:25 +02:00
Jens Schauder
b6ad32d7d4 Prepare 3.3 M1 (2021.1.0).
See #3631
2021-07-16 14:08:59 +02:00
Jens Schauder
e875f9ea33 Updated changelog.
See #3631
2021-07-16 14:08:43 +02:00
Jens Schauder
9db9d16cf8 Updated changelog.
See #3681
2021-07-16 10:48:16 +02:00
Mark Paluch
f00991dc29 Polishing.
Rename Granularities/Granularity to Granularity and GranularityDefinition to proivide a more natural wording towards using predefined granularities.

Validate presence of referenced properties through the TimeSeries annotation.

Tweak Javadoc, reformat code, add unit tests.

See #3731
Original pull request: #3732.
2021-07-16 09:44:43 +02:00
Christoph Strobl
bacbd7133e Add support for creating Time Series collection.
Introduce time series to CollectionOptions and add dedicated TimeSeries annotation to derive values from.

Closes #3731
Original pull request: #3732.
2021-07-16 09:44:35 +02:00
Mark Paluch
f38f6d67ab Polishing.
Support DBObject and Map that as source for entity materialization and map conversion.

See #3702
Original pull request: #3704.
2021-07-15 10:00:33 +02:00
Christoph Strobl
3f27e8e152 Fix raw document conversion in Collection like properties.
Along the lines make sure to convert map like structures correctly if they do not come as a Document, eg. cause they got converted to a plain Map in a post load, pre convert event.

Closes #3702
Original pull request: #3704.
2021-07-15 10:00:31 +02:00
Christoph Strobl
23177fef0c Custom Converter should also be applicable for simple types.
This commit fixes a regression that prevented custom converters from being applied to types considered store native ones.

Original pull request: #3703.
Fixes #3670
2021-07-15 08:54:31 +02:00
Mark Paluch
f3b90c2b8a Polishing.
Reformat code. Tweak javadoc. Reject wildcard projection usage on properties with a MappingException. Omit wildcard projections when declared on document types that are used as subdocument.

See #3225
Original pull request: #3671.
2021-07-14 15:16:00 +02:00
Christoph Strobl
d57c5a9529 Add support for Wildcard Index.
Add WildcardIndexed annotation and the programatic WildcardIndex.

Closes #3225
Original pull request: #3671.
2021-07-14 15:16:00 +02:00
Mark Paluch
986ea39f90 Upgrade to Querydsl 5.0.
Move off our own Querydsl copies, as Querydsl 5.0 ships MongoDB Document API support.
Remove package-private duplicates of Querydsl code.
Introduce SpringDataMongodbQuerySupport to provide a well-formatted toString representation of the actual query.

Original Pull Request: #3674
2021-07-14 10:15:12 +02:00
Christoph Strobl
5bd7ff1413 Upgrade to MongoDB 4.3.0 Drivers.
Closes: #3706
2021-07-14 07:44:00 +02:00
Mark Paluch
93b9f23b07 Polishing.
Fix proxy comparison.

See #3705
2021-07-13 08:42:24 +02:00
Mark Paluch
42ab7d2f63 Adapt to changes in AssertJ 3.20.
Closes #3705
2021-07-13 08:27:51 +02:00
Christoph Strobl
a6a2f0bde9 Upgrade to MongoDB 4.3.0-beta4 Drivers.
Closes: #3693
2021-07-12 14:56:44 +02:00
Oliver Drotbohm
7d0b070d1f Adapt to API consolidation in Spring Data Commons' PersistentProperty.
Closes: #3700
Original Pull Request: #3701
Related to: spring-projects/spring-data-commons#2408
2021-07-12 08:09:26 +02:00
Christoph Strobl
81bc3c599b Disable tests on Java 16 that require class-based proxies.
Original pull request: #3687.
Resolves #3656
2021-07-08 12:08:54 +02:00
Christoph Strobl
403f0019d5 Fix Optional handling in query creation and result processing.
Original pull request: #3687.
Resolves #3656
2021-07-08 12:08:50 +02:00
Christoph Strobl
4f65bb0810 Fix mapping context setup to include simple type holder.
Original pull request: #3687.
Resolves #3656
2021-07-08 12:08:32 +02:00
Christoph Strobl
ef29e69a87 Polishing.
Simplify KeyMapper current property/index setup.

Original Pull Request: #3689
2021-07-06 11:46:35 +02:00
David Julia
5cffb3c07c Fix Regression in generating queries with nested maps with numeric keys.
While maps that have numeric keys work if there is only one map with an integer key, when there are multiple maps with numeric keys in a given query, it fails.

Take the following example for a map called outer with numeric keys holding reference to another object with a map called inner with numeric keys: Updates that are meant to generate {"$set": {"outerMap.1234.inner.5678": "hello"}} are instead generating {"$set": {"outerMap.1234.inner.inner": "hello"}}, repeating the later map property name instead of using the integer key value.

This commit adds unit tests both for the UpdateMapper and QueryMapper, which check multiple consecutive maps with numeric keys, and adds a fix in the KeyMapper. Because we cannot easily change the path parsing to somehow parse path parts corresponding to map keys differently, we address the issue in the KeyMapper. We keep track of the partial path corresponding to the current property and use it to skip adding the duplicated property name for the map to the query, and instead add the key.

This is a bit redundant in that we now have both an iterator and an index-based way of accessing the path parts, but it gets the tests passing and fixes the issue without making a large change to the current approach.

Fixes: #3688
Original Pull Request: #3689
2021-07-06 11:36:11 +02:00
Christoph Strobl
61d3a0bd1f Fix NPE when reading/mapping null value inside collection.
Closes: #3686
2021-07-01 10:50:57 +02:00
Christoph Strobl
82d67c1dbb Favor ObjectUtils over Objects for equals/hashCode.
Original Pull Request: #3684
2021-06-24 13:32:48 +02:00
Gatto
85a30ec915 Add equals and hashCode to UnwrappedMongoPersistentProperty.
Fixes #3683
Original Pull Request: #3684
2021-06-24 13:32:40 +02:00
Mark Paluch
c70c29b2c7 Updated changelog.
See #3650
2021-06-22 16:07:25 +02:00
Mark Paluch
2a5ae0da37 Updated changelog.
See #3649
2021-06-22 15:29:50 +02:00
Mark Paluch
826015e9c1 Update reference docs to use correct MongoClient.
Closes #3666
2021-06-22 14:37:05 +02:00
larsw
9dda0a2f93 Add closing quote to GeoJson javadoc.
Closes #3677
2021-06-21 13:58:07 +02:00
Christoph Strobl
7dfe460433 Fix field projection value conversion.
The field projection conversion should actually only map field names and avoid value conversion. In the MongoId case an inclusion parameter (1) was unintentionally converted into its String representation which causes trouble on Mongo 4.4 servers.

Fixes: #3668
Original pull request: #3678.
2021-06-21 13:45:46 +02:00
Christoph Strobl
73a0f04933 Polishing.
Fix typo in class name and make sure MongoTestTemplate uses the configured simple types.
Remove superfluous junit extension.

See: #3659
Original pull request: #3661.
2021-06-18 14:12:48 +02:00
Christoph Strobl
a1c165921d Fix query mapper path resolution for types considered simple ones.
spring-projects/spring-data-commons#2293 changed how PersistentProperty paths get resolved and considers potentially registered converters for those, which made the path resolution fail in during the query mapping process.
This commit makes sure to capture the according exception and continue with the given user input.

Fixes: #3659
Original pull request: #3661.
2021-06-18 14:12:39 +02:00
Christoph Strobl
3872b379cd Fix $or / $nor keyword mapping in query mapper.
This commit fixes an issue with the pattern used for detecting $or / $nor which also matched other keywords like $floor.

Closes: #3635
Original pull request: #3637.
2021-06-18 13:48:29 +02:00
Mark Paluch
98fe043b95 Directly import JSR305 jar.
Closes #3672
2021-06-17 09:41:46 +02:00
Mark Paluch
c217618d9d Polishing.
Reorder methods and types. Rename MongoPersistentProperty.isOmitNullProperty to writeNullValues. Adapt caching MongoPersistentProperty and add tests.

Tweak Javadoc wording, add author and since tags.

See #3407
Original pull request: #3646.
2021-06-14 11:53:24 +02:00
Divya Srivastava
b1020d19ba Add an option to @Field annotation to include/exclude null values on write.
Properties can be annotated with `@Field(write=…)` to control whether a property with a null value should be included or omitted (default) during conversion in the target Document.

Closes #3407
Original pull request: #3646.
2021-06-14 11:53:09 +02:00
Mark Paluch
a481636429 Polishing.
Add nullability annotation. Return early on null value conversion.

See #3633
Original pull request: #3643.
2021-06-09 14:13:42 +02:00
Christoph Strobl
efa9a2d408 Add Criteria.isNullValue() as alternative to Criteria.is(null).
See #3633
Original pull request: #3643.
2021-06-09 14:13:42 +02:00
Christoph Strobl
149a703ecc Fix NPE in QueryMapper when trying to apply target type on null value.
Closes #3633
Original pull request: #3643.
2021-06-09 14:13:42 +02:00
Mark Paluch
2b715c54d3 Polishing.
Reformat code.

See #3660.
Original pull request: #3662.
2021-06-09 11:33:12 +02:00
Christoph Strobl
ece261aadb Fix conversion for types having a converter registered.
Fixes: #3660
Original pull request: #3662.
2021-06-09 11:33:00 +02:00
Mark Paluch
dae0ac3b4d Remove duplicate LazyLoadingInterceptor code by reusing LazyLoadingProxyFactory.
Original pull request: #3647.
Closes #3602.
2021-05-21 14:20:01 +02:00
Mark Paluch
5ab75eb65a Polishing.
Reduce dependencies in tests by using NoOpDbRefResolver.
Add since tags.

Tweak documentation. Extract entity references into own documentation fragment.

Original pull request: #3647.
Closes #3602.
2021-05-21 14:20:01 +02:00
Christoph Strobl
e96ef8e18f Avoid capturing lambdas, update javadoc and add tests.
Also allow direct usage of (at)Reference from data commons to define associations.

Original pull request: #3647.
Closes #3602.
2021-05-21 14:20:00 +02:00
Mark Paluch
82af678cab Polishing
Rename ReferenceReader to ReferenceLookupDelegate.
Rename LazyLoadingProxyGenerator to LazyLoadingProxyFactory.
Rename DefaultReferenceLoader to MongoDatabaseFactoryReferenceLoader.

Reduce scope of LookupFunction and move it to ReferenceLookupDelegate.

Extract some checks into methods to reflect the underlying concepts. Simplify code, convert variables to constants where possible.

Original pull request: #3647.
Closes #3602.
2021-05-21 14:20:00 +02:00
Christoph Strobl
6ed274bd9b Update entity linking support to derive document pointer from lookup query.
Simplify usage by computing the pointer from the lookup.
Update the reference documentation, add JavaDoc and refine API.

Original pull request: #3647.
Closes #3602.
2021-05-21 14:20:00 +02:00
Mark Paluch
48ac7e75ba First pass of review polishing.
Original pull request: #3647.
Closes #3602.
2021-05-21 14:20:00 +02:00
Christoph Strobl
a51c96298f Enhance support for linking entities.
Add initial support for an alternative to the existing DBRef scenario.
The enhancement allows to store and retrieve linked entites via their id or a customizable lookup query.

Original pull request: #3647.
Closes #3602.
2021-05-21 14:20:00 +02:00
Mark Paluch
f1354c4508 Revise DocumentCallback nullability constraints.
DocumentCallback is now generally non-nullable for both, the input Document and the returned result expecting EntityReader to always return a non-null object.

Also, use try-with-resources where applicable.

Closes #3648
2021-05-18 15:11:06 +02:00
Mark Paluch
ff7588f648 Updated changelog.
See #3629
2021-05-14 12:36:34 +02:00
Mark Paluch
124036fe36 Updated changelog.
See #3628
2021-05-14 12:06:37 +02:00
Greg L. Turnquist
80c5b536df Polishing. 2021-05-13 16:15:04 -05:00
Greg L. Turnquist
2ee33b1444 Polishing. 2021-05-13 15:56:39 -05:00
Greg L. Turnquist
eec6cea507 Update CI to JDK 16.
See #3603.
2021-05-13 15:52:30 -05:00
Mark Paluch
90d03d92d8 Polishing.
Let appendLimitAndOffsetIfPresent accept unary operators for adjusting limit/offset values instead of appendModifiedLimitAndOffsetIfPresent. Apply simple type extraction for Slice. Add support for aggregation result streaming.

Extend tests, add author tags, update docs.

See #3543.
Original pull request: #3645.
2021-05-11 11:50:40 +02:00
divya_jnu08
9a48e32565 Aggregation query method should be able to return Slice and Stream.
Aggregation query methods can not return Slice and Stream.

interface PersonRepository extends CrudReppsitory<Person, String> {

  @Aggregation("{ $group: { _id : $lastname, names : { $addToSet : ?0 } } }")
  Slice<PersonAggregate> groupByLastnameAnd(String property, Pageable page);

  @Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $firstname } } }")
  Stream<PersonAggregate> groupByLastnameAndFirstnamesAsStream();
}

Closes #3543.
Original pull request: #3645.
2021-05-11 11:50:10 +02:00
Mark Paluch
ede6927b65 Introduce template method for easier customization of fragments.
Closes #3638.
2021-04-27 10:45:43 +02:00
Greg L. Turnquist
2edc29f758 Authenticate with artifactory.
See #3616.
2021-04-22 14:59:20 -05:00
Clément Petit
5bd9bcca75 Fix bullet points in aggregation framework reference documentation.
Closes: #3632
2021-04-20 08:19:46 +02:00
Greg L. Turnquist
54f75e653b Migrate to main branch.
See #3616.
2021-04-16 12:27:26 -05:00
Mark Paluch
7b33f56e33 After release cleanups.
See #3616
2021-04-14 14:30:14 +02:00
Mark Paluch
829eed7d6c Prepare next development iteration.
See #3616
2021-04-14 14:30:11 +02:00
282 changed files with 20391 additions and 4017 deletions

View File

@@ -1 +1,2 @@
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip
#Mon Oct 11 14:30:24 CEST 2021
distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.3/apache-maven-3.8.3-bin.zip

View File

@@ -1,6 +1,6 @@
= Continuous Integration
image:https://jenkins.spring.io/buildStatus/icon?job=spring-data-mongodb%2Fmaster&subject=Moore%20(master)[link=https://jenkins.spring.io/view/SpringData/job/spring-data-mongodb/]
image:https://jenkins.spring.io/buildStatus/icon?job=spring-data-mongodb%2Fmain&subject=Moore%20(main)[link=https://jenkins.spring.io/view/SpringData/job/spring-data-mongodb/]
image:https://jenkins.spring.io/buildStatus/icon?job=spring-data-mongodb%2F2.1.x&subject=Lovelace%20(2.1.x)[link=https://jenkins.spring.io/view/SpringData/job/spring-data-mongodb/]
image:https://jenkins.spring.io/buildStatus/icon?job=spring-data-mongodb%2F1.10.x&subject=Ingalls%20(1.10.x)[link=https://jenkins.spring.io/view/SpringData/job/spring-data-mongodb/]

View File

@@ -1,3 +1,3 @@
= Spring Data contribution guidelines
You find the contribution guidelines for Spring Data projects https://github.com/spring-projects/spring-data-build/blob/master/CONTRIBUTING.adoc[here].
You find the contribution guidelines for Spring Data projects https://github.com/spring-projects/spring-data-build/blob/main/CONTRIBUTING.adoc[here].

112
Jenkinsfile vendored
View File

@@ -3,7 +3,7 @@ pipeline {
triggers {
pollSCM 'H/10 * * * *'
upstream(upstreamProjects: "spring-data-commons/master", threshold: hudson.model.Result.SUCCESS)
upstream(upstreamProjects: "spring-data-commons/main", threshold: hudson.model.Result.SUCCESS)
}
options {
@@ -14,6 +14,22 @@ pipeline {
stages {
stage("Docker images") {
parallel {
stage('Publish JDK 8 + MongoDB 5.0') {
when {
changeset "ci/openjdk8-mongodb-5.0/**"
}
agent { label 'data' }
options { timeout(time: 30, unit: 'MINUTES') }
steps {
script {
def image = docker.build("springci/spring-data-openjdk8-with-mongodb-5.0.0", "ci/openjdk8-mongodb-5.0/")
docker.withRegistry('', 'hub.docker.com-springbuildmaster') {
image.push()
}
}
}
}
stage('Publish JDK 8 + MongoDB 4.0') {
when {
changeset "ci/openjdk8-mongodb-4.0/**"
@@ -46,16 +62,16 @@ pipeline {
}
}
}
stage('Publish JDK 15 + MongoDB 4.4') {
stage('Publish JDK 16 + MongoDB 4.4') {
when {
changeset "ci/openjdk15-mongodb-4.4/**"
changeset "ci/openjdk16-mongodb-4.4/**"
}
agent { label 'data' }
options { timeout(time: 30, unit: 'MINUTES') }
steps {
script {
def image = docker.build("springci/spring-data-openjdk15-with-mongodb-4.4.4", "ci/openjdk15-mongodb-4.4/")
def image = docker.build("springci/spring-data-openjdk16-with-mongodb-4.4.4", "ci/openjdk16-mongodb-4.4/")
docker.withRegistry('', 'hub.docker.com-springbuildmaster') {
image.push()
}
@@ -67,8 +83,9 @@ pipeline {
stage("test: baseline (jdk8)") {
when {
beforeAgent(true)
anyOf {
branch 'master'
branch(pattern: "main|(\\d\\.\\d\\.x)", comparator: "REGEXP")
not { triggeredBy 'UpstreamCause' }
}
}
@@ -76,6 +93,9 @@ pipeline {
label 'data'
}
options { timeout(time: 30, unit: 'MINUTES') }
environment {
ARTIFACTORY = credentials('02bd1690-b54f-4c9f-819d-a77cb7a9822c')
}
steps {
script {
docker.withRegistry('', 'hub.docker.com-springbuildmaster') {
@@ -85,7 +105,7 @@ pipeline {
sh 'sleep 10'
sh 'mongo --eval "rs.initiate({_id: \'rs0\', members:[{_id: 0, host: \'127.0.0.1:27017\'}]});"'
sh 'sleep 15'
sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw clean dependency:list test -Duser.name=jenkins -Dsort -U -B'
sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -s settings.xml clean dependency:list test -Duser.name=jenkins -Dsort -U -B'
}
}
}
@@ -94,8 +114,9 @@ pipeline {
stage("Test other configurations") {
when {
beforeAgent(true)
allOf {
branch 'master'
branch(pattern: "main|(\\d\\.\\d\\.x)", comparator: "REGEXP")
not { triggeredBy 'UpstreamCause' }
}
}
@@ -105,6 +126,9 @@ pipeline {
label 'data'
}
options { timeout(time: 30, unit: 'MINUTES') }
environment {
ARTIFACTORY = credentials('02bd1690-b54f-4c9f-819d-a77cb7a9822c')
}
steps {
script {
docker.withRegistry('', 'hub.docker.com-springbuildmaster') {
@@ -114,7 +138,7 @@ pipeline {
sh 'sleep 10'
sh 'mongo --eval "rs.initiate({_id: \'rs0\', members:[{_id: 0, host: \'127.0.0.1:27017\'}]});"'
sh 'sleep 15'
sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw clean dependency:list test -Duser.name=jenkins -Dsort -U -B'
sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -s settings.xml clean dependency:list test -Duser.name=jenkins -Dsort -U -B'
}
}
}
@@ -126,6 +150,9 @@ pipeline {
label 'data'
}
options { timeout(time: 30, unit: 'MINUTES') }
environment {
ARTIFACTORY = credentials('02bd1690-b54f-4c9f-819d-a77cb7a9822c')
}
steps {
script {
docker.withRegistry('', 'hub.docker.com-springbuildmaster') {
@@ -135,28 +162,55 @@ pipeline {
sh 'sleep 10'
sh 'mongo --eval "rs.initiate({_id: \'rs0\', members:[{_id: 0, host: \'127.0.0.1:27017\'}]});"'
sh 'sleep 15'
sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw clean dependency:list test -Duser.name=jenkins -Dsort -U -B'
sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -s settings.xml clean dependency:list test -Duser.name=jenkins -Dsort -U -B'
}
}
}
}
}
stage("test: baseline (jdk15)") {
stage("test: mongodb 5.0 (jdk8)") {
agent {
label 'data'
}
options { timeout(time: 30, unit: 'MINUTES') }
environment {
ARTIFACTORY = credentials('02bd1690-b54f-4c9f-819d-a77cb7a9822c')
}
steps {
script {
docker.withRegistry('', 'hub.docker.com-springbuildmaster') {
docker.image('springci/spring-data-openjdk8-with-mongodb-5.0.0:latest').inside('-v $HOME:/tmp/jenkins-home') {
sh 'mkdir -p /tmp/mongodb/db /tmp/mongodb/log'
sh 'mongod --setParameter transactionLifetimeLimitSeconds=90 --setParameter maxTransactionLockRequestTimeoutMillis=10000 --dbpath /tmp/mongodb/db --replSet rs0 --fork --logpath /tmp/mongodb/log/mongod.log &'
sh 'sleep 10'
sh 'mongo --eval "rs.initiate({_id: \'rs0\', members:[{_id: 0, host: \'127.0.0.1:27017\'}]});"'
sh 'sleep 15'
sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -s settings.xml clean dependency:list test -Duser.name=jenkins -Dsort -U -B'
}
}
}
}
}
stage("test: baseline (jdk16)") {
agent {
label 'data'
}
options { timeout(time: 30, unit: 'MINUTES') }
environment {
ARTIFACTORY = credentials('02bd1690-b54f-4c9f-819d-a77cb7a9822c')
}
steps {
script {
docker.withRegistry('', 'hub.docker.com-springbuildmaster') {
docker.image('springci/spring-data-openjdk15-with-mongodb-4.4.4:latest').inside('-v $HOME:/tmp/jenkins-home') {
docker.image('springci/spring-data-openjdk16-with-mongodb-4.4.4:latest').inside('-v $HOME:/tmp/jenkins-home') {
sh 'mkdir -p /tmp/mongodb/db /tmp/mongodb/log'
sh 'mongod --setParameter transactionLifetimeLimitSeconds=90 --setParameter maxTransactionLockRequestTimeoutMillis=10000 --dbpath /tmp/mongodb/db --replSet rs0 --fork --logpath /tmp/mongodb/log/mongod.log &'
sh 'sleep 10'
sh 'mongo --eval "rs.initiate({_id: \'rs0\', members:[{_id: 0, host: \'127.0.0.1:27017\'}]});"'
sh 'sleep 15'
sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -Pjava11 clean dependency:list test -Duser.name=jenkins -Dsort -U -B'
sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -s settings.xml -Pjava11 clean dependency:list test -Duser.name=jenkins -Dsort -U -B'
}
}
}
@@ -167,8 +221,9 @@ pipeline {
stage('Release to artifactory') {
when {
beforeAgent(true)
anyOf {
branch 'master'
branch(pattern: "main|(\\d\\.\\d\\.x)", comparator: "REGEXP")
not { triggeredBy 'UpstreamCause' }
}
}
@@ -185,7 +240,7 @@ pipeline {
script {
docker.withRegistry('', 'hub.docker.com-springbuildmaster') {
docker.image('adoptopenjdk/openjdk8:latest').inside('-v $HOME:/tmp/jenkins-home') {
sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -Pci,artifactory ' +
sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -s settings.xml -Pci,artifactory ' +
'-Dartifactory.server=https://repo.spring.io ' +
"-Dartifactory.username=${ARTIFACTORY_USR} " +
"-Dartifactory.password=${ARTIFACTORY_PSW} " +
@@ -198,35 +253,6 @@ pipeline {
}
}
}
stage('Publish documentation') {
when {
branch 'master'
}
agent {
label 'data'
}
options { timeout(time: 20, unit: 'MINUTES') }
environment {
ARTIFACTORY = credentials('02bd1690-b54f-4c9f-819d-a77cb7a9822c')
}
steps {
script {
docker.withRegistry('', 'hub.docker.com-springbuildmaster') {
docker.image('adoptopenjdk/openjdk8:latest').inside('-v $HOME:/tmp/jenkins-home') {
sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -Pci,distribute ' +
'-Dartifactory.server=https://repo.spring.io ' +
"-Dartifactory.username=${ARTIFACTORY_USR} " +
"-Dartifactory.password=${ARTIFACTORY_PSW} " +
"-Dartifactory.distribution-repository=temp-private-local " +
'-Dmaven.test.skip=true clean deploy -U -B'
}
}
}
}
}
}
post {

202
LICENSE.txt Normal file
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
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
https://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.

View File

@@ -1,6 +1,6 @@
image:https://spring.io/badges/spring-data-mongodb/ga.svg[Spring Data MongoDB,link=https://projects.spring.io/spring-data-mongodb#quick-start] image:https://spring.io/badges/spring-data-mongodb/snapshot.svg[Spring Data MongoDB,link=https://projects.spring.io/spring-data-mongodb#quick-start]
= Spring Data MongoDB image:https://jenkins.spring.io/buildStatus/icon?job=spring-data-mongodb%2Fmaster&subject=Build[link=https://jenkins.spring.io/view/SpringData/job/spring-data-mongodb/] https://gitter.im/spring-projects/spring-data[image:https://badges.gitter.im/spring-projects/spring-data.svg[Gitter]]
= Spring Data MongoDB image:https://jenkins.spring.io/buildStatus/icon?job=spring-data-mongodb%2Fmain&subject=Build[link=https://jenkins.spring.io/view/SpringData/job/spring-data-mongodb/] https://gitter.im/spring-projects/spring-data[image:https://badges.gitter.im/spring-projects/spring-data.svg[Gitter]]
The primary goal of the https://projects.spring.io/spring-data[Spring Data] project is to make it easier to build Spring-powered applications that use new data access technologies such as non-relational databases, map-reduce frameworks, and cloud based data services.
@@ -8,10 +8,12 @@ The Spring Data MongoDB project aims to provide a familiar and consistent Spring
The Spring Data MongoDB project provides integration with the MongoDB document database.
Key functional areas of Spring Data MongoDB are a POJO centric model for interacting with a MongoDB `+Document+` and easily writing a repository style data access layer.
[[code-of-conduct]]
== Code of Conduct
This project is governed by the https://github.com/spring-projects/.github/blob/e3cc2ff230d8f1dca06535aa6b5a4a23815861d4/CODE_OF_CONDUCT.md[Spring Code of Conduct]. By participating, you are expected to uphold this code of conduct. Please report unacceptable behavior to spring-code-of-conduct@pivotal.io.
[[getting-started]]
== Getting Started
Here is a quick teaser of an application using Spring Data Repositories in Java:
@@ -59,6 +61,7 @@ class ApplicationConfig extends AbstractMongoClientConfiguration {
}
----
[[maven-configuration]]
=== Maven configuration
Add the Maven dependency:
@@ -68,24 +71,25 @@ Add the Maven dependency:
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
<version>${version}.RELEASE</version>
<version>${version}</version>
</dependency>
----
If you'd rather like the latest snapshots of the upcoming major version, use our Maven snapshot repository and declare the appropriate dependency version.
If you'd rather like the latest snapshots of the upcoming major version, use our Maven snapshot repository
and declare the appropriate dependency version.
[source,xml]
----
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
<version>${version}.BUILD-SNAPSHOT</version>
<version>${version}-SNAPSHOT</version>
</dependency>
<repository>
<id>spring-libs-snapshot</id>
<id>spring-snapshot</id>
<name>Spring Snapshot Repository</name>
<url>https://repo.spring.io/libs-snapshot</url>
<url>https://repo.spring.io/snapshot</url>
</repository>
----
@@ -98,7 +102,7 @@ Some of the changes affect the initial setup configuration as well as compile/ru
.Changed XML Namespace Elements and Attributes:
|===
Element / Attribute | 2.x | 3.x
| Element / Attribute | 2.x | 3.x
| `<mongo:mongo-client />`
| Used to create a `com.mongodb.MongoClient`
@@ -116,7 +120,7 @@ Use `<mongo:client-settings cluster-hosts="..." />` instead
.Removed XML Namespace Elements and Attributes:
|===
Element / Attribute | Replacement in 3.x | Comment
| Element / Attribute | Replacement in 3.x | Comment
| `<mongo:db-factory mongo-ref="..." />`
| `<mongo:db-factory mongo-client-ref="..." />`
@@ -133,7 +137,7 @@ Element / Attribute | Replacement in 3.x | Comment
.New XML Namespace Elements and Attributes:
|===
Element | Comment
| Element | Comment
| `<mongo:db-factory mongo-client-ref="..." />`
| Replacement for `<mongo:db-factory mongo-ref="..." />`
@@ -153,7 +157,7 @@ Element | Comment
.Java API changes
|===
Type | Comment
| Type | Comment
| `MongoClientFactoryBean`
| Creates `com.mongodb.client.MongoClient` instead of `com.mongodb.MongoClient` +
@@ -174,7 +178,7 @@ Uses `MongoClientSettings` instead of `MongoClientOptions`.
.Removed Java API:
|===
2.x | Replacement in 3.x | Comment
| 2.x | Replacement in 3.x | Comment
| `MongoClientOptionsFactoryBean`
| `MongoClientSettingsFactoryBean`
@@ -226,6 +230,7 @@ static class Config extends AbstractMongoClientConfiguration {
----
====
[[getting-help]]
== Getting Help
Having trouble with Spring Data? Wed love to help!
@@ -239,6 +244,7 @@ If you are just starting out with Spring, try one of the https://spring.io/guide
You can also chat with the community on https://gitter.im/spring-projects/spring-data[Gitter].
* Report bugs with Spring Data MongoDB at https://github.com/spring-projects/spring-data-mongodb/issues[github.com/spring-projects/spring-data-mongodb/issues].
[[reporting-issues]]
== Reporting Issues
Spring Data uses Github as issue tracking system to record bugs and feature requests.
@@ -249,10 +255,85 @@ If you want to raise an issue, please follow the recommendations below:
* Please provide as much information as possible with the issue report, we like to know the version of Spring Data that you are using, the JVM version, Stacktrace, etc.
* If you need to paste code, or include a stack trace use https://guides.github.com/features/mastering-markdown/[Markdown] code fences +++```+++.
[[guides]]
== Guides
The https://spring.io/[spring.io] site contains several guides that show how to use Spring Data step-by-step:
* https://spring.io/guides/gs/accessing-data-mongodb/[Accessing Data with MongoDB] is a very basic guide that shows you how to create a simple application and how to access data using repositories.
* https://spring.io/guides/gs/accessing-mongodb-data-rest/[Accessing MongoDB Data with REST] is a guide to creating a REST web service exposing data stored in MongoDB through repositories.
[[examples]]
== Examples
* https://github.com/spring-projects/spring-data-examples/[Spring Data Examples] contains example projects that explain specific features in more detail.
[[building-from-source]]
== Building from Source
You dont need to build from source to use Spring Data (binaries in https://repo.spring.io[repo.spring.io]), but if you want to try out the latest and greatest, Spring Data can be easily built with the https://github.com/takari/maven-wrapper[maven wrapper].
You also need JDK 1.8.
You do not need to build from source to use Spring Data. Binaries are available in https://repo.spring.io[repo.spring.io].
and accessible from Maven using the Maven configuration noted <<maven-configuration,above>>.
NOTE: Configuration for Gradle is similar to Maven.
The best way to get started is by creating a Spring Boot project using MongoDB on https://start.spring.io[start.spring.io].
Follow this https://start.spring.io/#type=maven-project&language=java&platformVersion=2.5.4&packaging=jar&jvmVersion=1.8&groupId=com.example&artifactId=demo&name=demo&description=Demo%20project%20for%20Spring%20Boot&packageName=com.example.demo&dependencies=data-mongodb[link]
to build an imperative application and this https://start.spring.io/#type=maven-project&language=java&platformVersion=2.5.4&packaging=jar&jvmVersion=1.8&groupId=com.example&artifactId=demo&name=demo&description=Demo%20project%20for%20Spring%20Boot&packageName=com.example.demo&dependencies=data-mongodb-reactive[link]
to build a reactive one.
However, if you want to try out the latest and greatest, Spring Data can be easily built with the https://github.com/takari/maven-wrapper[maven wrapper]
and minimally JDK 8 (https://www.oracle.com/java/technologies/downloads/[JDK downloads]).
In order to build Spring Data MongoDB, first you will need to https://www.mongodb.com/try/download/community[download]
and https://docs.mongodb.com/manual/installation/[install a MongoDB distribution].
Once you have installed MongoDB, you need to start a MongoDB server. It is convenient to set an environment variable to
your MongoDB installation (e.g. `MONGODB_HOME`).
To run the full test suite a https://docs.mongodb.com/manual/tutorial/deploy-replica-set/[MongoDB Replica Set] is required.
To run the MongoDB server enter the following command from a command-line:
[source,bash]
----
$ $MONGODB_HOME/bin/mongod --dbpath $MONGODB_HOME/runtime/data --ipv6 --port 27017 --replSet rs0
...
"msg":"Successfully connected to host"
----
Once the MongoDB server starts up, you should see the message (`msg`), "_Successfully connected to host_".
Notice the `--dbpath` option to the `mongod` command. You can set this to anything you like, but in this case, we set
the absolute path to a sub-directory (`runtime/data/`) under the MongoDB installation directory (in `$MONGODB_HOME`).
You need to initialize the MongoDB replica set only once on the first time the MongoDB server is started.
To initialize the replica set, start a mongo client:
[source,bash]
----
$ $MONGODB_HOME/bin/mongo
MongoDB server version: 5.0.0
...
----
Then enter the following command:
[source,bash]
----
mongo> rs.initiate({ _id: 'rs0', members: [ { _id: 0, host: '127.0.0.1:27017' } ] })
----
Finally, on UNIX-based system (for example, Linux or Mac OS X) you may need to adjust the `ulimit`.
In case you need to, you can adjust the `ulimit` with the following command (32768 is just a recommendation):
[source,bash]
----
$ ulimit -n 32768
----
You can use `ulimit -a` again to verify the `ulimit` on "_open files_" was set appropriately.
Now you are ready to build Spring Data MongoDB. Simply enter the following `mvnw` (Maven Wrapper) command:
[source,bash]
----
@@ -261,7 +342,8 @@ You also need JDK 1.8.
If you want to build with the regular `mvn` command, you will need https://maven.apache.org/run-maven/index.html[Maven v3.5.0 or above].
_Also see link:CONTRIBUTING.adoc[CONTRIBUTING.adoc] if you wish to submit pull requests, and in particular please sign the https://cla.pivotal.io/sign/spring[Contributors Agreement] before your first non-trivial change._
_Also see link:CONTRIBUTING.adoc[CONTRIBUTING.adoc] if you wish to submit pull requests, and in particular, please sign
the https://cla.pivotal.io/sign/spring[Contributors Agreement] before your first non-trivial change._
=== Building reference documentation
@@ -274,17 +356,7 @@ Building the documentation builds also the project without running tests.
The generated documentation is available from `target/site/reference/html/index.html`.
== Guides
The https://spring.io/[spring.io] site contains several guides that show how to use Spring Data step-by-step:
* https://spring.io/guides/gs/accessing-data-mongodb/[Accessing Data with MongoDB] is a very basic guide that shows you how to create a simple application and how to access data using repositories.
* https://spring.io/guides/gs/accessing-mongodb-data-rest/[Accessing MongoDB Data with REST] is a guide to creating a REST web service exposing data stored in MongoDB through repositories.
== Examples
* https://github.com/spring-projects/spring-data-examples/[Spring Data Examples] contains example projects that explain specific features in more detail.
[[license]]
== License
Spring Data MongoDB is Open Source software released under the https://www.apache.org/licenses/LICENSE-2.0.html[Apache 2.0 license].

View File

@@ -4,6 +4,9 @@ ENV TZ=Etc/UTC
ENV DEBIAN_FRONTEND=noninteractive
RUN set -eux; \
sed -i -e 's/archive.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list; \
sed -i -e 's/security.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list; \
sed -i -e 's/http/https/g' /etc/apt/sources.list ; \
apt-get update && apt-get install -y apt-transport-https apt-utils gnupg2 ; \
apt-key adv --keyserver hkps://keyserver.ubuntu.com:443 --recv 656408E390CFB1F5 ; \
echo "deb [ arch=amd64 ] https://repo.mongodb.org/apt/ubuntu bionic/mongodb-org/4.4 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-4.4.list; \

View File

@@ -1,9 +1,12 @@
FROM adoptopenjdk/openjdk15:latest
FROM adoptopenjdk/openjdk16:latest
ENV TZ=Etc/UTC
ENV DEBIAN_FRONTEND=noninteractive
RUN set -eux; \
sed -i -e 's/archive.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list; \
sed -i -e 's/security.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list; \
sed -i -e 's/http/https/g' /etc/apt/sources.list ; \
apt-get update && apt-get install -y apt-transport-https apt-utils gnupg2 ; \
apt-key adv --keyserver hkps://keyserver.ubuntu.com:443 --recv 656408E390CFB1F5 ; \
echo "deb [ arch=amd64 ] https://repo.mongodb.org/apt/ubuntu bionic/mongodb-org/4.4 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-4.4.list; \

View File

@@ -4,6 +4,9 @@ ENV TZ=Etc/UTC
ENV DEBIAN_FRONTEND=noninteractive
RUN RUN set -eux; \
sed -i -e 's/archive.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list; \
sed -i -e 's/security.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list; \
sed -i -e 's/http/https/g' /etc/apt/sources.list ; \
apt-get update && apt-get install -y apt-transport-https apt-utils gnupg2 ; \
apt-key adv --keyserver hkps://keyserver.ubuntu.com:443 --recv 9DA31620334BD75D9DCB49F368818C72E52529D4 ; \
echo "deb [ arch=amd64 ] https://repo.mongodb.org/apt/ubuntu bionic/mongodb-org/4.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-4.0.list; \

View File

@@ -4,6 +4,9 @@ ENV TZ=Etc/UTC
ENV DEBIAN_FRONTEND=noninteractive
RUN set -eux; \
sed -i -e 's/archive.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list; \
sed -i -e 's/security.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list; \
sed -i -e 's/http/https/g' /etc/apt/sources.list ; \
apt-get update && apt-get install -y apt-transport-https apt-utils gnupg2 ; \
apt-key adv --keyserver hkps://keyserver.ubuntu.com:443 --recv 656408E390CFB1F5 ; \
echo "deb [ arch=amd64 ] https://repo.mongodb.org/apt/ubuntu bionic/mongodb-org/4.4 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-4.4.list; \

View File

@@ -0,0 +1,20 @@
FROM adoptopenjdk/openjdk8:latest
ENV TZ=Etc/UTC
ENV DEBIAN_FRONTEND=noninteractive
RUN set -eux; \
sed -i -e 's/archive.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list; \
sed -i -e 's/security.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list; \
sed -i -e 's/http/https/g' /etc/apt/sources.list ; \
apt-get update && apt-get install -y apt-transport-https apt-utils gnupg2 wget ; \
# MongoDB 5.0 release signing key
apt-key adv --keyserver hkps://keyserver.ubuntu.com:443 --recv B00A0BD1E2C63C11 ; \
# Needed when MongoDB creates a 5.0 folder.
echo "deb [ arch=amd64 ] https://repo.mongodb.org/apt/ubuntu bionic/mongodb-org/5.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-5.0.list; \
echo ${TZ} > /etc/timezone;
RUN apt-get update; \
apt-get install -y mongodb-org=5.0.3 mongodb-org-server=5.0.3 mongodb-org-shell=5.0.3 mongodb-org-mongos=5.0.3 mongodb-org-tools=5.0.3; \
apt-get clean; \
rm -rf /var/lib/apt/lists/*;

21
pom.xml
View File

@@ -5,7 +5,7 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>3.2.0</version>
<version>3.3.0</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.5.0</version>
<version>2.6.0</version>
</parent>
<modules>
@@ -26,8 +26,8 @@
<properties>
<project.type>multi</project.type>
<dist.id>spring-data-mongodb</dist.id>
<springdata.commons>2.5.0</springdata.commons>
<mongo>4.2.3</mongo>
<springdata.commons>2.6.0</springdata.commons>
<mongo>4.4.0</mongo>
<mongo.reactivestreams>${mongo}</mongo.reactivestreams>
<jmh.version>1.19</jmh.version>
</properties>
@@ -141,11 +141,11 @@
<id>sonatype-libs-snapshot</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<releases>
<enabled>false</enabled>
</releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
@@ -158,11 +158,6 @@
<id>spring-libs-milestone</id>
<url>https://repo.spring.io/libs-milestone</url>
</pluginRepository>
<pluginRepository>
<id>bintray-plugins</id>
<name>bintray-plugins</name>
<url>https://jcenter.bintray.com</url>
</pluginRepository>
</pluginRepositories>
</project>

29
settings.xml Normal file
View File

@@ -0,0 +1,29 @@
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
https://maven.apache.org/xsd/settings-1.0.0.xsd">
<servers>
<server>
<id>spring-plugins-release</id>
<username>${env.ARTIFACTORY_USR}</username>
<password>${env.ARTIFACTORY_PSW}</password>
</server>
<server>
<id>spring-libs-snapshot</id>
<username>${env.ARTIFACTORY_USR}</username>
<password>${env.ARTIFACTORY_PSW}</password>
</server>
<server>
<id>spring-libs-milestone</id>
<username>${env.ARTIFACTORY_USR}</username>
<password>${env.ARTIFACTORY_PSW}</password>
</server>
<server>
<id>spring-libs-release</id>
<username>${env.ARTIFACTORY_USR}</username>
<password>${env.ARTIFACTORY_PSW}</password>
</server>
</servers>
</settings>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>3.2.0</version>
<version>3.3.0</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -14,7 +14,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>3.2.0</version>
<version>3.3.0</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>3.2.0</version>
<version>3.3.0</version>
<relativePath>../pom.xml</relativePath>
</parent>
@@ -87,6 +87,13 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>3.0.2</version>
<optional>true</optional>
</dependency>
<!-- reactive -->
<dependency>
@@ -310,6 +317,15 @@
<scope>test</scope>
</dependency>
<!-- jMolecules -->
<dependency>
<groupId>org.jmolecules</groupId>
<artifactId>jmolecules-ddd</artifactId>
<version>${jmolecules}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@@ -31,7 +31,7 @@ import org.springframework.util.StringUtils;
* expression. The expression will be wrapped within <code>{ ... }</code> if necessary. The actual parsing and parameter
* binding of placeholders like {@code ?0} is delayed upon first call on the the target {@link Document} via
* {@link #toDocument()}.
* <p />
* <br />
*
* <pre class="code">
* $toUpper : $name -> { '$toUpper' : '$name' }

View File

@@ -20,8 +20,8 @@ import org.springframework.util.StringUtils;
/**
* Helper class featuring helper methods for working with MongoDb collections.
* <p/>
* <p/>
* <br />
* <br />
* Mainly intended for internal use within the framework.
*
* @author Thomas Risberg

View File

@@ -30,7 +30,7 @@ import com.mongodb.client.MongoDatabase;
* Helper class for managing a {@link MongoDatabase} instances via {@link MongoDatabaseFactory}. Used for obtaining
* {@link ClientSession session bound} resources, such as {@link MongoDatabase} and
* {@link com.mongodb.client.MongoCollection} suitable for transactional usage.
* <p />
* <br />
* <strong>Note:</strong> Intended for internal usage only.
*
* @author Christoph Strobl
@@ -43,7 +43,7 @@ public class MongoDatabaseUtils {
/**
* Obtain the default {@link MongoDatabase database} form the given {@link MongoDatabaseFactory factory} using
* {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}.
* <p />
* <br />
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the current
* {@link Thread} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}.
*
@@ -56,7 +56,7 @@ public class MongoDatabaseUtils {
/**
* Obtain the default {@link MongoDatabase database} form the given {@link MongoDatabaseFactory factory}.
* <p />
* <br />
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the current
* {@link Thread} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}.
*
@@ -71,7 +71,7 @@ public class MongoDatabaseUtils {
/**
* Obtain the {@link MongoDatabase database} with given name form the given {@link MongoDatabaseFactory factory} using
* {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}.
* <p />
* <br />
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the current
* {@link Thread} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}.
*
@@ -85,7 +85,7 @@ public class MongoDatabaseUtils {
/**
* Obtain the {@link MongoDatabase database} with given name form the given {@link MongoDatabaseFactory factory}.
* <p />
* <br />
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the current
* {@link Thread} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}.
*
@@ -104,7 +104,8 @@ public class MongoDatabaseUtils {
Assert.notNull(factory, "Factory must not be null!");
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
if (sessionSynchronization == SessionSynchronization.NEVER
|| !TransactionSynchronizationManager.isSynchronizationActive()) {
return StringUtils.hasText(dbName) ? factory.getMongoDatabase(dbName) : factory.getMongoDatabase();
}

View File

@@ -18,7 +18,7 @@ package org.springframework.data.mongodb;
/**
* Wrapper object for MongoDB expressions like {@code $toUpper : $name} that manifest as {@link org.bson.Document} when
* passed on to the driver.
* <p />
* <br />
* A set of predefined {@link MongoExpression expressions}, including a
* {@link org.springframework.data.mongodb.core.aggregation.AggregationSpELExpression SpEL based variant} for method
* like expressions (eg. {@code toUpper(name)}) are available via the

View File

@@ -24,7 +24,7 @@ import com.mongodb.client.ClientSession;
/**
* MongoDB specific {@link ResourceHolderSupport resource holder}, wrapping a {@link ClientSession}.
* {@link MongoTransactionManager} binds instances of this class to the thread.
* <p />
* <br />
* <strong>Note:</strong> Intended for internal usage only.
*
* @author Christoph Strobl

View File

@@ -37,18 +37,18 @@ import com.mongodb.client.ClientSession;
/**
* A {@link org.springframework.transaction.PlatformTransactionManager} implementation that manages
* {@link ClientSession} based transactions for a single {@link MongoDatabaseFactory}.
* <p />
* <br />
* Binds a {@link ClientSession} from the specified {@link MongoDatabaseFactory} to the thread.
* <p />
* <br />
* {@link TransactionDefinition#isReadOnly() Readonly} transactions operate on a {@link ClientSession} and enable causal
* consistency, and also {@link ClientSession#startTransaction() start}, {@link ClientSession#commitTransaction()
* commit} or {@link ClientSession#abortTransaction() abort} a transaction.
* <p />
* <br />
* Application code is required to retrieve the {@link com.mongodb.client.MongoDatabase} via
* {@link MongoDatabaseUtils#getDatabase(MongoDatabaseFactory)} instead of a standard
* {@link MongoDatabaseFactory#getMongoDatabase()} call. Spring classes such as
* {@link org.springframework.data.mongodb.core.MongoTemplate} use this strategy implicitly.
* <p />
* <br />
* 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>
@@ -69,11 +69,11 @@ public class MongoTransactionManager extends AbstractPlatformTransactionManager
/**
* Create a new {@link MongoTransactionManager} for bean-style usage.
* <p />
* <br />
* <strong>Note:</strong>The {@link MongoDatabaseFactory db factory} has to be
* {@link #setDbFactory(MongoDatabaseFactory) set} before using the instance. Use this constructor to prepare a
* {@link MongoTransactionManager} via a {@link org.springframework.beans.factory.BeanFactory}.
* <p />
* <br />
* Optionally it is possible to set default {@link TransactionOptions transaction options} defining
* {@link com.mongodb.ReadConcern} and {@link com.mongodb.WriteConcern}.
*
@@ -212,8 +212,8 @@ public class MongoTransactionManager extends AbstractPlatformTransactionManager
* 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 />
* <pre>
* <code>
* <pre>
* int retries = 3;
* do {
* try {
@@ -226,8 +226,8 @@ public class MongoTransactionManager extends AbstractPlatformTransactionManager
* }
* Thread.sleep(500);
* } while (--retries > 0);
* </pre>
* </code>
* </pre>
*
* @param transactionObject never {@literal null}.
* @throws Exception in case of transaction errors.

View File

@@ -36,7 +36,7 @@ import com.mongodb.reactivestreams.client.MongoDatabase;
* Helper class for managing reactive {@link MongoDatabase} instances via {@link ReactiveMongoDatabaseFactory}. Used for
* obtaining {@link ClientSession session bound} resources, such as {@link MongoDatabase} and {@link MongoCollection}
* suitable for transactional usage.
* <p />
* <br />
* <strong>Note:</strong> Intended for internal usage only.
*
* @author Mark Paluch
@@ -75,7 +75,7 @@ public class ReactiveMongoDatabaseUtils {
/**
* Obtain the default {@link MongoDatabase database} form the given {@link ReactiveMongoDatabaseFactory factory} using
* {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}.
* <p />
* <br />
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the subscriber
* {@link Context} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}.
*
@@ -88,7 +88,7 @@ public class ReactiveMongoDatabaseUtils {
/**
* Obtain the default {@link MongoDatabase database} form the given {@link ReactiveMongoDatabaseFactory factory}.
* <p />
* <br />
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the subscriber
* {@link Context} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}.
*
@@ -104,7 +104,7 @@ public class ReactiveMongoDatabaseUtils {
/**
* Obtain the {@link MongoDatabase database} with given name form the given {@link ReactiveMongoDatabaseFactory
* factory} using {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}.
* <p />
* <br />
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the subscriber
* {@link Context} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}.
*
@@ -119,7 +119,7 @@ public class ReactiveMongoDatabaseUtils {
/**
* Obtain the {@link MongoDatabase database} with given name form the given {@link ReactiveMongoDatabaseFactory
* factory}.
* <p />
* <br />
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the subscriber
* {@link Context} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}.
*
@@ -138,6 +138,10 @@ public class ReactiveMongoDatabaseUtils {
Assert.notNull(factory, "DatabaseFactory must not be null!");
if (sessionSynchronization == SessionSynchronization.NEVER) {
return getMongoDatabaseOrDefault(dbName, factory);
}
return TransactionSynchronizationManager.forCurrentTransaction()
.filter(TransactionSynchronizationManager::isSynchronizationActive) //
.flatMap(synchronizationManager -> {

View File

@@ -24,7 +24,7 @@ import com.mongodb.reactivestreams.client.ClientSession;
/**
* MongoDB specific resource holder, wrapping a {@link ClientSession}. {@link ReactiveMongoTransactionManager} binds
* instances of this class to the subscriber context.
* <p />
* <br />
* <strong>Note:</strong> Intended for internal usage only.
*
* @author Mark Paluch

View File

@@ -38,21 +38,21 @@ import com.mongodb.reactivestreams.client.ClientSession;
* A {@link org.springframework.transaction.ReactiveTransactionManager} implementation that manages
* {@link com.mongodb.reactivestreams.client.ClientSession} based transactions for a single
* {@link org.springframework.data.mongodb.ReactiveMongoDatabaseFactory}.
* <p />
* <br />
* Binds a {@link ClientSession} from the specified
* {@link org.springframework.data.mongodb.ReactiveMongoDatabaseFactory} to the subscriber
* {@link reactor.util.context.Context}.
* <p />
* <br />
* {@link org.springframework.transaction.TransactionDefinition#isReadOnly() Readonly} transactions operate on a
* {@link ClientSession} and enable causal consistency, and also {@link ClientSession#startTransaction() start},
* {@link com.mongodb.reactivestreams.client.ClientSession#commitTransaction() commit} or
* {@link ClientSession#abortTransaction() abort} a transaction.
* <p />
* <br />
* Application code is required to retrieve the {@link com.mongodb.reactivestreams.client.MongoDatabase} via
* {@link org.springframework.data.mongodb.ReactiveMongoDatabaseUtils#getDatabase(ReactiveMongoDatabaseFactory)} instead
* of a standard {@link org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#getMongoDatabase()} call. Spring
* classes such as {@link org.springframework.data.mongodb.core.ReactiveMongoTemplate} use this strategy implicitly.
* <p />
* <br />
* By default failure of a {@literal commit} operation raises a {@link TransactionSystemException}. You can override
* {@link #doCommit(TransactionSynchronizationManager, ReactiveMongoTransactionObject)} to implement the
* <a href="https://docs.mongodb.com/manual/core/transactions/#retry-commit-operation">Retry Commit Operation</a>
@@ -71,11 +71,11 @@ public class ReactiveMongoTransactionManager extends AbstractReactiveTransaction
/**
* Create a new {@link ReactiveMongoTransactionManager} for bean-style usage.
* <p />
* <br />
* <strong>Note:</strong>The {@link org.springframework.data.mongodb.ReactiveMongoDatabaseFactory db factory} has to
* be {@link #setDatabaseFactory(ReactiveMongoDatabaseFactory)} set} before using the instance. Use this constructor
* to prepare a {@link ReactiveMongoTransactionManager} via a {@link org.springframework.beans.factory.BeanFactory}.
* <p />
* <br />
* Optionally it is possible to set default {@link TransactionOptions transaction options} defining
* {@link com.mongodb.ReadConcern} and {@link com.mongodb.WriteConcern}.
*

View File

@@ -35,7 +35,7 @@ import com.mongodb.session.ClientSession;
/**
* {@link MethodInterceptor} implementation looking up and invoking an alternative target method having
* {@link ClientSession} as its first argument. This allows seamless integration with the existing code base.
* <p />
* <br />
* The {@link MethodInterceptor} is aware of methods on {@code MongoCollection} that my return new instances of itself
* like (eg. {@link com.mongodb.reactivestreams.client.MongoCollection#withWriteConcern(WriteConcern)} and decorate them
* if not already proxied.

View File

@@ -15,13 +15,20 @@
*/
package org.springframework.data.mongodb;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
/**
* {@link SessionSynchronization} is used along with {@link org.springframework.data.mongodb.core.MongoTemplate} to
* define in which type of transactions to participate if any.
* {@link SessionSynchronization} is used along with {@code MongoTemplate} to define in which type of transactions to
* participate if any.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.1
* @see MongoTemplate#setSessionSynchronization(SessionSynchronization)
* @see MongoDatabaseUtils#getDatabase(MongoDatabaseFactory, SessionSynchronization)
* @see ReactiveMongoTemplate#setSessionSynchronization(SessionSynchronization)
* @see ReactiveMongoDatabaseUtils#getDatabase(ReactiveMongoDatabaseFactory, SessionSynchronization)
*/
public enum SessionSynchronization {
@@ -34,5 +41,12 @@ public enum SessionSynchronization {
/**
* Synchronize with native MongoDB transactions initiated via {@link MongoTransactionManager}.
*/
ON_ACTUAL_TRANSACTION;
ON_ACTUAL_TRANSACTION,
/**
* Do not participate in ongoing transactions.
*
* @since 3.2.5
*/
NEVER;
}

View File

@@ -48,7 +48,7 @@ public class SpringDataMongoDB {
/**
* Fetches the "Implementation-Version" manifest attribute from the jar file.
* <p />
* <br />
* Note that some ClassLoaders do not expose the package metadata, hence this class might not be able to determine the
* version in all environments. In this case the current Major version is returned as a fallback.
*

View File

@@ -172,8 +172,7 @@ public abstract class MongoConfigurationSupport {
/**
* Configures whether to abbreviate field names for domain objects by configuring a
* {@link CamelCaseAbbreviatingFieldNamingStrategy} on the {@link MongoMappingContext} instance created. For advanced
* customization needs, consider overriding {@link #mappingMongoConverter()}.
* {@link CamelCaseAbbreviatingFieldNamingStrategy} on the {@link MongoMappingContext} instance created.
*
* @return
*/

View File

@@ -22,9 +22,12 @@ import java.util.Map;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.CustomEditorConfigurer;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionValidationException;
import org.springframework.beans.factory.support.ManagedMap;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.data.mongodb.core.MongoClientSettingsFactoryBean;
import org.springframework.data.mongodb.core.MongoServerApiFactoryBean;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;
@@ -112,6 +115,20 @@ abstract class MongoParsingUtils {
// Field level encryption
setPropertyReference(clientOptionsDefBuilder, settingsElement, "encryption-settings-ref", "autoEncryptionSettings");
// ServerAPI
if (StringUtils.hasText(settingsElement.getAttribute("server-api-version"))) {
MongoServerApiFactoryBean serverApiFactoryBean = new MongoServerApiFactoryBean();
serverApiFactoryBean.setVersion(settingsElement.getAttribute("server-api-version"));
try {
clientOptionsDefBuilder.addPropertyValue("serverApi", serverApiFactoryBean.getObject());
} catch (Exception exception) {
throw new BeanDefinitionValidationException("Non parsable server-api.", exception);
}
} else {
setPropertyReference(clientOptionsDefBuilder, settingsElement, "server-api-ref", "serverApi");
}
// and the rest
mongoClientBuilder.addPropertyValue("mongoClientSettings", clientOptionsDefBuilder.getBeanDefinition());

View File

@@ -28,7 +28,7 @@ import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
* @author Christoph Strobl
* @since 3.1
*/
class PersistentEntitiesFactoryBean implements FactoryBean<PersistentEntities> {
public class PersistentEntitiesFactoryBean implements FactoryBean<PersistentEntities> {
private final MappingMongoConverter converter;

View File

@@ -242,13 +242,13 @@ public class ChangeStreamOptions {
/**
* Set the filter to apply.
* <p/>
* <br />
* Fields on aggregation expression root level are prefixed to map to fields contained in
* {@link ChangeStreamDocument#getFullDocument() fullDocument}. However {@literal operationType}, {@literal ns},
* {@literal documentKey} and {@literal fullDocument} are reserved words that will be omitted, and therefore taken
* as given, during the mapping procedure. You may want to have a look at the
* <a href="https://docs.mongodb.com/manual/reference/change-events/">structure of Change Events</a>.
* <p/>
* <br />
* Use {@link org.springframework.data.mongodb.core.aggregation.TypedAggregation} to ensure filter expressions are
* mapped to domain type fields.
*

View File

@@ -17,8 +17,11 @@ package org.springframework.data.mongodb.core;
import java.util.Optional;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
import org.springframework.data.mongodb.core.timeseries.Granularity;
import org.springframework.data.mongodb.core.timeseries.GranularityDefinition;
import org.springframework.data.mongodb.core.validation.Validator;
import org.springframework.data.util.Optionals;
import org.springframework.lang.Nullable;
@@ -42,6 +45,7 @@ public class CollectionOptions {
private @Nullable Boolean capped;
private @Nullable Collation collation;
private ValidationOptions validationOptions;
private @Nullable TimeSeriesOptions timeSeriesOptions;
/**
* Constructs a new <code>CollectionOptions</code> instance.
@@ -54,17 +58,19 @@ public class CollectionOptions {
*/
@Deprecated
public CollectionOptions(@Nullable Long size, @Nullable Long maxDocuments, @Nullable Boolean capped) {
this(size, maxDocuments, capped, null, ValidationOptions.none());
this(size, maxDocuments, capped, null, ValidationOptions.none(), null);
}
private CollectionOptions(@Nullable Long size, @Nullable Long maxDocuments, @Nullable Boolean capped,
@Nullable Collation collation, ValidationOptions validationOptions) {
@Nullable Collation collation, ValidationOptions validationOptions,
@Nullable TimeSeriesOptions timeSeriesOptions) {
this.maxDocuments = maxDocuments;
this.size = size;
this.capped = capped;
this.collation = collation;
this.validationOptions = validationOptions;
this.timeSeriesOptions = timeSeriesOptions;
}
/**
@@ -78,7 +84,7 @@ public class CollectionOptions {
Assert.notNull(collation, "Collation must not be null!");
return new CollectionOptions(null, null, null, collation, ValidationOptions.none());
return new CollectionOptions(null, null, null, collation, ValidationOptions.none(), null);
}
/**
@@ -88,7 +94,21 @@ public class CollectionOptions {
* @since 2.0
*/
public static CollectionOptions empty() {
return new CollectionOptions(null, null, null, null, ValidationOptions.none());
return new CollectionOptions(null, null, null, null, ValidationOptions.none(), null);
}
/**
* Quick way to set up {@link CollectionOptions} for a Time Series collection. For more advanced settings use
* {@link #timeSeries(TimeSeriesOptions)}.
*
* @param timeField The name of the property which contains the date in each time series document. Must not be
* {@literal null}.
* @return new instance of {@link CollectionOptions}.
* @see #timeSeries(TimeSeriesOptions)
* @since 3.3
*/
public static CollectionOptions timeSeries(String timeField) {
return empty().timeSeries(TimeSeriesOptions.timeSeries(timeField));
}
/**
@@ -99,7 +119,7 @@ public class CollectionOptions {
* @since 2.0
*/
public CollectionOptions capped() {
return new CollectionOptions(size, maxDocuments, true, collation, validationOptions);
return new CollectionOptions(size, maxDocuments, true, collation, validationOptions, null);
}
/**
@@ -110,7 +130,7 @@ public class CollectionOptions {
* @since 2.0
*/
public CollectionOptions maxDocuments(long maxDocuments) {
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions);
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions);
}
/**
@@ -121,7 +141,7 @@ public class CollectionOptions {
* @since 2.0
*/
public CollectionOptions size(long size) {
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions);
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions);
}
/**
@@ -132,7 +152,7 @@ public class CollectionOptions {
* @since 2.0
*/
public CollectionOptions collation(@Nullable Collation collation) {
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions);
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions);
}
/**
@@ -252,7 +272,20 @@ public class CollectionOptions {
public CollectionOptions validation(ValidationOptions validationOptions) {
Assert.notNull(validationOptions, "ValidationOptions must not be null!");
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions);
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions);
}
/**
* Create new {@link CollectionOptions} with the given {@link TimeSeriesOptions}.
*
* @param timeSeriesOptions must not be {@literal null}.
* @return new instance of {@link CollectionOptions}.
* @since 3.3
*/
public CollectionOptions timeSeries(TimeSeriesOptions timeSeriesOptions) {
Assert.notNull(timeSeriesOptions, "TimeSeriesOptions must not be null!");
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions);
}
/**
@@ -303,6 +336,16 @@ public class CollectionOptions {
return validationOptions.isEmpty() ? Optional.empty() : Optional.of(validationOptions);
}
/**
* Get the {@link TimeSeriesOptions} if available.
*
* @return {@link Optional#empty()} if not specified.
* @since 3.3
*/
public Optional<TimeSeriesOptions> getTimeSeriesOptions() {
return Optional.ofNullable(timeSeriesOptions);
}
/**
* Encapsulation of ValidationOptions options.
*
@@ -385,7 +428,7 @@ public class CollectionOptions {
/**
* Get the {@code validationAction} to perform.
*
* @return @return {@link Optional#empty()} if not set.
* @return {@link Optional#empty()} if not set.
*/
public Optional<ValidationAction> getValidationAction() {
return Optional.ofNullable(validationAction);
@@ -398,4 +441,89 @@ public class CollectionOptions {
return !Optionals.isAnyPresent(getValidator(), getValidationAction(), getValidationLevel());
}
}
/**
* Options applicable to Time Series collections.
*
* @author Christoph Strobl
* @since 3.3
* @see <a href=
* "https://docs.mongodb.com/manual/core/timeseries-collections">https://docs.mongodb.com/manual/core/timeseries-collections</a>
*/
public static class TimeSeriesOptions {
private final String timeField;
private @Nullable final String metaField;
private final GranularityDefinition granularity;
private TimeSeriesOptions(String timeField, @Nullable String metaField, GranularityDefinition granularity) {
Assert.hasText(timeField, "Time field must not be empty or null!");
this.timeField = timeField;
this.metaField = metaField;
this.granularity = granularity;
}
/**
* Create a new instance of {@link TimeSeriesOptions} using the given field as its {@literal timeField}. The one,
* that contains the date in each time series document. <br />
* {@link Field#name() Annotated fieldnames} will be considered during the mapping process.
*
* @param timeField must not be {@literal null}.
* @return new instance of {@link TimeSeriesOptions}.
*/
public static TimeSeriesOptions timeSeries(String timeField) {
return new TimeSeriesOptions(timeField, null, Granularity.DEFAULT);
}
/**
* Set the name of the field which contains metadata in each time series document. Should not be the {@literal id}
* nor {@link TimeSeriesOptions#timeSeries(String)} timeField} nor point to an {@literal array} or
* {@link java.util.Collection}. <br />
* {@link Field#name() Annotated fieldnames} will be considered during the mapping process.
*
* @param metaField must not be {@literal null}.
* @return new instance of {@link TimeSeriesOptions}.
*/
public TimeSeriesOptions metaField(String metaField) {
return new TimeSeriesOptions(timeField, metaField, granularity);
}
/**
* Select the {@link GranularityDefinition} parameter to define how data in the time series collection is organized.
* Select one that is closest to the time span between incoming measurements.
*
* @return new instance of {@link TimeSeriesOptions}.
* @see Granularity
*/
public TimeSeriesOptions granularity(GranularityDefinition granularity) {
return new TimeSeriesOptions(timeField, metaField, granularity);
}
/**
* @return never {@literal null}.
*/
public String getTimeField() {
return timeField;
}
/**
* @return can be {@literal null}. Might be an {@literal empty} {@link String} as well, so maybe check via
* {@link org.springframework.util.StringUtils#hasText(String)}.
*/
@Nullable
public String getMetaField() {
return metaField;
}
/**
* @return never {@literal null}.
*/
public GranularityDefinition getGranularity() {
return granularity;
}
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2021 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
*
* https://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;
/**
* Encryption algorithms supported by MongoDB Client Side Field Level Encryption.
*
* @author Christoph Strobl
* @since 3.3
*/
public final class EncryptionAlgorithms {
public static final String AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic";
public static final String AEAD_AES_256_CBC_HMAC_SHA_512_Random = "AEAD_AES_256_CBC_HMAC_SHA_512-Random";
}

View File

@@ -29,19 +29,23 @@ import org.springframework.data.mapping.PersistentEntity;
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.CollectionOptions.TimeSeriesOptions;
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.mapping.TimeSeries;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.timeseries.Granularity;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Common operations performed on an entity in the context of it's mapping metadata.
@@ -778,6 +782,24 @@ class EntityOperations {
* @return
*/
Optional<Collation> getCollation(Query query);
/**
* Derive the applicable {@link CollectionOptions} for the given type.
*
* @return never {@literal null}.
* @since 3.3
*/
CollectionOptions getCollectionOptions();
/**
* Map the fields of a given {@link TimeSeriesOptions} against the target domain type to consider potentially
* annotated field names.
*
* @param options must not be {@literal null}.
* @return never {@literal null}.
* @since 3.3
*/
TimeSeriesOptions mapTimeSeriesOptions(TimeSeriesOptions options);
}
/**
@@ -817,6 +839,16 @@ class EntityOperations {
return query.getCollation();
}
@Override
public CollectionOptions getCollectionOptions() {
return CollectionOptions.empty();
}
@Override
public TimeSeriesOptions mapTimeSeriesOptions(TimeSeriesOptions options) {
return options;
}
}
/**
@@ -854,6 +886,58 @@ class EntityOperations {
return Optional.ofNullable(entity.getCollation());
}
@Override
public CollectionOptions getCollectionOptions() {
CollectionOptions collectionOptions = CollectionOptions.empty();
if (entity.hasCollation()) {
collectionOptions = collectionOptions.collation(entity.getCollation());
}
if (entity.isAnnotationPresent(TimeSeries.class)) {
TimeSeries timeSeries = entity.getRequiredAnnotation(TimeSeries.class);
if (entity.getPersistentProperty(timeSeries.timeField()) == null) {
throw new MappingException(String.format("Time series field '%s' does not exist in type %s",
timeSeries.timeField(), entity.getName()));
}
TimeSeriesOptions options = TimeSeriesOptions.timeSeries(timeSeries.timeField());
if (StringUtils.hasText(timeSeries.metaField())) {
if (entity.getPersistentProperty(timeSeries.metaField()) == null) {
throw new MappingException(
String.format("Meta field '%s' does not exist in type %s", timeSeries.metaField(), entity.getName()));
}
options = options.metaField(timeSeries.metaField());
}
if (!Granularity.DEFAULT.equals(timeSeries.granularity())) {
options = options.granularity(timeSeries.granularity());
}
collectionOptions = collectionOptions.timeSeries(options);
}
return collectionOptions;
}
@Override
public TimeSeriesOptions mapTimeSeriesOptions(TimeSeriesOptions source) {
TimeSeriesOptions target = TimeSeriesOptions.timeSeries(mappedNameOrDefault(source.getTimeField()));
if (StringUtils.hasText(source.getMetaField())) {
target = target.metaField(mappedNameOrDefault(source.getMetaField()));
}
return target.granularity(source.getGranularity());
}
private String mappedNameOrDefault(String name) {
MongoPersistentProperty persistentProperty = entity.getPersistentProperty(name);
return persistentProperty != null ? persistentProperty.getFieldName() : name;
}
}
}

View File

@@ -125,7 +125,7 @@ public interface ExecutableFindOperation {
/**
* Get the number of matching elements.
* <p />
* <br />
* This method uses an {@link com.mongodb.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions) aggregation
* execution} even for empty {@link Query queries} which may have an impact on performance, but guarantees shard,
* session and transaction compliance. In case an inaccurate count satisfies the applications needs use

View File

@@ -89,7 +89,7 @@ public interface ExecutableUpdateOperation {
/**
* Trigger
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* <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

View File

@@ -17,7 +17,7 @@ package org.springframework.data.mongodb.core;
/**
* Options for
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>.
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace</a>.
* <br />
* Defaults to
* <dl>

View File

@@ -115,6 +115,10 @@ abstract class IndexConverters {
ops = ops.collation(fromDocument(indexOptions.get("collation", Document.class)));
}
if (indexOptions.containsKey("wildcardProjection")) {
ops.wildcardProjection(indexOptions.get("wildcardProjection", Document.class));
}
return ops;
};
}

View File

@@ -156,5 +156,14 @@ public class MappedDocument {
public List<ArrayFilter> getArrayFilters() {
return delegate.getArrayFilters();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.query.UpdateDefinition#hasArrayFilters()
*/
@Override
public boolean hasArrayFilters() {
return delegate.hasArrayFilters();
}
}
}

View File

@@ -20,13 +20,20 @@ import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.bson.Document;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.mapping.Encrypted;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ArrayJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ObjectJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.JsonSchemaObject;
import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type;
@@ -34,10 +41,12 @@ import org.springframework.data.mongodb.core.schema.JsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema.MongoJsonSchemaBuilder;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* {@link MongoJsonSchemaCreator} implementation using both {@link MongoConverter} and {@link MappingContext} to obtain
@@ -52,6 +61,7 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
private final MongoConverter converter;
private final MappingContext<MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
private final Predicate<JsonSchemaPropertyContext> filter;
/**
* Create a new instance of {@link MappingMongoJsonSchemaCreator}.
@@ -61,10 +71,24 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
@SuppressWarnings("unchecked")
MappingMongoJsonSchemaCreator(MongoConverter converter) {
this(converter, (MappingContext<MongoPersistentEntity<?>, MongoPersistentProperty>) converter.getMappingContext(),
(property) -> true);
}
@SuppressWarnings("unchecked")
MappingMongoJsonSchemaCreator(MongoConverter converter,
MappingContext<MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
Predicate<JsonSchemaPropertyContext> filter) {
Assert.notNull(converter, "Converter must not be null!");
this.converter = converter;
this.mappingContext = (MappingContext<MongoPersistentEntity<?>, MongoPersistentProperty>) converter
.getMappingContext();
this.mappingContext = mappingContext;
this.filter = filter;
}
@Override
public MongoJsonSchemaCreator filter(Predicate<JsonSchemaPropertyContext> filter) {
return new MappingMongoJsonSchemaCreator(converter, mappingContext, filter);
}
/*
@@ -77,11 +101,29 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
MongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(type);
MongoJsonSchemaBuilder schemaBuilder = MongoJsonSchema.builder();
{
Encrypted encrypted = entity.findAnnotation(Encrypted.class);
if (encrypted != null) {
Document encryptionMetadata = new Document();
Collection<Object> encryptionKeyIds = entity.getEncryptionKeyIds();
if (!CollectionUtils.isEmpty(encryptionKeyIds)) {
encryptionMetadata.append("keyId", encryptionKeyIds);
}
if (StringUtils.hasText(encrypted.algorithm())) {
encryptionMetadata.append("algorithm", encrypted.algorithm());
}
schemaBuilder.encryptionMetadata(encryptionMetadata);
}
}
List<JsonSchemaProperty> schemaProperties = computePropertiesForEntity(Collections.emptyList(), entity);
schemaBuilder.properties(schemaProperties.toArray(new JsonSchemaProperty[0]));
return schemaBuilder.build();
}
private List<JsonSchemaProperty> computePropertiesForEntity(List<MongoPersistentProperty> path,
@@ -93,6 +135,11 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
List<MongoPersistentProperty> currentPath = new ArrayList<>(path);
if (!filter.test(new PropertyContext(
currentPath.stream().map(PersistentProperty::getName).collect(Collectors.joining(".")), nested))) {
continue;
}
if (path.contains(nested)) { // cycle guard
schemaProperties.add(createSchemaProperty(computePropertyFieldName(CollectionUtils.lastElement(currentPath)),
Object.class, false));
@@ -114,21 +161,88 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
Class<?> rawTargetType = computeTargetType(property); // target type before conversion
Class<?> targetType = converter.getTypeMapper().getWriteTargetTypeFor(rawTargetType); // conversion target type
if (property.isEntity() && ObjectUtils.nullSafeEquals(rawTargetType, targetType)) {
if (!isCollection(property) && property.isEntity() && ObjectUtils.nullSafeEquals(rawTargetType, targetType)) {
return createObjectSchemaPropertyForEntity(path, property, required);
}
String fieldName = computePropertyFieldName(property);
if (property.isCollectionLike()) {
return createSchemaProperty(fieldName, targetType, required);
JsonSchemaProperty schemaProperty;
if (isCollection(property)) {
schemaProperty = createArraySchemaProperty(fieldName, property, required);
} else if (property.isMap()) {
return createSchemaProperty(fieldName, Type.objectType(), required);
schemaProperty = createSchemaProperty(fieldName, Type.objectType(), required);
} else if (ClassUtils.isAssignable(Enum.class, targetType)) {
return createEnumSchemaProperty(fieldName, targetType, required);
schemaProperty = createEnumSchemaProperty(fieldName, targetType, required);
} else {
schemaProperty = createSchemaProperty(fieldName, targetType, required);
}
return createSchemaProperty(fieldName, targetType, required);
return applyEncryptionDataIfNecessary(property, schemaProperty);
}
private JsonSchemaProperty createArraySchemaProperty(String fieldName, MongoPersistentProperty property,
boolean required) {
ArrayJsonSchemaProperty schemaProperty = JsonSchemaProperty.array(fieldName);
if (isSpecificType(property)) {
schemaProperty = potentiallyEnhanceArraySchemaProperty(property, schemaProperty);
}
return createPotentiallyRequiredSchemaProperty(schemaProperty, required);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private ArrayJsonSchemaProperty potentiallyEnhanceArraySchemaProperty(MongoPersistentProperty property,
ArrayJsonSchemaProperty schemaProperty) {
MongoPersistentEntity<?> persistentEntity = mappingContext
.getPersistentEntity(property.getTypeInformation().getRequiredComponentType());
if (persistentEntity != null) {
List<JsonSchemaProperty> nestedProperties = computePropertiesForEntity(Collections.emptyList(), persistentEntity);
if (nestedProperties.isEmpty()) {
return schemaProperty;
}
return schemaProperty
.items(JsonSchemaObject.object().properties(nestedProperties.toArray(new JsonSchemaProperty[0])));
}
if (ClassUtils.isAssignable(Enum.class, property.getActualType())) {
List<Object> possibleValues = getPossibleEnumValues((Class<Enum>) property.getActualType());
return schemaProperty
.items(createSchemaObject(computeTargetType(property.getActualType(), possibleValues), possibleValues));
}
return schemaProperty.items(JsonSchemaObject.of(property.getActualType()));
}
private boolean isSpecificType(MongoPersistentProperty property) {
return !ClassTypeInformation.OBJECT.equals(property.getTypeInformation().getActualType());
}
private JsonSchemaProperty applyEncryptionDataIfNecessary(MongoPersistentProperty property,
JsonSchemaProperty schemaProperty) {
Encrypted encrypted = property.findAnnotation(Encrypted.class);
if (encrypted == null) {
return schemaProperty;
}
EncryptedJsonSchemaProperty enc = new EncryptedJsonSchemaProperty(schemaProperty);
if (StringUtils.hasText(encrypted.algorithm())) {
enc = enc.algorithm(encrypted.algorithm());
}
if (!ObjectUtils.isEmpty(encrypted.keyId())) {
enc = enc.keys(property.getEncryptionKeyIds());
}
return enc;
}
private JsonSchemaProperty createObjectSchemaPropertyForEntity(List<MongoPersistentProperty> path,
@@ -142,15 +256,12 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
target.properties(nestedProperties.toArray(new JsonSchemaProperty[0])), required);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private JsonSchemaProperty createEnumSchemaProperty(String fieldName, Class<?> targetType, boolean required) {
List<Object> possibleValues = new ArrayList<>();
List<Object> possibleValues = getPossibleEnumValues((Class<Enum>) targetType);
for (Object enumValue : EnumSet.allOf((Class) targetType)) {
possibleValues.add(converter.convertToMongoType(enumValue));
}
targetType = possibleValues.isEmpty() ? targetType : possibleValues.iterator().next().getClass();
targetType = computeTargetType(targetType, possibleValues);
return createSchemaProperty(fieldName, targetType, required, possibleValues);
}
@@ -161,14 +272,20 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
JsonSchemaProperty createSchemaProperty(String fieldName, Object type, boolean required,
Collection<?> possibleValues) {
TypedJsonSchemaObject schemaObject = createSchemaObject(type, possibleValues);
return createPotentiallyRequiredSchemaProperty(JsonSchemaProperty.named(fieldName).with(schemaObject), required);
}
private TypedJsonSchemaObject createSchemaObject(Object type, Collection<?> possibleValues) {
TypedJsonSchemaObject schemaObject = type instanceof Type ? JsonSchemaObject.of(Type.class.cast(type))
: JsonSchemaObject.of(Class.class.cast(type));
if (!CollectionUtils.isEmpty(possibleValues)) {
schemaObject = schemaObject.possibleValues(possibleValues);
}
return createPotentiallyRequiredSchemaProperty(JsonSchemaProperty.named(fieldName).with(schemaObject), required);
return schemaObject;
}
private String computePropertyFieldName(PersistentProperty property) {
@@ -199,12 +316,53 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
return mongoProperty.getFieldType() != mongoProperty.getActualType() ? Object.class : mongoProperty.getFieldType();
}
static JsonSchemaProperty createPotentiallyRequiredSchemaProperty(JsonSchemaProperty property, boolean required) {
private static Class<?> computeTargetType(Class<?> fallback, List<Object> possibleValues) {
return possibleValues.isEmpty() ? fallback : possibleValues.iterator().next().getClass();
}
if (!required) {
private <E extends Enum<E>> List<Object> getPossibleEnumValues(Class<E> targetType) {
EnumSet<E> enumSet = EnumSet.allOf(targetType);
List<Object> possibleValues = new ArrayList<>(enumSet.size());
for (Object enumValue : enumSet) {
possibleValues.add(converter.convertToMongoType(enumValue));
}
return possibleValues;
}
private static boolean isCollection(MongoPersistentProperty property) {
return property.isCollectionLike() && !property.getType().equals(byte[].class);
}
static JsonSchemaProperty createPotentiallyRequiredSchemaProperty(JsonSchemaProperty property, boolean required) {
return required ? JsonSchemaProperty.required(property) : property;
}
class PropertyContext implements JsonSchemaPropertyContext {
private final String path;
private final MongoPersistentProperty property;
public PropertyContext(String path, MongoPersistentProperty property) {
this.path = path;
this.property = property;
}
@Override
public String getPath() {
return path;
}
@Override
public MongoPersistentProperty getProperty() {
return property;
}
return JsonSchemaProperty.required(property);
@Override
public <T> MongoPersistentEntity<T> resolveEntity(MongoPersistentProperty property) {
return (MongoPersistentEntity<T>) mappingContext.getPersistentEntity(property);
}
}
}

View File

@@ -36,6 +36,7 @@ import com.mongodb.MongoClientSettings.Builder;
import com.mongodb.ReadConcern;
import com.mongodb.ReadPreference;
import com.mongodb.ServerAddress;
import com.mongodb.ServerApi;
import com.mongodb.WriteConcern;
import com.mongodb.connection.ClusterConnectionMode;
import com.mongodb.connection.ClusterType;
@@ -113,6 +114,7 @@ public class MongoClientSettingsFactoryBean extends AbstractFactoryBean<MongoCli
// encryption and retry
private @Nullable AutoEncryptionSettings autoEncryptionSettings;
private @Nullable ServerApi serverApi;
/**
* @param socketConnectTimeoutMS in msec
@@ -395,6 +397,15 @@ public class MongoClientSettingsFactoryBean extends AbstractFactoryBean<MongoCli
this.autoEncryptionSettings = autoEncryptionSettings;
}
/**
* @param serverApi can be {@literal null}.
* @see MongoClientSettings.Builder#serverApi(ServerApi)
* @since 3.3
*/
public void setServerApi(@Nullable ServerApi serverApi) {
this.serverApi = serverApi;
}
@Override
public Class<?> getObjectType() {
return MongoClientSettings.class;
@@ -476,9 +487,11 @@ public class MongoClientSettingsFactoryBean extends AbstractFactoryBean<MongoCli
if (retryWrites != null) {
builder = builder.retryWrites(retryWrites);
}
if (uUidRepresentation != null) {
builder.uuidRepresentation(uUidRepresentation);
builder = builder.uuidRepresentation(uUidRepresentation);
}
if (serverApi != null) {
builder = builder.serverApi(serverApi);
}
return builder.build();

View File

@@ -33,7 +33,7 @@ import com.mongodb.client.MongoDatabase;
/**
* Common base class for usage with both {@link com.mongodb.client.MongoClients} defining common properties such as
* database name and exception translator.
* <p/>
* <br />
* Not intended to be used directly.
*
* @author Christoph Strobl

View File

@@ -20,7 +20,7 @@ import org.springframework.dao.support.PersistenceExceptionTranslator;
/**
* Common base class for usage with both {@link com.mongodb.client.MongoClients} defining common properties such as
* database name and exception translator.
* <p/>
* <br />
* Not intended to be used directly.
*
* @author Christoph Strobl

View File

@@ -15,7 +15,23 @@
*/
package org.springframework.data.mongodb.core;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
import org.springframework.data.mongodb.core.mapping.Encrypted;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
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.mapping.Unwrapped.Nullable;
import org.springframework.data.mongodb.core.schema.JsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
import org.springframework.util.Assert;
@@ -24,6 +40,7 @@ import org.springframework.util.Assert;
* following mapping rules.
* <p>
* <strong>Required Properties</strong>
* </p>
* <ul>
* <li>Properties of primitive type</li>
* </ul>
@@ -45,7 +62,8 @@ import org.springframework.util.Assert;
* {@link org.springframework.data.annotation.Id _id} properties using types that can be converted into
* {@link org.bson.types.ObjectId} like {@link String} will be mapped to {@code type : 'object'} unless there is more
* specific information available via the {@link org.springframework.data.mongodb.core.mapping.MongoId} annotation.
* </p>
* {@link Encrypted} properties will contain {@literal encrypt} information.
*
* @author Christoph Strobl
* @since 2.2
@@ -60,6 +78,88 @@ public interface MongoJsonSchemaCreator {
*/
MongoJsonSchema createSchemaFor(Class<?> type);
/**
* Filter matching {@link JsonSchemaProperty properties}.
*
* @param filter the {@link Predicate} to evaluate for inclusion. Must not be {@literal null}.
* @return new instance of {@link MongoJsonSchemaCreator}.
* @since 3.3
*/
MongoJsonSchemaCreator filter(Predicate<JsonSchemaPropertyContext> filter);
/**
* The context in which a specific {@link #getProperty()} is encountered during schema creation.
*
* @since 3.3
*/
interface JsonSchemaPropertyContext {
/**
* The path to a given field/property in dot notation.
*
* @return never {@literal null}.
*/
String getPath();
/**
* The current property.
*
* @return never {@literal null}.
*/
MongoPersistentProperty getProperty();
/**
* Obtain the {@link MongoPersistentEntity} for a given property.
*
* @param property must not be {@literal null}.
* @param <T>
* @return {@literal null} if the property is not an entity. It is nevertheless recommend to check
* {@link PersistentProperty#isEntity()} first.
*/
@Nullable
<T> MongoPersistentEntity<T> resolveEntity(MongoPersistentProperty property);
}
/**
* A filter {@link Predicate} that matches {@link Encrypted encrypted properties} and those having nested ones.
*
* @return new instance of {@link Predicate}.
* @since 3.3
*/
static Predicate<JsonSchemaPropertyContext> encryptedOnly() {
return new Predicate<JsonSchemaPropertyContext>() {
// cycle guard
private final Set<MongoPersistentProperty> seen = new HashSet<>();
@Override
public boolean test(JsonSchemaPropertyContext context) {
return extracted(context.getProperty(), context);
}
private boolean extracted(MongoPersistentProperty property, JsonSchemaPropertyContext context) {
if (property.isAnnotationPresent(Encrypted.class)) {
return true;
}
if (!property.isEntity() || seen.contains(property)) {
return false;
}
seen.add(property);
for (MongoPersistentProperty nested : context.resolveEntity(property)) {
if (extracted(nested, context)) {
return true;
}
}
return false;
}
};
}
/**
* Creates a new {@link MongoJsonSchemaCreator} that is aware of conversions applied by the given
* {@link MongoConverter}.
@@ -72,4 +172,41 @@ public interface MongoJsonSchemaCreator {
Assert.notNull(mongoConverter, "MongoConverter must not be null!");
return new MappingMongoJsonSchemaCreator(mongoConverter);
}
/**
* Creates a new {@link MongoJsonSchemaCreator} that is aware of type mappings and potential
* {@link org.springframework.data.spel.spi.EvaluationContextExtension extensions}.
*
* @param mappingContext must not be {@literal null}.
* @return new instance of {@link MongoJsonSchemaCreator}.
* @since 3.3
*/
static MongoJsonSchemaCreator create(MappingContext mappingContext) {
MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext);
converter.setCustomConversions(MongoCustomConversions.create(config -> {}));
converter.afterPropertiesSet();
return create(converter);
}
/**
* Creates a new {@link MongoJsonSchemaCreator} that does not consider potential extensions - suitable for testing. We
* recommend to use {@link #create(MappingContext)}.
*
* @return new instance of {@link MongoJsonSchemaCreator}.
* @since 3.3
*/
static MongoJsonSchemaCreator create() {
MongoMappingContext mappingContext = new MongoMappingContext();
mappingContext.setSimpleTypeHolder(MongoSimpleTypes.HOLDER);
mappingContext.afterPropertiesSet();
MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext);
converter.setCustomConversions(MongoCustomConversions.create(config -> {}));
converter.afterPropertiesSet();
return create(converter);
}
}

View File

@@ -58,7 +58,7 @@ import com.mongodb.client.result.UpdateResult;
* Interface that specifies a basic set of MongoDB operations. Implemented by {@link MongoTemplate}. Not often used but
* a useful option for extensibility and testability (as it can be easily mocked, stubbed, or be the target of a JDK
* proxy).
* <p/>
* <br />
* <strong>NOTE:</strong> Some operations cannot be executed within a MongoDB transaction. Please refer to the MongoDB
* specific documentation to learn more about <a href="https://docs.mongodb.com/manual/core/transactions/">Multi
* Document Transactions</a>.
@@ -125,7 +125,7 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Executes a {@link DbCallback} translating any exceptions as necessary.
* <p/>
* <br />
* Allows for returning a result object, that is a domain object or a collection of domain objects.
*
* @param action callback object that specifies the MongoDB actions to perform on the passed in DB instance. Must not
@@ -138,7 +138,7 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Executes the given {@link CollectionCallback} on the entity collection of the specified class.
* <p/>
* <br />
* Allows for returning a result object, that is a domain object or a collection of domain objects.
*
* @param entityClass class that determines the collection to use. Must not be {@literal null}.
@@ -151,7 +151,7 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Executes the given {@link CollectionCallback} on the collection of the given name.
* <p/>
* <br />
* Allows for returning a result object, that is a domain object or a collection of domain objects.
*
* @param collectionName the name of the collection that specifies which {@link MongoCollection} instance will be
@@ -176,7 +176,7 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Obtain a {@link ClientSession session} bound instance of {@link SessionScoped} binding the {@link ClientSession}
* provided by the given {@link Supplier} to each and every command issued against MongoDB.
* <p/>
* <br />
* <strong>Note:</strong> It is up to the caller to manage the {@link ClientSession} lifecycle. Use the
* {@link SessionScoped#execute(SessionCallback, Consumer)} hook to potentially close the {@link ClientSession}.
*
@@ -212,7 +212,7 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Obtain a {@link ClientSession} bound instance of {@link MongoOperations}.
* <p/>
* <br />
* <strong>Note:</strong> It is up to the caller to manage the {@link ClientSession} lifecycle.
*
* @param session must not be {@literal null}.
@@ -300,7 +300,7 @@ public interface MongoOperations extends FluentMongoOperations {
* is created on first interaction with the server. Collections can be explicitly created via
* {@link #createCollection(Class)}. Please make sure to check if the collection {@link #collectionExists(Class)
* exists} first.
* <p/>
* <br />
* Translate any exceptions as necessary.
*
* @param collectionName name of the collection. Must not be {@literal null}.
@@ -310,7 +310,7 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Check to see if a collection with a name indicated by the entity class exists.
* <p/>
* <br />
* Translate any exceptions as necessary.
*
* @param entityClass class that determines the name of the collection. Must not be {@literal null}.
@@ -320,7 +320,7 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Check to see if a collection with a given name exists.
* <p/>
* <br />
* Translate any exceptions as necessary.
*
* @param collectionName name of the collection. Must not be {@literal null}.
@@ -330,7 +330,7 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Drop the collection with the name indicated by the entity class.
* <p/>
* <br />
* Translate any exceptions as necessary.
*
* @param entityClass class that determines the collection to drop/delete. Must not be {@literal null}.
@@ -339,7 +339,7 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Drop the collection with the given name.
* <p/>
* <br />
* Translate any exceptions as necessary.
*
* @param collectionName name of the collection to drop/delete.
@@ -403,10 +403,10 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Query for a list of objects of type T from the collection used by the entity class.
* <p/>
* <br />
* The object is converted from the MongoDB native representation using an instance of {@see MongoConverter}. Unless
* configured otherwise, an instance of {@link MappingMongoConverter} will be used.
* <p/>
* <br />
* If your collection does not contain a homogeneous collection of types, this operation will not be an efficient way
* to map objects since the test for class type is done in the client and not on the server.
*
@@ -417,10 +417,10 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Query for a list of objects of type T from the specified collection.
* <p/>
* <br />
* The object is converted from the MongoDB native representation using an instance of {@see MongoConverter}. Unless
* configured otherwise, an instance of {@link MappingMongoConverter} will be used.
* <p/>
* <br />
* If your collection does not contain a homogeneous collection of types, this operation will not be an efficient way
* to map objects since the test for class type is done in the client and not on the server.
*
@@ -539,11 +539,11 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Execute an aggregation operation backed by a Mongo DB {@link com.mongodb.client.AggregateIterable}.
* <p/>
* <br />
* Returns a {@link CloseableIterator} that wraps the a Mongo DB {@link com.mongodb.client.AggregateIterable} that
* needs to be closed. The raw results will be mapped to the given entity class and are returned as stream. The name
* of the inputCollection is derived from the inputType of the aggregation.
* <p/>
* <br />
* Aggregation streaming can't be used with {@link AggregationOptions#isExplain() aggregation explain}. Enabling
* explanation mode will throw an {@link IllegalArgumentException}.
*
@@ -557,10 +557,10 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Execute an aggregation operation backed by a Mongo DB {@link com.mongodb.client.AggregateIterable}.
* <p/>
* <br />
* Returns a {@link CloseableIterator} that wraps the a Mongo DB {@link com.mongodb.client.AggregateIterable} that
* needs to be closed. The raw results will be mapped to the given entity class.
* <p/>
* <br />
* Aggregation streaming can't be used with {@link AggregationOptions#isExplain() aggregation explain}. Enabling
* explanation mode will throw an {@link IllegalArgumentException}.
*
@@ -576,10 +576,10 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Execute an aggregation operation backed by a Mongo DB {@link com.mongodb.client.AggregateIterable}.
* <p/>
* <br />
* Returns a {@link CloseableIterator} that wraps the a Mongo DB {@link com.mongodb.client.AggregateIterable} that
* needs to be closed. The raw results will be mapped to the given entity class.
* <p/>
* <br />
* Aggregation streaming can't be used with {@link AggregationOptions#isExplain() aggregation explain}. Enabling
* explanation mode will throw an {@link IllegalArgumentException}.
*
@@ -702,10 +702,10 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Map the results of an ad-hoc query on the collection for the entity class to a single instance of an object of the
* specified type.
* <p/>
* <br />
* The object is converted from the MongoDB native representation using an instance of {@see MongoConverter}. Unless
* configured otherwise, an instance of {@link MappingMongoConverter} will be used.
* <p/>
* <br />
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
@@ -720,10 +720,10 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Map the results of an ad-hoc query on the specified collection to a single instance of an object of the specified
* type.
* <p/>
* <br />
* The object is converted from the MongoDB native representation using an instance of {@see MongoConverter}. Unless
* configured otherwise, an instance of {@link MappingMongoConverter} will be used.
* <p/>
* <br />
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
@@ -768,10 +768,10 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Map the results of an ad-hoc query on the collection for the entity class to a List of the specified type.
* <p/>
* <br />
* The object is converted from the MongoDB native representation using an instance of {@see MongoConverter}. Unless
* configured otherwise, an instance of {@link MappingMongoConverter} will be used.
* <p/>
* <br />
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
@@ -784,10 +784,10 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Map the results of an ad-hoc query on the specified collection to a List of the specified type.
* <p/>
* <br />
* The object is converted from the MongoDB native representation using an instance of {@see MongoConverter}. Unless
* configured otherwise, an instance of {@link MappingMongoConverter} will be used.
* <p/>
* <br />
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
@@ -881,7 +881,7 @@ public interface MongoOperations extends FluentMongoOperations {
}
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify <a/>
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify </a>
* to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
@@ -897,7 +897,7 @@ public interface MongoOperations extends FluentMongoOperations {
<T> T findAndModify(Query query, UpdateDefinition update, Class<T> entityClass);
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify <a/>
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify </a>
* to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
@@ -914,7 +914,7 @@ public interface MongoOperations extends FluentMongoOperations {
<T> T findAndModify(Query query, UpdateDefinition update, Class<T> entityClass, String collectionName);
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify <a/>
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify </a>
* to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query} taking
* {@link FindAndModifyOptions} into account.
*
@@ -934,7 +934,7 @@ public interface MongoOperations extends FluentMongoOperations {
<T> T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class<T> entityClass);
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify <a/>
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify </a>
* to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query} taking
* {@link FindAndModifyOptions} into account.
*
@@ -957,7 +957,7 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* <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 />
@@ -977,7 +977,7 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* <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 />
@@ -997,7 +997,7 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* <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}.
@@ -1018,7 +1018,7 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* <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}.
@@ -1041,7 +1041,7 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* <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}.
@@ -1066,7 +1066,7 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* <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}.
@@ -1094,7 +1094,7 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* <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}.
@@ -1120,9 +1120,9 @@ public interface MongoOperations extends FluentMongoOperations {
* 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
* database.
* <p/>
* <br />
* The object is converted from the MongoDB native representation using an instance of {@see MongoConverter}.
* <p/>
* <br />
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
@@ -1137,10 +1137,10 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Map the results of an ad-hoc query on the specified collection 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 database.
* <p/>
* <br />
* The object is converted from the MongoDB native representation using an instance of {@see MongoConverter}. Unless
* configured otherwise, an instance of {@link MappingMongoConverter} will be used.
* <p/>
* <br />
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
@@ -1160,7 +1160,7 @@ public interface MongoOperations extends FluentMongoOperations {
* influence on the resulting number of documents found as those values are passed on to the server and potentially
* limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to
* count all matches.
* <p />
* <br />
* This method uses an
* {@link com.mongodb.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions)
* aggregation execution} even for empty {@link Query queries} which may have an impact on performance, but guarantees
@@ -1182,7 +1182,7 @@ public interface MongoOperations extends FluentMongoOperations {
* influence on the resulting number of documents found as those values are passed on to the server and potentially
* limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to
* count all matches.
* <p />
* <br />
* This method uses an
* {@link com.mongodb.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions)
* aggregation execution} even for empty {@link Query queries} which may have an impact on performance, but guarantees
@@ -1199,7 +1199,7 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Estimate the number of documents, in the collection {@link #getCollectionName(Class) identified by the given type},
* based on collection statistics.
* <p />
* <br />
* Please make sure to read the MongoDB reference documentation about limitations on eg. sharded cluster or inside
* transactions.
*
@@ -1215,7 +1215,7 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Estimate the number of documents in the given collection based on collection statistics.
* <p />
* <br />
* Please make sure to read the MongoDB reference documentation about limitations on eg. sharded cluster or inside
* transactions.
*
@@ -1232,7 +1232,7 @@ public interface MongoOperations extends FluentMongoOperations {
* influence on the resulting number of documents found as those values are passed on to the server and potentially
* limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to
* count all matches.
* <p />
* <br />
* This method uses an
* {@link com.mongodb.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions)
* aggregation execution} even for empty {@link Query queries} which may have an impact on performance, but guarantees
@@ -1249,17 +1249,17 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Insert the object into the collection for the entity type of the object to save.
* <p/>
* <br />
* The object is converted to the MongoDB native representation using an instance of {@see MongoConverter}.
* <p/>
* <br />
* If your object has an "Id' property, it will be set with the generated Id from MongoDB. If your Id property is a
* String then MongoDB ObjectId will be used to populate that string. Otherwise, the conversion from ObjectId to your
* property type will be handled by Spring's BeanWrapper class that leverages Type Conversion API. See
* <a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation" > Spring's
* Type Conversion"</a> for more details.
* <p/>
* <br />
* Insert is used to initially store the object into the database. To update an existing object use the save method.
* <p/>
* <br />
* The {@code objectToSave} must not be collection-like.
*
* @param objectToSave the object to store in the collection. Must not be {@literal null}.
@@ -1270,12 +1270,12 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Insert the object into the specified collection.
* <p/>
* <br />
* The object is converted to the MongoDB native representation using an instance of {@see MongoConverter}. Unless
* configured otherwise, an instance of {@link MappingMongoConverter} will be used.
* <p/>
* <br />
* Insert is used to initially store the object into the database. To update an existing object use the save method.
* <p/>
* <br />
* The {@code objectToSave} must not be collection-like.
*
* @param objectToSave the object to store in the collection. Must not be {@literal null}.
@@ -1315,16 +1315,16 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Save the object to the collection for the entity type of the object to save. This will perform an insert if the
* object is not already present, that is an 'upsert'.
* <p/>
* <br />
* The object is converted to the MongoDB native representation using an instance of {@see MongoConverter}. Unless
* configured otherwise, an instance of {@link MappingMongoConverter} will be used.
* <p/>
* <br />
* If your object has an "Id' property, it will be set with the generated Id from MongoDB. If your Id property is a
* String then MongoDB ObjectId will be used to populate that string. Otherwise, the conversion from ObjectId to your
* property type will be handled by Spring's BeanWrapper class that leverages Type Conversion API. See
* <a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation" > Spring's
* Type Conversion"</a> for more details.
* <p />
* <br />
* The {@code objectToSave} must not be collection-like.
*
* @param objectToSave the object to store in the collection. Must not be {@literal null}.
@@ -1336,16 +1336,15 @@ public interface MongoOperations extends FluentMongoOperations {
/**
* Save the object to the specified collection. This will perform an insert if the object is not already present, that
* is an 'upsert'.
* <p/>
* <br />
* The object is converted to the MongoDB native representation using an instance of {@see MongoConverter}. Unless
* configured otherwise, an instance of {@link MappingMongoConverter} will be used.
* <p/>
* <br />
* If your object has an "Id' property, it will be set with the generated Id from MongoDB. If your Id property is a
* String then MongoDB ObjectId will be used to populate that string. Otherwise, the conversion from ObjectId to your
* property type will be handled by Spring's BeanWrapper class that leverages Type Conversion API. See <a
* https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation">Spring's Type
* Conversion"</a> for more details.
* <p />
* property type will be handled by Spring's BeanWrapper class that leverages Type Conversion API.
* See <a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation">Spring's Type Conversion</a> for more details.
* <br />
* The {@code objectToSave} must not be collection-like.
*
* @param objectToSave the object to store in the collection. Must not be {@literal null}.

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2021 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
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
import com.mongodb.ServerApi;
import com.mongodb.ServerApi.Builder;
import com.mongodb.ServerApiVersion;
/**
* {@link FactoryBean} for creating {@link ServerApi} using the {@link ServerApi.Builder}.
*
* @author Christoph Strobl
* @since 3.3
*/
public class MongoServerApiFactoryBean implements FactoryBean<ServerApi> {
private String version;
private @Nullable Boolean deprecationErrors;
private @Nullable Boolean strict;
/**
* @param version the version string either as the enum name or the server version value.
* @see ServerApiVersion
*/
public void setVersion(String version) {
this.version = version;
}
/**
* @param deprecationErrors
* @see ServerApi.Builder#deprecationErrors(boolean)
*/
public void setDeprecationErrors(@Nullable Boolean deprecationErrors) {
this.deprecationErrors = deprecationErrors;
}
/**
* @param strict
* @see ServerApi.Builder#strict(boolean)
*/
public void setStrict(@Nullable Boolean strict) {
this.strict = strict;
}
@Nullable
@Override
public ServerApi getObject() throws Exception {
Builder builder = ServerApi.builder().version(version());
if (deprecationErrors != null) {
builder = builder.deprecationErrors(deprecationErrors);
}
if (strict != null) {
builder = builder.strict(strict);
}
return builder.build();
}
@Nullable
@Override
public Class<?> getObjectType() {
return ServerApi.class;
}
private ServerApiVersion version() {
try {
// lookup by name eg. 'V1'
return ObjectUtils.caseInsensitiveValueOf(ServerApiVersion.values(), version);
} catch (IllegalArgumentException e) {
// or just the version number, eg. just '1'
return ServerApiVersion.findByValue(version);
}
}
}

View File

@@ -28,6 +28,7 @@ import org.bson.Document;
import org.bson.conversions.Bson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
@@ -46,6 +47,7 @@ import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Metric;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.callback.EntityCallbacks;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.MongoDatabaseFactory;
@@ -97,12 +99,12 @@ import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.UpdateDefinition;
import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter;
import org.springframework.data.mongodb.core.timeseries.Granularity;
import org.springframework.data.mongodb.core.validation.Validator;
import org.springframework.data.mongodb.util.BsonUtils;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.util.CloseableIterator;
import org.springframework.data.util.Optionals;
import org.springframework.jca.cci.core.ConnectionCallback;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -336,7 +338,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
/**
* Set the {@link EntityCallbacks} instance to use when invoking
* {@link org.springframework.data.mapping.callback.EntityCallback callbacks} like the {@link BeforeSaveCallback}.
* <p />
* <br />
* Overrides potentially existing {@link EntityCallbacks}.
*
* @param entityCallbacks must not be {@literal null}.
@@ -596,7 +598,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
* @see org.springframework.data.mongodb.core.MongoOperations#createCollection(java.lang.Class)
*/
public <T> MongoCollection<Document> createCollection(Class<T> entityClass) {
return createCollection(entityClass, CollectionOptions.empty());
return createCollection(entityClass, operations.forType(entityClass).getCollectionOptions());
}
/*
@@ -972,7 +974,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
for (Document element : results) {
GeoResult<T> geoResult = callback.doWith(element);
aggregate = aggregate.add(new BigDecimal(geoResult.getDistance().getValue()));
aggregate = aggregate.add(BigDecimal.valueOf(geoResult.getDistance().getValue()));
result.add(geoResult);
}
@@ -1383,7 +1385,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
return source.isVersionedEntity() //
? doSaveVersioned(source, collectionName) //
: (T) doSave(collectionName, objectToSave, this.mongoConverter);
}
@SuppressWarnings("unchecked")
@@ -2434,6 +2435,20 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
co.validationOptions(options);
}
if (collectionOptions.containsKey("timeseries")) {
Document timeSeries = collectionOptions.get("timeseries", Document.class);
com.mongodb.client.model.TimeSeriesOptions options = new com.mongodb.client.model.TimeSeriesOptions(
timeSeries.getString("timeField"));
if (timeSeries.containsKey("metaField")) {
options.metaField(timeSeries.getString("metaField"));
}
if (timeSeries.containsKey("granularity")) {
options.granularity(TimeSeriesGranularity.valueOf(timeSeries.getString("granularity").toUpperCase()));
}
co.timeSeriesOptions(options);
}
db.createCollection(collectionName, co);
MongoCollection<Document> coll = db.getCollection(collectionName, Document.class);
@@ -2588,6 +2603,19 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
collectionOptions.getValidationOptions().ifPresent(it -> it.getValidator() //
.ifPresent(val -> doc.put("validator", getMappedValidator(val, targetType))));
collectionOptions.getTimeSeriesOptions().map(operations.forType(targetType)::mapTimeSeriesOptions)
.ifPresent(it -> {
Document timeseries = new Document("timeField", it.getTimeField());
if (StringUtils.hasText(it.getMetaField())) {
timeseries.append("metaField", it.getMetaField());
}
if (!Granularity.DEFAULT.equals(it.getGranularity())) {
timeseries.append("granularity", it.getGranularity().name().toLowerCase());
}
doc.put("timeseries", timeseries);
});
}
return doc;
@@ -2635,7 +2663,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
/**
* Map the results of an ad-hoc query on the default MongoDB collection to an object using the template's converter.
* The first document that matches the query is returned and also removed from the collection in the database.
* <p/>
* <br />
* The query document is specified as a standard Document and so is the fields specification.
*
* @param collectionName name of the collection to retrieve the objects from
@@ -2751,25 +2779,24 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
* Internal method using callbacks to do queries against the datastore that requires reading a single object from a
* collection of objects. It will take the following steps
* <ol>
* <li>Execute the given {@link ConnectionCallback} for a {@link Document}.</li>
* <li>Execute the given {@link CollectionCallback} for a {@link Document}.</li>
* <li>Apply the given {@link DocumentCallback} to each of the {@link Document}s to obtain the result.</li>
* <ol>
*
* @param <T>
* @param collectionCallback the callback to retrieve the {@link Document} with
* @param objectCallback the {@link DocumentCallback} to transform {@link Document}s into the actual domain type
* @param documentCallback the {@link DocumentCallback} to transform {@link Document}s into the actual domain type
* @param collectionName the collection to be queried
* @return
*/
@Nullable
private <T> T executeFindOneInternal(CollectionCallback<Document> collectionCallback,
DocumentCallback<T> objectCallback, String collectionName) {
DocumentCallback<T> documentCallback, String collectionName) {
try {
T result = objectCallback
.doWith(collectionCallback.doInCollection(getAndPrepareCollection(doGetDatabase(), collectionName)));
return result;
Document document = collectionCallback.doInCollection(getAndPrepareCollection(doGetDatabase(), collectionName));
return document != null ? documentCallback.doWith(document) : null;
} catch (RuntimeException e) {
throw potentiallyConvertRuntimeException(e, exceptionTranslator);
}
@@ -2779,7 +2806,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
* Internal method using callback to do queries against the datastore that requires reading a collection of objects.
* It will take the following steps
* <ol>
* <li>Execute the given {@link ConnectionCallback} for a {@link FindIterable}.</li>
* <li>Execute the given {@link CollectionCallback} for a {@link FindIterable}.</li>
* <li>Prepare that {@link FindIterable} with the given {@link CursorPreparer} (will be skipped if
* {@link CursorPreparer} is {@literal null}</li>
* <li>Iterate over the {@link FindIterable} and applies the given {@link DocumentCallback} to each of the
@@ -2789,36 +2816,27 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
* @param <T>
* @param collectionCallback the callback to retrieve the {@link FindIterable} with
* @param preparer the {@link CursorPreparer} to potentially modify the {@link FindIterable} before iterating over it
* @param objectCallback the {@link DocumentCallback} to transform {@link Document}s into the actual domain type
* @param documentCallback the {@link DocumentCallback} to transform {@link Document}s into the actual domain type
* @param collectionName the collection to be queried
* @return
*/
private <T> List<T> executeFindMultiInternal(CollectionCallback<FindIterable<Document>> collectionCallback,
CursorPreparer preparer, DocumentCallback<T> objectCallback, String collectionName) {
CursorPreparer preparer, DocumentCallback<T> documentCallback, String collectionName) {
try {
MongoCursor<Document> cursor = null;
try {
cursor = preparer
.initiateFind(getAndPrepareCollection(doGetDatabase(), collectionName), collectionCallback::doInCollection)
.iterator();
try (MongoCursor<Document> cursor = preparer
.initiateFind(getAndPrepareCollection(doGetDatabase(), collectionName), collectionCallback::doInCollection)
.iterator()) {
List<T> result = new ArrayList<>();
while (cursor.hasNext()) {
Document object = cursor.next();
result.add(objectCallback.doWith(object));
result.add(documentCallback.doWith(object));
}
return result;
} finally {
if (cursor != null) {
cursor.close();
}
}
} catch (RuntimeException e) {
throw potentiallyConvertRuntimeException(e, exceptionTranslator);
@@ -2828,23 +2846,12 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
private void executeQueryInternal(CollectionCallback<FindIterable<Document>> collectionCallback,
CursorPreparer preparer, DocumentCallbackHandler callbackHandler, String collectionName) {
try {
try (MongoCursor<Document> cursor = preparer
.initiateFind(getAndPrepareCollection(doGetDatabase(), collectionName), collectionCallback::doInCollection)
.iterator()) {
MongoCursor<Document> cursor = null;
try {
cursor = preparer
.initiateFind(getAndPrepareCollection(doGetDatabase(), collectionName), collectionCallback::doInCollection)
.iterator();
while (cursor.hasNext()) {
callbackHandler.processDocument(cursor.next());
}
} finally {
if (cursor != null) {
cursor.close();
}
while (cursor.hasNext()) {
callbackHandler.processDocument(cursor.next());
}
} catch (RuntimeException e) {
throw potentiallyConvertRuntimeException(e, exceptionTranslator);
@@ -3143,8 +3150,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
interface DocumentCallback<T> {
@Nullable
T doWith(@Nullable Document object);
T doWith(Document object);
}
/**
@@ -3168,22 +3174,19 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
this.collectionName = collectionName;
}
@Nullable
public T doWith(@Nullable Document document) {
public T doWith(Document document) {
T source = null;
maybeEmitEvent(new AfterLoadEvent<>(document, type, collectionName));
T entity = reader.read(type, document);
if (document != null) {
maybeEmitEvent(new AfterLoadEvent<>(document, type, collectionName));
source = reader.read(type, document);
if (entity == null) {
throw new MappingException(String.format("EntityReader %s returned null", reader));
}
if (source != null) {
maybeEmitEvent(new AfterConvertEvent<>(document, source, collectionName));
source = maybeCallAfterConvert(source, document, collectionName);
}
maybeEmitEvent(new AfterConvertEvent<>(document, entity, collectionName));
entity = maybeCallAfterConvert(entity, document, collectionName);
return source;
return entity;
}
}
@@ -3216,8 +3219,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
* @see org.springframework.data.mongodb.core.MongoTemplate.DocumentCallback#doWith(org.bson.Document)
*/
@SuppressWarnings("unchecked")
@Nullable
public T doWith(@Nullable Document document) {
public T doWith(Document document) {
if (document == null) {
return null;
@@ -3228,15 +3230,16 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
maybeEmitEvent(new AfterLoadEvent<>(document, targetType, collectionName));
Object source = reader.read(typeToRead, document);
Object result = targetType.isInterface() ? projectionFactory.createProjection(targetType, source) : source;
Object entity = reader.read(typeToRead, document);
if (result != null) {
maybeEmitEvent(new AfterConvertEvent<>(document, result, collectionName));
result = maybeCallAfterConvert(result, document, collectionName);
if (entity == null) {
throw new MappingException(String.format("EntityReader %s returned null", reader));
}
return (T) result;
Object result = targetType.isInterface() ? projectionFactory.createProjection(targetType, entity) : entity;
maybeEmitEvent(new AfterConvertEvent<>(document, result, collectionName));
return (T) maybeCallAfterConvert(result, document, collectionName);
}
}
@@ -3373,8 +3376,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
this.metric = metric;
}
@Nullable
public GeoResult<T> doWith(@Nullable Document object) {
public GeoResult<T> doWith(Document object) {
double distance = Double.NaN;
if (object.containsKey(distanceField)) {
@@ -3401,10 +3403,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
/**
* Creates a new {@link CloseableIterableCursorAdapter} backed by the given {@link MongoCollection}.
*
* @param cursor
* @param exceptionTranslator
* @param objectReadCallback
*/
CloseableIterableCursorAdapter(MongoIterable<Document> cursor, PersistenceExceptionTranslator exceptionTranslator,
DocumentCallback<T> objectReadCallback) {
@@ -3448,8 +3446,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
try {
Document item = cursor.next();
T converted = objectReadCallback.doWith(item);
return converted;
return objectReadCallback.doWith(item);
} catch (RuntimeException ex) {
throw potentiallyConvertRuntimeException(ex, exceptionTranslator);
}
@@ -3495,7 +3492,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
/**
* {@link MongoTemplate} extension bound to a specific {@link ClientSession} that is applied when interacting with the
* server through the driver API.
* <p />
* <br />
* The prepare steps for {@link MongoDatabase} and {@link MongoCollection} proxy the target and invoke the desired
* target method matching the actual arguments plus a {@link ClientSession}.
*

View File

@@ -613,7 +613,7 @@ class QueryOperations {
UpdateContext(MappedDocument update, boolean upsert) {
super(new BasicQuery(new Document(BsonUtils.asMap(update.getIdFilter()))));
super(new BasicQuery(BsonUtils.asDocument(update.getIdFilter())));
this.multi = false;
this.upsert = upsert;
this.mappedDocument = update;

View File

@@ -71,7 +71,7 @@ public interface ReactiveChangeStreamOperation {
/**
* Start listening to changes. The stream will not be completed unless the {@link org.reactivestreams.Subscription}
* is {@link org.reactivestreams.Subscription#cancel() canceled}.
* <p />
* <br />
* However, the stream may become dead, or invalid, if all watched collections, databases are dropped.
*/
Flux<ChangeStreamEvent<T>> listen();

View File

@@ -91,10 +91,10 @@ public interface ReactiveFindOperation {
* 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 />
* <br />
* 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 />
* <br />
* 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.
@@ -106,7 +106,7 @@ public interface ReactiveFindOperation {
/**
* Get the number of matching elements.
* <p />
* <br />
* This method uses an
* {@link com.mongodb.reactivestreams.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions)
* aggregation execution} even for empty {@link Query queries} which may have an impact on performance, but

View File

@@ -59,7 +59,7 @@ import com.mongodb.reactivestreams.client.MongoCollection;
* Implemented by {@link ReactiveMongoTemplate}. Not often used but a useful option for extensibility and testability
* (as it can be easily mocked, stubbed, or be the target of a JDK proxy). Command execution using
* {@link ReactiveMongoOperations} is deferred until subscriber subscribes to the {@link Publisher}.
* <p />
* <br />
* <strong>NOTE:</strong> Some operations cannot be executed within a MongoDB transaction. Please refer to the MongoDB
* specific documentation to learn more about <a href="https://docs.mongodb.com/manual/core/transactions/">Multi
* Document Transactions</a>.
@@ -121,7 +121,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Executes a {@link ReactiveDatabaseCallback} translating any exceptions as necessary.
* <p/>
* <br />
* Allows for returning a result object, that is a domain object or a collection of domain objects.
*
* @param action callback object that specifies the MongoDB actions to perform on the passed in DB instance. Must not
@@ -133,7 +133,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Executes the given {@link ReactiveCollectionCallback} on the entity collection of the specified class.
* <p/>
* <br />
* Allows for returning a result object, that is a domain object or a collection of domain objects.
*
* @param entityClass class that determines the collection to use. Must not be {@literal null}.
@@ -145,7 +145,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Executes the given {@link ReactiveCollectionCallback} on the collection of the given name.
* <p/>
* <br />
* Allows for returning a result object, that is a domain object or a collection of domain objects.
*
* @param collectionName the name of the collection that specifies which {@link MongoCollection} instance will be
@@ -159,7 +159,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Obtain a {@link ClientSession session} bound instance of {@link SessionScoped} binding the {@link ClientSession}
* provided by the given {@link Supplier} to each and every command issued against MongoDB.
* <p />
* <br />
* <strong>Note:</strong> It is up to the caller to manage the {@link ClientSession} lifecycle. Use
* {@link ReactiveSessionScoped#execute(ReactiveSessionCallback, Consumer)} to provide a hook for processing the
* {@link ClientSession} when done.
@@ -178,7 +178,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Obtain a {@link ClientSession session} bound instance of {@link SessionScoped} binding a new {@link ClientSession}
* with given {@literal sessionOptions} to each and every command issued against MongoDB.
* <p />
* <br />
* <strong>Note:</strong> It is up to the caller to manage the {@link ClientSession} lifecycle. Use
* {@link ReactiveSessionScoped#execute(ReactiveSessionCallback, Consumer)} to provide a hook for processing the
* {@link ClientSession} when done.
@@ -192,7 +192,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Obtain a {@link ClientSession session} bound instance of {@link ReactiveSessionScoped} binding the
* {@link ClientSession} provided by the given {@link Publisher} to each and every command issued against MongoDB.
* <p />
* <br />
* <strong>Note:</strong> It is up to the caller to manage the {@link ClientSession} lifecycle. Use
* {@link ReactiveSessionScoped#execute(ReactiveSessionCallback, Consumer)} to provide a hook for processing the
* {@link ClientSession} when done.
@@ -205,7 +205,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Obtain a {@link ClientSession} bound instance of {@link ReactiveMongoOperations}.
* <p />
* <br />
* <strong>Note:</strong> It is up to the caller to manage the {@link ClientSession} lifecycle.
*
* @param session must not be {@literal null}.
@@ -218,7 +218,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* Initiate a new {@link ClientSession} and obtain a {@link ClientSession session} bound instance of
* {@link ReactiveSessionScoped}. Starts the transaction and adds the {@link ClientSession} to each and every command
* issued against MongoDB.
* <p/>
* <br />
* Each {@link ReactiveSessionScoped#execute(ReactiveSessionCallback) execution} initiates a new managed transaction
* that is {@link ClientSession#commitTransaction() committed} on success. Transactions are
* {@link ClientSession#abortTransaction() rolled back} upon errors.
@@ -233,7 +233,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* Obtain a {@link ClientSession session} bound instance of {@link ReactiveSessionScoped}, start the transaction and
* bind the {@link ClientSession} provided by the given {@link Publisher} to each and every command issued against
* MongoDB.
* <p/>
* <br />
* Each {@link ReactiveSessionScoped#execute(ReactiveSessionCallback) execution} initiates a new managed transaction
* that is {@link ClientSession#commitTransaction() committed} on success. Transactions are
* {@link ClientSession#abortTransaction() rolled back} upon errors.
@@ -293,7 +293,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* created on first interaction with the server. Collections can be explicitly created via
* {@link #createCollection(Class)}. Please make sure to check if the collection {@link #collectionExists(Class)
* exists} first.
* <p/>
* <br />
* Translate any exceptions as necessary.
*
* @param collectionName name of the collection.
@@ -303,7 +303,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Check to see if a collection with a name indicated by the entity class exists.
* <p/>
* <br />
* Translate any exceptions as necessary.
*
* @param entityClass class that determines the name of the collection. Must not be {@literal null}.
@@ -313,7 +313,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Check to see if a collection with a given name exists.
* <p/>
* <br />
* Translate any exceptions as necessary.
*
* @param collectionName name of the collection. Must not be {@literal null}.
@@ -323,7 +323,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Drop the collection with the name indicated by the entity class.
* <p/>
* <br />
* Translate any exceptions as necessary.
*
* @param entityClass class that determines the collection to drop/delete. Must not be {@literal null}.
@@ -332,7 +332,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Drop the collection with the given name.
* <p/>
* <br />
* Translate any exceptions as necessary.
*
* @param collectionName name of the collection to drop/delete.
@@ -341,10 +341,10 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Query for a {@link Flux} of objects of type T from the collection used by the entity class.
* <p/>
* <br />
* The object is converted from the MongoDB native representation using an instance of {@see MongoConverter}. Unless
* configured otherwise, an instance of {@link MappingMongoConverter} will be used.
* <p/>
* <br />
* If your collection does not contain a homogeneous collection of types, this operation will not be an efficient way
* to map objects since the test for class type is done in the client and not on the server.
*
@@ -355,10 +355,10 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Query for a {@link Flux} of objects of type T from the specified collection.
* <p/>
* <br />
* The object is converted from the MongoDB native representation using an instance of {@see MongoConverter}. Unless
* configured otherwise, an instance of {@link MappingMongoConverter} will be used.
* <p/>
* <br />
* If your collection does not contain a homogeneous collection of types, this operation will not be an efficient way
* to map objects since the test for class type is done in the client and not on the server.
*
@@ -371,10 +371,10 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Map the results of an ad-hoc query on the collection for the entity class to a single instance of an object of the
* specified type.
* <p/>
* <br />
* The object is converted from the MongoDB native representation using an instance of {@see MongoConverter}. Unless
* configured otherwise, an instance of {@link MappingMongoConverter} will be used.
* <p/>
* <br />
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
@@ -388,10 +388,10 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Map the results of an ad-hoc query on the specified collection to a single instance of an object of the specified
* type.
* <p/>
* <br />
* The object is converted from the MongoDB native representation using an instance of {@see MongoConverter}. Unless
* configured otherwise, an instance of {@link MappingMongoConverter} will be used.
* <p/>
* <br />
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
@@ -435,10 +435,10 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Map the results of an ad-hoc query on the collection for the entity class to a {@link Flux} of the specified type.
* <p/>
* <br />
* The object is converted from the MongoDB native representation using an instance of {@see MongoConverter}. Unless
* configured otherwise, an instance of {@link MappingMongoConverter} will be used.
* <p/>
* <br />
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
@@ -451,10 +451,10 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Map the results of an ad-hoc query on the specified collection to a {@link Flux} of the specified type.
* <p/>
* <br />
* The object is converted from the MongoDB native representation using an instance of {@see MongoConverter}. Unless
* configured otherwise, an instance of {@link MappingMongoConverter} will be used.
* <p/>
* <br />
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
@@ -566,10 +566,10 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Execute an aggregation operation.
* <p/>
* <br />
* The raw results will be mapped to the given entity class and are returned as stream. The name of the
* inputCollection is derived from the {@link TypedAggregation#getInputType() aggregation input type}.
* <p/>
* <br />
* Aggregation streaming cannot be used with {@link AggregationOptions#isExplain() aggregation explain} nor with
* {@link AggregationOptions#getCursorBatchSize()}. Enabling explanation mode or setting batch size cause
* {@link IllegalArgumentException}.
@@ -584,10 +584,10 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Execute an aggregation operation.
* <p/>
* <br />
* The raw results will be mapped to the given {@code ouputType}. The name of the inputCollection is derived from the
* {@code inputType}.
* <p/>
* <br />
* Aggregation streaming cannot be used with {@link AggregationOptions#isExplain() aggregation explain} nor with
* {@link AggregationOptions#getCursorBatchSize()}. Enabling explanation mode or setting batch size cause
* {@link IllegalArgumentException}.
@@ -604,9 +604,9 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Execute an aggregation operation.
* <p/>
* <br />
* The raw results will be mapped to the given entity class.
* <p/>
* <br />
* Aggregation streaming cannot be used with {@link AggregationOptions#isExplain() aggregation explain} nor with
* {@link AggregationOptions#getCursorBatchSize()}. Enabling explanation mode or setting batch size cause
* {@link IllegalArgumentException}.
@@ -676,7 +676,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
<T> Flux<GeoResult<T>> geoNear(NearQuery near, Class<T> entityClass, String collectionName);
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify<a/>
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify</a>
* to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
@@ -691,7 +691,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
<T> Mono<T> findAndModify(Query query, UpdateDefinition update, Class<T> entityClass);
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify<a/>
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify</a>
* to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
@@ -707,7 +707,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
<T> Mono<T> findAndModify(Query query, UpdateDefinition update, Class<T> entityClass, String collectionName);
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify<a/>
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify</a>
* to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query} taking
* {@link FindAndModifyOptions} into account.
*
@@ -725,7 +725,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
<T> Mono<T> findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class<T> entityClass);
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify<a/>
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify</a>
* to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query} taking
* {@link FindAndModifyOptions} into account.
*
@@ -746,7 +746,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* <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 />
@@ -764,7 +764,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* <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 />
@@ -783,7 +783,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* <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}.
@@ -803,7 +803,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* <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}.
@@ -825,7 +825,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* <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}.
@@ -849,7 +849,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* <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}.
@@ -876,7 +876,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Triggers
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/">findOneAndReplace<a/>
* <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}.
@@ -902,9 +902,9 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* 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
* database.
* <p/>
* <br />
* The object is converted from the MongoDB native representation using an instance of {@see MongoConverter}.
* <p/>
* <br />
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
@@ -918,10 +918,10 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Map the results of an ad-hoc query on the specified collection 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 database.
* <p/>
* <br />
* The object is converted from the MongoDB native representation using an instance of {@see MongoConverter}. Unless
* configured otherwise, an instance of {@link MappingMongoConverter} will be used.
* <p/>
* <br />
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
@@ -940,7 +940,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* influence on the resulting number of documents found as those values are passed on to the server and potentially
* limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to
* count all matches.
* <p />
* <br />
* This method uses an
* {@link com.mongodb.reactivestreams.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions)
* aggregation execution} even for empty {@link Query queries} which may have an impact on performance, but guarantees
@@ -962,7 +962,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* influence on the resulting number of documents found as those values are passed on to the server and potentially
* limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to
* count all matches.
* <p />
* <br />
* This method uses an
* {@link com.mongodb.reactivestreams.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions)
* aggregation execution} even for empty {@link Query queries} which may have an impact on performance, but guarantees
@@ -983,7 +983,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* influence on the resulting number of documents found as those values are passed on to the server and potentially
* limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to
* count all matches.
* <p />
* <br />
* This method uses an
* {@link com.mongodb.reactivestreams.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions)
* aggregation execution} even for empty {@link Query queries} which may have an impact on performance, but guarantees
@@ -1001,7 +1001,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Estimate the number of documents, in the collection {@link #getCollectionName(Class) identified by the given type},
* based on collection statistics.
* <p />
* <br />
* Please make sure to read the MongoDB reference documentation about limitations on eg. sharded cluster or inside
* transactions.
*
@@ -1017,7 +1017,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Estimate the number of documents in the given collection based on collection statistics.
* <p />
* <br />
* Please make sure to read the MongoDB reference documentation about limitations on eg. sharded cluster or inside
* transactions.
*
@@ -1029,17 +1029,17 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Insert the object into the collection for the entity type of the object to save.
* <p/>
* <br />
* The object is converted to the MongoDB native representation using an instance of {@see MongoConverter}.
* <p/>
* <br />
* If your object has an "Id' property, it will be set with the generated Id from MongoDB. If your Id property is a
* String then MongoDB ObjectId will be used to populate that string. Otherwise, the conversion from ObjectId to your
* property type will be handled by Spring's BeanWrapper class that leverages Type Conversion API. See
* <a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation" > Spring's
* Type Conversion"</a> for more details.
* <p/>
* <br />
* Insert is used to initially store the object into the database. To update an existing object use the save method.
* <p />
* <br />
* The {@code objectToSave} must not be collection-like.
*
* @param objectToSave the object to store in the collection. Must not be {@literal null}.
@@ -1050,12 +1050,12 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Insert the object into the specified collection.
* <p/>
* <br />
* The object is converted to the MongoDB native representation using an instance of {@see MongoConverter}. Unless
* configured otherwise, an instance of {@link MappingMongoConverter} will be used.
* <p/>
* <br />
* Insert is used to initially store the object into the database. To update an existing object use the save method.
* <p />
* <br />
* The {@code objectToSave} must not be collection-like.
*
* @param objectToSave the object to store in the collection. Must not be {@literal null}.
@@ -1094,15 +1094,15 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Insert the object into the collection for the entity type of the object to save.
* <p/>
* <br />
* The object is converted to the MongoDB native representation using an instance of {@see MongoConverter}.
* <p/>
* <br />
* If your object has an "Id' property, it will be set with the generated Id from MongoDB. If your Id property is a
* String then MongoDB ObjectId will be used to populate that string. Otherwise, the conversion from ObjectId to your
* property type will be handled by Spring's BeanWrapper class that leverages Type Conversion API. See
* <a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation" > Spring's
* Type Conversion"</a> for more details.
* <p/>
* <br />
* 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}.
@@ -1140,16 +1140,16 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Save the object to the collection for the entity type of the object to save. This will perform an insert if the
* object is not already present, that is an 'upsert'.
* <p/>
* <br />
* The object is converted to the MongoDB native representation using an instance of {@see MongoConverter}. Unless
* configured otherwise, an instance of {@link MappingMongoConverter} will be used.
* <p/>
* <br />
* If your object has an "Id' property, it will be set with the generated Id from MongoDB. If your Id property is a
* String then MongoDB ObjectId will be used to populate that string. Otherwise, the conversion from ObjectId to your
* property type will be handled by Spring's BeanWrapper class that leverages Type Conversion API. See
* <a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation" > Spring's
* Type Conversion"</a> for more details.
* <p />
* <br />
* The {@code objectToSave} must not be collection-like.
*
* @param objectToSave the object to store in the collection. Must not be {@literal null}.
@@ -1161,15 +1161,14 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Save the object to the specified collection. This will perform an insert if the object is not already present, that
* is an 'upsert'.
* <p/>
* <br />
* The object is converted to the MongoDB native representation using an instance of {@see MongoConverter}. Unless
* configured otherwise, an instance of {@link MappingMongoConverter} will be used.
* <p/>
* <br />
* If your object has an "Id' property, it will be set with the generated Id from MongoDB. If your Id property is a
* String then MongoDB ObjectId will be used to populate that string. Otherwise, the conversion from ObjectId to your
* property type will be handled by Spring's BeanWrapper class that leverages Type Conversion API. See <a
* https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation">Spring's Type
* Conversion"</a> for more details.
* property type will be handled by Spring's BeanWrapper class that leverages Type Conversion API.
* See <a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation">Spring's Type Conversion</a> for more details.
*
* @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}.
@@ -1181,15 +1180,14 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Save the object to the collection for the entity type of the object to save. This will perform an insert if the
* object is not already present, that is an 'upsert'.
* <p/>
* <br />
* The object is converted to the MongoDB native representation using an instance of {@see MongoConverter}. Unless
* configured otherwise, an instance of {@link MappingMongoConverter} will be used.
* <p/>
* <br />
* If your object has an "Id' property, it will be set with the generated Id from MongoDB. If your Id property is a
* String then MongoDB ObjectId will be used to populate that string. Otherwise, the conversion from ObjectId to your
* property type will be handled by Spring's BeanWrapper class that leverages Type Conversion API. See
* <a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation" > Spring's
* Type Conversion"</a> for more details.
* property type will be handled by Spring's BeanWrapper class that leverages Type Conversion API.
* See <a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation"> Spring's Type Conversion</a> for more details.
*
* @param objectToSave the object to store in the collection. Must not be {@literal null}.
* @return the saved object.
@@ -1199,17 +1197,16 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
/**
* Save the object to the specified collection. This will perform an insert if the object is not already present, that
* is an 'upsert'.
* <p/>
* <br />
* The object is converted to the MongoDB native representation using an instance of {@see MongoConverter}. Unless
* configured otherwise, an instance of {@link MappingMongoConverter} will be used.
* <p/>
* <br />
* If your object has an "Id' property, it will be set with the generated Id from MongoDB. If your Id property is a
* String then MongoDB ObjectId will be used to populate that string. Otherwise, the conversion from ObjectId to your
* property type will be handled by Spring's BeanWrapper class that leverages Type Conversion API. See <a
* https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation">Spring's Type
* Conversion"</a> for more details.
* property type will be handled by Spring's BeanWrapper class that leverages Type Conversion API.
* See <a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation">Spring's Type Conversion</a> for more details.
*
* @param objectToSave the object to store in the collection. Must not be {@literal null}.
* @param objectToSave the object to store in the collReactiveMongoOperationsection. 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.
*/
@@ -1481,10 +1478,10 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* type. The stream uses a {@link com.mongodb.CursorType#TailableAwait tailable} cursor that may be an infinite
* stream. The stream will not be completed unless the {@link org.reactivestreams.Subscription} is
* {@link Subscription#cancel() canceled}.
* <p/>
* <br />
* The object is converted from the MongoDB native representation using an instance of {@see MongoConverter}. Unless
* configured otherwise, an instance of {@link MappingMongoConverter} will be used.
* <p/>
* <br />
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
@@ -1500,10 +1497,10 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* type. The stream uses a {@link com.mongodb.CursorType#TailableAwait tailable} cursor that may be an infinite
* stream. The stream will not be completed unless the {@link org.reactivestreams.Subscription} is
* {@link Subscription#cancel() canceled}.
* <p/>
* <br />
* The object is converted from the MongoDB native representation using an instance of {@see MongoConverter}. Unless
* configured otherwise, an instance of {@link MappingMongoConverter} will be used.
* <p/>
* <br />
* The query is specified as a {@link Query} which can be created either using the {@link BasicQuery} or the more
* feature rich {@link Query}.
*
@@ -1520,10 +1517,10 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* 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 />
* <br />
* The {@link ChangeStreamEvent#getBody()} is mapped to the {@literal resultType} while the
* {@link ChangeStreamEvent#getRaw()} contains the unmodified payload.
* <p />
* <br />
* Use {@link ChangeStreamOptions} to set arguments like {@link ChangeStreamOptions#getResumeToken() the resumseToken}
* for resuming change streams.
*
@@ -1544,10 +1541,10 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* 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 />
* <br />
* The {@link ChangeStreamEvent#getBody()} is mapped to the {@literal resultType} while the
* {@link ChangeStreamEvent#getRaw()} contains the unmodified payload.
* <p />
* <br />
* Use {@link ChangeStreamOptions} to set arguments like {@link ChangeStreamOptions#getResumeToken() the resumseToken}
* for resuming change streams.
*
@@ -1569,10 +1566,10 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* 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 />
* <br />
* The {@link ChangeStreamEvent#getBody()} is mapped to the {@literal resultType} while the
* {@link ChangeStreamEvent#getRaw()} contains the unmodified payload.
* <p />
* <br />
* Use {@link ChangeStreamOptions} to set arguments like {@link ChangeStreamOptions#getResumeToken() the resumseToken}
* for resuming change streams.
*

View File

@@ -22,7 +22,15 @@ import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -36,6 +44,7 @@ import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
@@ -51,6 +60,7 @@ import org.springframework.data.convert.EntityReader;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.Metric;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.callback.ReactiveEntityCallbacks;
import org.springframework.data.mapping.context.MappingContext;
@@ -100,6 +110,7 @@ import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.UpdateDefinition;
import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter;
import org.springframework.data.mongodb.core.timeseries.Granularity;
import org.springframework.data.mongodb.core.validation.Validator;
import org.springframework.data.mongodb.util.BsonUtils;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
@@ -351,7 +362,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
* Set the {@link ReactiveEntityCallbacks} instance to use when invoking
* {@link org.springframework.data.mapping.callback.EntityCallback callbacks} like the
* {@link ReactiveBeforeSaveCallback}.
* <p />
* <br />
* Overrides potentially existing {@link ReactiveEntityCallbacks}.
*
* @param entityCallbacks must not be {@literal null}.
@@ -655,7 +666,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
* @see org.springframework.data.mongodb.core.ReactiveMongoOperations#createCollection(java.lang.Class)
*/
public <T> Mono<MongoCollection<Document>> createCollection(Class<T> entityClass) {
return createCollection(entityClass, CollectionOptions.empty());
return createCollection(entityClass, operations.forType(entityClass).getCollectionOptions());
}
/*
@@ -964,7 +975,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return doAggregate(aggregation, collectionName, null, outputType);
}
protected <O> Flux<O> doAggregate(Aggregation aggregation, String collectionName, @Nullable Class<?> inputType, Class<O> outputType) {
protected <O> Flux<O> doAggregate(Aggregation aggregation, String collectionName, @Nullable Class<?> inputType,
Class<O> outputType) {
Assert.notNull(aggregation, "Aggregation pipeline must not be null!");
Assert.hasText(collectionName, "Collection name must not be null or empty!");
@@ -976,19 +988,18 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
AggregationDefinition ctx = queryOperations.createAggregation(aggregation, inputType);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Streaming aggregation: {} in collection {}", serializeToJsonSafely(ctx.getAggregationPipeline()), collectionName);
LOGGER.debug("Streaming aggregation: {} in collection {}", serializeToJsonSafely(ctx.getAggregationPipeline()),
collectionName);
}
ReadDocumentCallback<O> readCallback = new ReadDocumentCallback<>(mongoConverter, outputType, collectionName);
return execute(collectionName,
collection -> aggregateAndMap(collection, ctx.getAggregationPipeline(), ctx.isOutOrMerge(), options,
readCallback,
ctx.getInputType()));
return execute(collectionName, collection -> aggregateAndMap(collection, ctx.getAggregationPipeline(),
ctx.isOutOrMerge(), options, readCallback, ctx.getInputType()));
}
private <O> Flux<O> aggregateAndMap(MongoCollection<Document> collection, List<Document> pipeline,
boolean isOutOrMerge,
AggregationOptions options, ReadDocumentCallback<O> readCallback, @Nullable Class<?> inputType) {
boolean isOutOrMerge, AggregationOptions options, ReadDocumentCallback<O> readCallback,
@Nullable Class<?> inputType) {
AggregatePublisher<Document> cursor = collection.aggregate(pipeline, Document.class)
.allowDiskUse(options.isAllowDiskUse());
@@ -2495,6 +2506,20 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
result.validationOptions(validationOptions);
});
collectionOptions.getTimeSeriesOptions().map(operations.forType(entityType)::mapTimeSeriesOptions).ifPresent(it -> {
TimeSeriesOptions options = new TimeSeriesOptions(it.getTimeField());
if (StringUtils.hasText(it.getMetaField())) {
options.metaField(it.getMetaField());
}
if (!Granularity.DEFAULT.equals(it.getGranularity())) {
options.granularity(TimeSeriesGranularity.valueOf(it.getGranularity().name().toUpperCase()));
}
result.timeSeriesOptions(options);
});
return result;
}
@@ -2512,7 +2537,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
/**
* Map the results of an ad-hoc query on the default MongoDB collection to an object using the template's converter.
* The first document that matches the query is returned and also removed from the collection in the database.
* <p/>
* <br />
* The query document is specified as a standard Document and so is the fields specification.
*
* @param collectionName name of the collection to retrieve the objects from
@@ -3152,13 +3177,14 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
maybeEmitEvent(new AfterLoadEvent<>(document, type, collectionName));
T source = reader.read(type, document);
if (source != null) {
maybeEmitEvent(new AfterConvertEvent<>(document, source, collectionName));
return maybeCallAfterConvert(source, document, collectionName);
T entity = reader.read(type, document);
if (entity == null) {
throw new MappingException(String.format("EntityReader %s returned null", reader));
}
return Mono.empty();
maybeEmitEvent(new AfterConvertEvent<>(document, entity, collectionName));
return maybeCallAfterConvert(entity, document, collectionName);
}
}
@@ -3196,16 +3222,17 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
maybeEmitEvent(new AfterLoadEvent<>(document, typeToRead, collectionName));
Object source = reader.read(typeToRead, document);
Object result = targetType.isInterface() ? projectionFactory.createProjection(targetType, source) : source;
Object entity = reader.read(typeToRead, document);
T castEntity = (T) result;
if (castEntity != null) {
maybeEmitEvent(new AfterConvertEvent<>(document, castEntity, collectionName));
return maybeCallAfterConvert(castEntity, document, collectionName);
if (entity == null) {
throw new MappingException(String.format("EntityReader %s returned null", reader));
}
return Mono.empty();
Object result = targetType.isInterface() ? projectionFactory.createProjection(targetType, entity) : entity;
T castEntity = (T) result;
maybeEmitEvent(new AfterConvertEvent<>(document, castEntity, collectionName));
return maybeCallAfterConvert(castEntity, document, collectionName);
}
}
@@ -3363,7 +3390,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
/**
* {@link MongoTemplate} extension bound to a specific {@link ClientSession} that is applied when interacting with the
* server through the driver API.
* <p />
* <br />
* The prepare steps for {@link MongoDatabase} and {@link MongoCollection} proxy the target and invoke the desired
* target method matching the actual arguments plus a {@link ClientSession}.
*

View File

@@ -32,7 +32,7 @@ public interface ReactiveSessionCallback<T> {
/**
* Execute operations against a MongoDB instance via session bound {@link ReactiveMongoOperations}. The session is
* inferred directly into the operation so that no further interaction is necessary.
* <p />
* <br />
* Please note that only Spring Data-specific abstractions like {@link ReactiveMongoOperations#find(Query, Class)} and
* others are enhanced with the {@link com.mongodb.session.ClientSession}. When obtaining plain MongoDB gateway
* objects like {@link com.mongodb.reactivestreams.client.MongoCollection} or

View File

@@ -33,7 +33,7 @@ public interface ReactiveSessionScoped {
/**
* Executes the given {@link ReactiveSessionCallback} within the {@link com.mongodb.session.ClientSession}.
* <p/>
* <br />
* It is up to the caller to make sure the {@link com.mongodb.session.ClientSession} is {@link ClientSession#close()
* closed} when done.
*
@@ -47,7 +47,7 @@ public interface ReactiveSessionScoped {
/**
* Executes the given {@link ReactiveSessionCallback} within the {@link com.mongodb.session.ClientSession}.
* <p/>
* <br />
* It is up to the caller to make sure the {@link com.mongodb.session.ClientSession} is {@link ClientSession#close()
* closed} when done.
*

View File

@@ -23,7 +23,7 @@ import org.springframework.lang.Nullable;
/**
* Script operations on {@link com.mongodb.DB} level. Allows interaction with server side JavaScript functions.
* Script operations on {@link com.mongodb.client.MongoDatabase} level. Allows interaction with server side JavaScript functions.
*
* @author Christoph Strobl
* @author Oliver Gierke
@@ -72,10 +72,10 @@ public interface ScriptOperations {
Object call(String scriptName, Object... args);
/**
* Checks {@link DB} for existence of {@link ServerSideJavaScript} with given name.
* Checks {@link com.mongodb.client.MongoDatabase} for existence of {@literal ServerSideJavaScript} with given name.
*
* @param scriptName must not be {@literal null} or empty.
* @return false if no {@link ServerSideJavaScript} with given name exists.
* @return false if no {@literal ServerSideJavaScript} with given name exists.
*/
boolean exists(String scriptName);

View File

@@ -31,7 +31,7 @@ public interface SessionCallback<T> {
/**
* Execute operations against a MongoDB instance via session bound {@link MongoOperations}. The session is inferred
* directly into the operation so that no further interaction is necessary.
* <p />
* <br />
* Please note that only Spring Data-specific abstractions like {@link MongoOperations#find(Query, Class)} and others
* are enhanced with the {@link com.mongodb.session.ClientSession}. When obtaining plain MongoDB gateway objects like
* {@link com.mongodb.client.MongoCollection} or {@link com.mongodb.client.MongoDatabase} via eg.

View File

@@ -23,7 +23,7 @@ import com.mongodb.client.ClientSession;
/**
* Gateway interface to execute {@link ClientSession} bound operations against MongoDB via a {@link SessionCallback}.
* <p />
* <br />
* The very same bound {@link ClientSession} is used for all invocations of {@code execute} on the instance.
*
* @author Christoph Strobl
@@ -34,7 +34,7 @@ public interface SessionScoped {
/**
* Executes the given {@link SessionCallback} within the {@link com.mongodb.session.ClientSession}.
* <p/>
* <br />
* It is up to the caller to make sure the {@link com.mongodb.session.ClientSession} is {@link ClientSession#close()
* closed} when done.
*
@@ -49,7 +49,7 @@ public interface SessionScoped {
/**
* Executes the given {@link SessionCallback} within the {@link com.mongodb.session.ClientSession}.
* <p/>
* <br />
* It is up to the caller to make sure the {@link com.mongodb.session.ClientSession} is {@link ClientSession#close()
* closed} when done.
*

View File

@@ -142,11 +142,118 @@ public class AccumulatorOperators {
return usesFieldRef() ? StdDevSamp.stdDevSampOf(fieldReference) : StdDevSamp.stdDevSampOf(expression);
}
/**
* Creates new {@link AggregationExpression} that uses the previous input (field/expression) and the value of the
* given field to calculate the population covariance of the two.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link CovariancePop}.
* @since 3.3
*/
public CovariancePop covariancePop(String fieldReference) {
return covariancePop().and(fieldReference);
}
/**
* Creates new {@link AggregationExpression} that uses the previous input (field/expression) and the result of the
* given {@link AggregationExpression expression} to calculate the population covariance of the two.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link CovariancePop}.
* @since 3.3
*/
public CovariancePop covariancePop(AggregationExpression expression) {
return covariancePop().and(expression);
}
private CovariancePop covariancePop() {
return usesFieldRef() ? CovariancePop.covariancePopOf(fieldReference) : CovariancePop.covariancePopOf(expression);
}
/**
* Creates new {@link AggregationExpression} that uses the previous input (field/expression) and the value of the
* given field to calculate the sample covariance of the two.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link CovariancePop}.
* @since 3.3
*/
public CovarianceSamp covarianceSamp(String fieldReference) {
return covarianceSamp().and(fieldReference);
}
/**
* Creates new {@link AggregationExpression} that uses the previous input (field/expression) and the result of the
* given {@link AggregationExpression expression} to calculate the sample covariance of the two.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link CovariancePop}.
* @since 3.3
*/
public CovarianceSamp covarianceSamp(AggregationExpression expression) {
return covarianceSamp().and(expression);
}
private CovarianceSamp covarianceSamp() {
return usesFieldRef() ? CovarianceSamp.covarianceSampOf(fieldReference)
: CovarianceSamp.covarianceSampOf(expression);
}
/**
* Creates new {@link ExpMovingAvgBuilder} that to build {@link AggregationExpression expMovingAvg} that calculates
* the exponential moving average of numeric values
*
* @return new instance of {@link ExpMovingAvg}.
* @since 3.3
*/
public ExpMovingAvgBuilder expMovingAvg() {
ExpMovingAvg expMovingAvg = usesFieldRef() ? ExpMovingAvg.expMovingAvgOf(fieldReference)
: ExpMovingAvg.expMovingAvgOf(expression);
return new ExpMovingAvgBuilder() {
@Override
public ExpMovingAvg historicalDocuments(int numberOfHistoricalDocuments) {
return expMovingAvg.n(numberOfHistoricalDocuments);
}
@Override
public ExpMovingAvg alpha(double exponentialDecayValue) {
return expMovingAvg.alpha(exponentialDecayValue);
}
};
}
private boolean usesFieldRef() {
return fieldReference != null;
}
}
/**
* Builder for {@link ExpMovingAvg}.
*
* @since 3.3
*/
public interface ExpMovingAvgBuilder {
/**
* Define the number of historical documents with significant mathematical weight.
*
* @param numberOfHistoricalDocuments
* @return new instance of {@link ExpMovingAvg}.
*/
ExpMovingAvg historicalDocuments(int numberOfHistoricalDocuments);
/**
* Define the exponential decay value.
*
* @param exponentialDecayValue
* @return new instance of {@link ExpMovingAvg}.
*/
ExpMovingAvg alpha(double exponentialDecayValue);
}
/**
* {@link AggregationExpression} for {@code $sum}.
*
@@ -658,4 +765,185 @@ public class AccumulatorOperators {
return super.toDocument(value, context);
}
}
/**
* {@link AggregationExpression} for {@code $covariancePop}.
*
* @author Christoph Strobl
* @since 3.3
*/
public static class CovariancePop extends AbstractAggregationExpression {
private CovariancePop(Object value) {
super(value);
}
/**
* Creates new {@link CovariancePop}.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link CovariancePop}.
*/
public static CovariancePop covariancePopOf(String fieldReference) {
Assert.notNull(fieldReference, "FieldReference must not be null!");
return new CovariancePop(asFields(fieldReference));
}
/**
* Creates new {@link CovariancePop}.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link CovariancePop}.
*/
public static CovariancePop covariancePopOf(AggregationExpression expression) {
return new CovariancePop(Collections.singletonList(expression));
}
/**
* Creates new {@link CovariancePop} with all previously added arguments appending the given one.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link CovariancePop}.
*/
public CovariancePop and(String fieldReference) {
return new CovariancePop(append(asFields(fieldReference)));
}
/**
* Creates new {@link CovariancePop} with all previously added arguments appending the given one.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link CovariancePop}.
*/
public CovariancePop and(AggregationExpression expression) {
return new CovariancePop(append(expression));
}
@Override
protected String getMongoMethod() {
return "$covariancePop";
}
}
/**
* {@link AggregationExpression} for {@code $covarianceSamp}.
*
* @author Christoph Strobl
* @since 3.3
*/
public static class CovarianceSamp extends AbstractAggregationExpression {
private CovarianceSamp(Object value) {
super(value);
}
/**
* Creates new {@link CovarianceSamp}.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link CovarianceSamp}.
*/
public static CovarianceSamp covarianceSampOf(String fieldReference) {
Assert.notNull(fieldReference, "FieldReference must not be null!");
return new CovarianceSamp(asFields(fieldReference));
}
/**
* Creates new {@link CovarianceSamp}.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link CovarianceSamp}.
*/
public static CovarianceSamp covarianceSampOf(AggregationExpression expression) {
return new CovarianceSamp(Collections.singletonList(expression));
}
/**
* Creates new {@link CovarianceSamp} with all previously added arguments appending the given one.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link CovarianceSamp}.
*/
public CovarianceSamp and(String fieldReference) {
return new CovarianceSamp(append(asFields(fieldReference)));
}
/**
* Creates new {@link CovarianceSamp} with all previously added arguments appending the given one.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link CovarianceSamp}.
*/
public CovarianceSamp and(AggregationExpression expression) {
return new CovarianceSamp(append(expression));
}
@Override
protected String getMongoMethod() {
return "$covarianceSamp";
}
}
/**
* {@link ExpMovingAvg} calculates the exponential moving average of numeric values.
*
* @author Christoph Strobl
* @since 3.3
*/
public static class ExpMovingAvg extends AbstractAggregationExpression {
private ExpMovingAvg(Object value) {
super(value);
}
/**
* Create a new {@link ExpMovingAvg} by defining the field holding the value to be used as input.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link ExpMovingAvg}.
*/
public static ExpMovingAvg expMovingAvgOf(String fieldReference) {
return new ExpMovingAvg(Collections.singletonMap("input", Fields.field(fieldReference)));
}
/**
* Create a new {@link ExpMovingAvg} by defining the {@link AggregationExpression expression} to compute the value
* to be used as input.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link ExpMovingAvg}.
*/
public static ExpMovingAvg expMovingAvgOf(AggregationExpression expression) {
return new ExpMovingAvg(Collections.singletonMap("input", expression));
}
/**
* Define the number of historical documents with significant mathematical weight. <br />
* Specify either {@link #n(int) N} or {@link #alpha(double) aplha}. Not both!
*
* @param numberOfHistoricalDocuments
* @return new instance of {@link ExpMovingAvg}.
*/
public ExpMovingAvg n/*umber of historical documents*/(int numberOfHistoricalDocuments) {
return new ExpMovingAvg(append("N", numberOfHistoricalDocuments));
}
/**
* Define the exponential decay value. <br />
* Specify either {@link #alpha(double) aplha} or {@link #n(int) N}. Not both!
*
* @param exponentialDecayValue
* @return new instance of {@link ExpMovingAvg}.
*/
public ExpMovingAvg alpha(double exponentialDecayValue) {
return new ExpMovingAvg(append("alpha", exponentialDecayValue));
}
@Override
protected String getMongoMethod() {
return "$expMovingAvg";
}
}
}

View File

@@ -201,4 +201,5 @@ public class AddFieldsOperation extends DocumentEnhancingOperation {
AddFieldsOperationBuilder withValueOfExpression(String operation, Object... values);
}
}
}

View File

@@ -227,7 +227,7 @@ public class Aggregation {
/**
* Obtain an {@link AddFieldsOperationBuilder builder} instance to create a new {@link AddFieldsOperation}.
* <p/>
* <br />
* Starting in version 4.2, MongoDB adds a new aggregation pipeline stage {@link AggregationUpdate#set $set} that is
* an alias for {@code $addFields}.
*
@@ -499,6 +499,17 @@ public class Aggregation {
return new MatchOperation(criteria);
}
/**
* Creates a new {@link MatchOperation} using the given {@link AggregationExpression}.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link MatchOperation}.
* @since 3.3
*/
public static MatchOperation match(AggregationExpression expression) {
return new MatchOperation(expression);
}
/**
* Creates a new {@link GeoNearOperation} instance from the given {@link NearQuery} and the {@code distanceField}. The
* {@code distanceField} defines output field that contains the calculated distance.
@@ -715,7 +726,7 @@ public class Aggregation {
/**
* Converts this {@link Aggregation} specification to a {@link Document}.
* <p/>
* <br />
* MongoDB requires as of 3.6 cursor-based aggregation. Use {@link #toPipeline(AggregationOperationContext)} to render
* an aggregation pipeline.
*

View File

@@ -24,15 +24,15 @@ import org.springframework.util.Assert;
* expression</a>. <br />
* <br />
* <strong>Samples:</strong> <br />
* <code>
* <pre>
* <code>
* // { $and: [ { $gt: [ "$qty", 100 ] }, { $lt: [ "$qty", 250 ] } ] }
* expressionOf("qty > 100 && qty < 250);
*
* // { $cond : { if : { $gte : [ "$a", 42 ]}, then : "answer", else : "no-answer" } }
* expressionOf("cond(a >= 42, 'answer', 'no-answer')");
* </pre>
* </code>
* </pre>
*
* @author Christoph Strobl
* @author Mark Paluch

View File

@@ -71,8 +71,7 @@ import org.springframework.util.Assert;
*
* @author Christoph Strobl
* @author Mark Paluch
* @see <a href=
* "https://docs.mongodb.com/manual/reference/method/db.collection.update/#update-with-aggregation-pipeline">MongoDB
* @see <a href="https://docs.mongodb.com/manual/reference/method/db.collection.update/#update-with-aggregation-pipeline">MongoDB
* Reference Documentation</a>
* @since 3.0
*/

View File

@@ -28,8 +28,7 @@ import org.springframework.util.Assert;
* We recommend to use the static factory method {@link Aggregation#bucketAuto(String, int)} instead of creating
* instances of this class directly.
*
* @see <a href=
* "https://docs.mongodb.org/manual/reference/aggregation/bucketAuto/">https://docs.mongodb.org/manual/reference/aggregation/bucketAuto/</a>
* @see <a href="https://docs.mongodb.org/manual/reference/aggregation/bucketAuto/">https://docs.mongodb.org/manual/reference/aggregation/bucketAuto/</a>
* @see BucketOperationSupport
* @author Mark Paluch
* @author Christoph Strobl
@@ -248,8 +247,7 @@ public class BucketAutoOperation extends BucketOperationSupport<BucketAutoOperat
/**
* Supported MongoDB granularities.
*
* @see <a
* href="https://docs.mongodb.com/manual/reference/operator/aggregation/bucketAuto/#granularity>https://docs.mongodb.com/manual/reference/operator/aggregation/bucketAuto/#granularity</a>
* @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/bucketAuto/#granularity">https://docs.mongodb.com/manual/reference/operator/aggregation/bucketAuto/#granularity</a>
* @author Mark Paluch
*/
public enum Granularities implements Granularity {

View File

@@ -31,8 +31,7 @@ import org.springframework.util.Assert;
* We recommend to use the static factory method {@link Aggregation#bucket(String)} instead of creating instances of
* this class directly.
*
* @see <a href=
* "https://docs.mongodb.org/manual/reference/aggregation/bucket/">https://docs.mongodb.org/manual/reference/aggregation/bucket/</a>
* @see <a href="https://docs.mongodb.org/manual/reference/aggregation/bucket/">https://docs.mongodb.org/manual/reference/aggregation/bucket/</a>
* @see BucketOperationSupport
* @author Mark Paluch
* @since 1.10

View File

@@ -17,6 +17,7 @@ package org.springframework.data.mongodb.core.aggregation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -235,7 +236,7 @@ public class ConditionalOperators {
*
* @author Mark Paluch
* @see <a href=
* "https://docs.mongodb.com/manual/reference/operator/aggregation/ifNull/">https://docs.mongodb.com/manual/reference/operator/aggregation/ifNull/</a>
* "https://docs.mongodb.com/manual/reference/operator/aggregation/ifNull/">https://docs.mongodb.com/manual/reference/operator/aggregation/ifNull/</a>
*/
public static class IfNull implements AggregationExpression {
@@ -251,7 +252,8 @@ public class ConditionalOperators {
/**
* Creates new {@link IfNull}.
*
* @param fieldReference the field to check for a {@literal null} value, field reference must not be {@literal null}.
* @param fieldReference the field to check for a {@literal null} value, field reference must not be
* {@literal null}.
* @return never {@literal null}.
*/
public static ThenBuilder ifNull(String fieldReference) {
@@ -264,7 +266,7 @@ public class ConditionalOperators {
* Creates new {@link IfNull}.
*
* @param expression the expression to check for a {@literal null} value, field reference must not be
* {@literal null}.
* {@literal null}.
* @return never {@literal null}.
*/
public static ThenBuilder ifNull(AggregationExpression expression) {
@@ -282,19 +284,29 @@ public class ConditionalOperators {
List<Object> list = new ArrayList<Object>();
if (condition instanceof Field) {
list.add(context.getReference((Field) condition).toString());
} else if (condition instanceof AggregationExpression) {
list.add(((AggregationExpression) condition).toDocument(context));
if (condition instanceof Collection) {
for (Object val : ((Collection) this.condition)) {
list.add(mapCondition(val, context));
}
} else {
list.add(condition);
list.add(mapCondition(condition, context));
}
list.add(resolve(value, context));
return new Document("$ifNull", list);
}
private Object mapCondition(Object condition, AggregationOperationContext context) {
if (condition instanceof Field) {
return context.getReference((Field) condition).toString();
} else if (condition instanceof AggregationExpression) {
return ((AggregationExpression) condition).toDocument(context);
} else {
return condition;
}
}
private Object resolve(Object value, AggregationOperationContext context) {
if (value instanceof Field) {
@@ -315,28 +327,48 @@ public class ConditionalOperators {
/**
* @param fieldReference the field to check for a {@literal null} value, field reference must not be
* {@literal null}.
* {@literal null}.
* @return the {@link ThenBuilder}
*/
ThenBuilder ifNull(String fieldReference);
/**
* @param expression the expression to check for a {@literal null} value, field name must not be {@literal null}
* or empty.
* @return the {@link ThenBuilder}
* or empty.
* @return the {@link ThenBuilder}.
*/
ThenBuilder ifNull(AggregationExpression expression);
}
/**
* @author Christoph Strobl
* @since 3.3
*/
public interface OrBuilder {
/**
* @param fieldReference the field to check for a {@literal null} value, field reference must not be
* {@literal null}.
* @return the {@link ThenBuilder}
*/
ThenBuilder orIfNull(String fieldReference);
/**
* @param expression the expression to check for a {@literal null} value,
* @return the {@link ThenBuilder}.
*/
ThenBuilder orIfNull(AggregationExpression expression);
}
/**
* @author Mark Paluch
*/
public interface ThenBuilder {
public interface ThenBuilder extends OrBuilder {
/**
* @param value the value to be used if the {@code $ifNull} condition evaluates {@literal true}. Can be a
* {@link Document}, a value that is supported by MongoDB or a value that can be converted to a MongoDB
* representation but must not be {@literal null}.
* {@link Document}, a value that is supported by MongoDB or a value that can be converted to a MongoDB
* representation but must not be {@literal null}.
* @return new instance of {@link IfNull}.
*/
IfNull then(Object value);
@@ -361,9 +393,10 @@ public class ConditionalOperators {
*/
static final class IfNullOperatorBuilder implements IfNullBuilder, ThenBuilder {
private @Nullable Object condition;
private @Nullable List<Object> conditions;
private IfNullOperatorBuilder() {
conditions = new ArrayList<>();
}
/**
@@ -381,7 +414,7 @@ public class ConditionalOperators {
public ThenBuilder ifNull(String fieldReference) {
Assert.hasText(fieldReference, "FieldReference name must not be null or empty!");
this.condition = Fields.field(fieldReference);
this.conditions.add(Fields.field(fieldReference));
return this;
}
@@ -392,15 +425,25 @@ public class ConditionalOperators {
public ThenBuilder ifNull(AggregationExpression expression) {
Assert.notNull(expression, "AggregationExpression name must not be null or empty!");
this.condition = expression;
this.conditions.add(expression);
return this;
}
@Override
public ThenBuilder orIfNull(String fieldReference) {
return ifNull(fieldReference);
}
@Override
public ThenBuilder orIfNull(AggregationExpression expression) {
return ifNull(expression);
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.ConditionalOperators.IfNull.ThenBuilder#then(java.lang.Object)
*/
public IfNull then(Object value) {
return new IfNull(condition, value);
return new IfNull(conditions, value);
}
/* (non-Javadoc)
@@ -409,7 +452,7 @@ public class ConditionalOperators {
public IfNull thenValueOf(String fieldReference) {
Assert.notNull(fieldReference, "FieldReference must not be null!");
return new IfNull(condition, Fields.field(fieldReference));
return new IfNull(conditions, Fields.field(fieldReference));
}
/* (non-Javadoc)
@@ -418,7 +461,7 @@ public class ConditionalOperators {
public IfNull thenValueOf(AggregationExpression expression) {
Assert.notNull(expression, "Expression must not be null!");
return new IfNull(condition, expression);
return new IfNull(conditions, expression);
}
}
}
@@ -458,7 +501,7 @@ public class ConditionalOperators {
public static Switch switchCases(List<CaseOperator> conditions) {
Assert.notNull(conditions, "Conditions must not be null!");
return new Switch(Collections.<String, Object>singletonMap("branches", new ArrayList<CaseOperator>(conditions)));
return new Switch(Collections.<String, Object> singletonMap("branches", new ArrayList<CaseOperator>(conditions)));
}
/**
@@ -545,7 +588,7 @@ public class ConditionalOperators {
* @author Mark Paluch
* @author Christoph Strobl
* @see <a href=
* "https://docs.mongodb.com/manual/reference/operator/aggregation/cond/">https://docs.mongodb.com/manual/reference/operator/aggregation/cond/</a>
* "https://docs.mongodb.com/manual/reference/operator/aggregation/cond/">https://docs.mongodb.com/manual/reference/operator/aggregation/cond/</a>
*/
public static class Cond implements AggregationExpression {
@@ -806,8 +849,8 @@ public class ConditionalOperators {
/**
* @param value the value to be used if the condition evaluates {@literal true}. Can be a {@link Document}, a
* value that is supported by MongoDB or a value that can be converted to a MongoDB representation but
* must not be {@literal null}.
* value that is supported by MongoDB or a value that can be converted to a MongoDB representation but
* must not be {@literal null}.
* @return the {@link OtherwiseBuilder}
*/
OtherwiseBuilder then(Object value);
@@ -832,8 +875,8 @@ public class ConditionalOperators {
/**
* @param value the value to be used if the condition evaluates {@literal false}. Can be a {@link Document}, a
* value that is supported by MongoDB or a value that can be converted to a MongoDB representation but
* must not be {@literal null}.
* value that is supported by MongoDB or a value that can be converted to a MongoDB representation but
* must not be {@literal null}.
* @return the {@link Cond}
*/
Cond otherwise(Object value);
@@ -861,8 +904,7 @@ public class ConditionalOperators {
private @Nullable Object condition;
private @Nullable Object thenValue;
private ConditionalExpressionBuilder() {
}
private ConditionalExpressionBuilder() {}
/**
* Creates a new builder for {@link Cond}.

View File

@@ -231,6 +231,17 @@ public class ConvertOperators {
return ToString.toString(valueObject());
}
/**
* {@link AggregationExpression} for {@code $degreesToRadians} that converts an input value measured in degrees to
* radians.
*
* @return new instance of {@link DegreesToRadians}.
* @since 3.3
*/
public DegreesToRadians convertDegreesToRadians() {
return DegreesToRadians.degreesToRadians(valueObject());
}
private Convert createConvert() {
return usesFieldRef() ? Convert.convertValueOf(fieldReference) : Convert.convertValueOf(expression);
}
@@ -317,9 +328,9 @@ public class ConvertOperators {
* <dt>1</dt>
* <dd>double</dd>
* <dt>2</dt>
* <dd>string</li>
* <dd>string</dd>
* <dt>7</dt>
* <dd>objectId</li>
* <dd>objectId</dd>
* <dt>8</dt>
* <dd>bool</dd>
* <dt>9</dt>
@@ -692,4 +703,52 @@ public class ConvertOperators {
return "$toString";
}
}
/**
* {@link AggregationExpression} for {@code $degreesToRadians} that converts an input value measured in degrees to radians.
*
* @author Christoph Strobl
* @since 3.3
*/
public static class DegreesToRadians extends AbstractAggregationExpression {
private DegreesToRadians(Object value) {
super(value);
}
/**
* Create a new instance of {@link DegreesToRadians} that converts the value of the given field, measured in degrees, to radians.
*
* @param fieldName must not be {@literal null}.
* @return new instance of {@link DegreesToRadians}.
*/
public static DegreesToRadians degreesToRadiansOf(String fieldName) {
return degreesToRadians(Fields.field(fieldName));
}
/**
* Create a new instance of {@link DegreesToRadians} that converts the result of the given {@link AggregationExpression expression}, measured in degrees, to radians.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link DegreesToRadians}.
*/
public static DegreesToRadians degreesToRadiansOf(AggregationExpression expression) {
return degreesToRadians(expression);
}
/**
* Create a new instance of {@link DegreesToRadians} that converts the given value, measured in degrees, to radians.
*
* @param value must not be {@literal null}.
* @return new instance of {@link DegreesToRadians}.
*/
public static DegreesToRadians degreesToRadians(Object value) {
return new DegreesToRadians(value);
}
@Override
protected String getMongoMethod() {
return "$degreesToRadians";
}
}
}

View File

@@ -24,8 +24,7 @@ import org.springframework.util.Assert;
* We recommend to use the static factory method {@link Aggregation#count()} instead of creating instances of this class
* directly.
*
* @see <a href=
* "https://docs.mongodb.com/manual/reference/operator/aggregation/count/#pipe._S_count">https://docs.mongodb.com/manual/reference/operator/aggregation/count/</a>
* @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/count/#pipe._S_count">https://docs.mongodb.com/manual/reference/operator/aggregation/count/</a>
* @author Mark Paluch
* @since 1.10
*/

View File

@@ -15,9 +15,16 @@
*/
package org.springframework.data.mongodb.core.aggregation;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -45,6 +52,19 @@ public class DateOperators {
return new DateOperatorFactory(fieldReference);
}
/**
* Take the date referenced by given {@literal fieldReference}.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link DateOperatorFactory}.
* @since 3.3
*/
public static DateOperatorFactory zonedDateOf(String fieldReference, Timezone timezone) {
Assert.notNull(fieldReference, "FieldReference must not be null!");
return new DateOperatorFactory(fieldReference).withTimezone(timezone);
}
/**
* Take the date resulting from the given {@link AggregationExpression}.
*
@@ -57,9 +77,22 @@ public class DateOperators {
return new DateOperatorFactory(expression);
}
/**
* Take the date resulting from the given {@link AggregationExpression}.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link DateOperatorFactory}.
* @since 3.3
*/
public static DateOperatorFactory zonedDateOf(AggregationExpression expression, Timezone timezone) {
Assert.notNull(expression, "Expression must not be null!");
return new DateOperatorFactory(expression).withTimezone(timezone);
}
/**
* Take the given value as date.
* <p/>
* <br />
* This can be one of:
* <ul>
* <li>{@link java.util.Date}</li>
@@ -109,7 +142,7 @@ public class DateOperators {
* Timezone represents a MongoDB timezone abstraction which can be represented with a timezone ID or offset as a
* {@link String}. Also accepts a {@link AggregationExpression} or {@link Field} that resolves to a {@link String} of
* either Olson Timezone Identifier or a UTC Offset.<br />
* <table valign="top">
* <table>
* <tr>
* <th>Format</th>
* <th>Example</th>
@@ -130,6 +163,7 @@ public class DateOperators {
* <strong>NOTE: </strong>Support for timezones in aggregations Requires MongoDB 3.6 or later.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.1
*/
public static class Timezone {
@@ -156,7 +190,7 @@ public class DateOperators {
* representing an Olson Timezone Identifier or UTC Offset.
*
* @param value the plain timezone {@link String}, a {@link Field} holding the timezone or an
* {@link AggregationExpression} resulting in the timezone.
* {@link AggregationExpression} resulting in the timezone.
* @return new instance of {@link Timezone}.
*/
public static Timezone valueOf(Object value) {
@@ -165,6 +199,61 @@ public class DateOperators {
return new Timezone(value);
}
/**
* Create a {@link Timezone} for the given {@link TimeZone} rendering the offset as UTC offset.
*
* @param timeZone {@link TimeZone} rendering the offset as UTC offset.
* @return new instance of {@link Timezone}.
* @since 3.3
*/
public static Timezone fromOffset(TimeZone timeZone) {
Assert.notNull(timeZone, "TimeZone must not be null!");
return fromOffset(
ZoneOffset.ofTotalSeconds(Math.toIntExact(TimeUnit.MILLISECONDS.toSeconds(timeZone.getRawOffset()))));
}
/**
* Create a {@link Timezone} for the given {@link ZoneOffset} rendering the offset as UTC offset.
*
* @param offset {@link ZoneOffset} rendering the offset as UTC offset.
* @return new instance of {@link Timezone}.
* @since 3.3
*/
public static Timezone fromOffset(ZoneOffset offset) {
Assert.notNull(offset, "ZoneOffset must not be null!");
return new Timezone(offset.toString());
}
/**
* Create a {@link Timezone} for the given {@link TimeZone} rendering the offset as UTC offset.
*
* @param timeZone {@link Timezone} rendering the offset as zone identifier.
* @return new instance of {@link Timezone}.
* @since 3.3
*/
public static Timezone fromZone(TimeZone timeZone) {
Assert.notNull(timeZone, "TimeZone must not be null!");
return valueOf(timeZone.getID());
}
/**
* Create a {@link Timezone} for the given {@link java.time.ZoneId} rendering the offset as UTC offset.
*
* @param zoneId {@link ZoneId} rendering the offset as zone identifier.
* @return new instance of {@link Timezone}.
* @since 3.3
*/
public static Timezone fromZone(ZoneId zoneId) {
Assert.notNull(zoneId, "ZoneId must not be null!");
return new Timezone(zoneId.toString());
}
/**
* Create a {@link Timezone} for the {@link Field} reference holding the Olson Timezone Identifier or UTC Offset.
*
@@ -185,6 +274,11 @@ public class DateOperators {
public static Timezone ofExpression(AggregationExpression expression) {
return valueOf(expression);
}
@Nullable
Object getValue() {
return value;
}
}
/**
@@ -240,7 +334,7 @@ public class DateOperators {
/**
* Creates new {@link DateOperatorFactory} for given {@code value} that resolves to a Date.
* <p/>
* <br />
* <ul>
* <li>{@link java.util.Date}</li>
* <li>{@link java.util.Calendar}</li>
@@ -274,6 +368,89 @@ public class DateOperators {
return new DateOperatorFactory(fieldReference, expression, dateValue, timezone);
}
/**
* Creates new {@link AggregationExpression} that adds the value of the given {@link AggregationExpression
* expression} (in {@literal units}).
*
* @param expression must not be {@literal null}.
* @param unit the unit of measure. Must not be {@literal null}.
* @return new instance of {@link DateAdd}. @since 3.3
*/
public DateAdd addValueOf(AggregationExpression expression, String unit) {
return applyTimezone(DateAdd.addValueOf(expression, unit).toDate(dateReference()), timezone);
}
/**
* Creates new {@link AggregationExpression} that adds the value of the given {@link AggregationExpression
* expression} (in {@literal units}).
*
* @param expression must not be {@literal null}.
* @param unit the unit of measure. Must not be {@literal null}.
* @return new instance of {@link DateAdd}. @since 3.3
*/
public DateAdd addValueOf(AggregationExpression expression, TemporalUnit unit) {
Assert.notNull(unit, "TemporalUnit must not be null");
return applyTimezone(DateAdd.addValueOf(expression, unit.name().toLowerCase(Locale.ROOT)).toDate(dateReference()),
timezone);
}
/**
* Creates new {@link AggregationExpression} that adds the value stored at the given {@literal field} (in
* {@literal units}).
*
* @param fieldReference must not be {@literal null}.
* @param unit the unit of measure. Must not be {@literal null}.
* @return new instance of {@link DateAdd}. @since 3.3
*/
public DateAdd addValueOf(String fieldReference, String unit) {
return applyTimezone(DateAdd.addValueOf(fieldReference, unit).toDate(dateReference()), timezone);
}
/**
* Creates new {@link AggregationExpression} that adds the value stored at the given {@literal field} (in
* {@literal units}).
*
* @param fieldReference must not be {@literal null}.
* @param unit the unit of measure. Must not be {@literal null}.
* @return new instance of {@link DateAdd}. @since 3.3
*/
public DateAdd addValueOf(String fieldReference, TemporalUnit unit) {
Assert.notNull(unit, "TemporalUnit must not be null");
return applyTimezone(
DateAdd.addValueOf(fieldReference, unit.name().toLowerCase(Locale.ROOT)).toDate(dateReference()), timezone);
}
/**
* Creates new {@link AggregationExpression} that adds the given value (in {@literal units}).
*
* @param value must not be {@literal null}.
* @param unit the unit of measure. Must not be {@literal null}.
* @return
* @since 3.3 new instance of {@link DateAdd}.
*/
public DateAdd add(Object value, String unit) {
return applyTimezone(DateAdd.addValue(value, unit).toDate(dateReference()), timezone);
}
/**
* Creates new {@link AggregationExpression} that adds the given value (in {@literal units}).
*
* @param value must not be {@literal null}.
* @param unit the unit of measure. Must not be {@literal null}.
* @return
* @since 3.3 new instance of {@link DateAdd}.
*/
public DateAdd add(Object value, TemporalUnit unit) {
Assert.notNull(unit, "TemporalUnit must not be null");
return applyTimezone(DateAdd.addValue(value, unit.name().toLowerCase(Locale.ROOT)).toDate(dateReference()),
timezone);
}
/**
* Creates new {@link AggregationExpression} that returns the day of the year for a date as a number between 1 and
* 366.
@@ -304,6 +481,90 @@ public class DateOperators {
return applyTimezone(DayOfWeek.dayOfWeek(dateReference()), timezone);
}
/**
* Creates new {@link AggregationExpression} that calculates the difference (in {@literal units}) to the date
* computed by the given {@link AggregationExpression expression}.
*
* @param expression must not be {@literal null}.
* @param unit the unit of measure. Must not be {@literal null}.
* @return new instance of {@link DateAdd}. @since 3.3
*/
public DateDiff diffValueOf(AggregationExpression expression, String unit) {
return applyTimezone(DateDiff.diffValueOf(expression, unit).toDate(dateReference()), timezone);
}
/**
* Creates new {@link AggregationExpression} that calculates the difference (in {@literal units}) to the date
* computed by the given {@link AggregationExpression expression}.
*
* @param expression must not be {@literal null}.
* @param unit the unit of measure. Must not be {@literal null}.
* @return new instance of {@link DateAdd}. @since 3.3
*/
public DateDiff diffValueOf(AggregationExpression expression, TemporalUnit unit) {
Assert.notNull(unit, "TemporalUnit must not be null");
return applyTimezone(
DateDiff.diffValueOf(expression, unit.name().toLowerCase(Locale.ROOT)).toDate(dateReference()), timezone);
}
/**
* Creates new {@link AggregationExpression} that calculates the difference (in {@literal units}) to the date stored
* at the given {@literal field}.
*
* @param fieldReference must not be {@literal null}.
* @param unit the unit of measure. Must not be {@literal null}.
* @return new instance of {@link DateAdd}. @since 3.3
*/
public DateDiff diffValueOf(String fieldReference, String unit) {
return applyTimezone(DateDiff.diffValueOf(fieldReference, unit).toDate(dateReference()), timezone);
}
/**
* Creates new {@link AggregationExpression} that calculates the difference (in {@literal units}) to the date stored
* at the given {@literal field}.
*
* @param fieldReference must not be {@literal null}.
* @param unit the unit of measure. Must not be {@literal null}.
* @return new instance of {@link DateAdd}. @since 3.3
*/
public DateDiff diffValueOf(String fieldReference, TemporalUnit unit) {
Assert.notNull(unit, "TemporalUnit must not be null");
return applyTimezone(
DateDiff.diffValueOf(fieldReference, unit.name().toLowerCase(Locale.ROOT)).toDate(dateReference()), timezone);
}
/**
* Creates new {@link AggregationExpression} that calculates the difference (in {@literal units}) to the date given
* {@literal value}.
*
* @param value anything the resolves to a valid date. Must not be {@literal null}.
* @param unit the unit of measure. Must not be {@literal null}.
* @return new instance of {@link DateAdd}. @since 3.3
*/
public DateDiff diff(Object value, String unit) {
return applyTimezone(DateDiff.diffValue(value, unit).toDate(dateReference()), timezone);
}
/**
* Creates new {@link AggregationExpression} that calculates the difference (in {@literal units}) to the date given
* {@literal value}.
*
* @param value anything the resolves to a valid date. Must not be {@literal null}.
* @param unit the unit of measure. Must not be {@literal null}.
* @return new instance of {@link DateAdd}. @since 3.3
*/
public DateDiff diff(Object value, TemporalUnit unit) {
Assert.notNull(unit, "TemporalUnit must not be null");
return applyTimezone(DateDiff.diffValue(value, unit.name().toLowerCase(Locale.ROOT)).toDate(dateReference()),
timezone);
}
/**
* Creates new {@link AggregationExpression} that returns the year portion of a date.
*
@@ -1480,7 +1741,6 @@ public class DateOperators {
} else {
clone.put("timezone", ((Timezone) value).value);
}
} else {
clone.put(key, value);
}
@@ -1911,7 +2171,7 @@ public class DateOperators {
* @author Matt Morrissette
* @author Christoph Strobl
* @see <a href=
* "https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromParts/">https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromParts/</a>
* "https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromParts/">https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromParts/</a>
* @since 2.1
*/
public static class DateFromParts extends TimezonedDateAggregationExpression implements DateParts<DateFromParts> {
@@ -2086,7 +2346,7 @@ public class DateOperators {
* @author Matt Morrissette
* @author Christoph Strobl
* @see <a href=
* "https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromParts/">https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromParts/</a>
* "https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromParts/">https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromParts/</a>
* @since 2.1
*/
public static class IsoDateFromParts extends TimezonedDateAggregationExpression
@@ -2262,7 +2522,7 @@ public class DateOperators {
* @author Matt Morrissette
* @author Christoph Strobl
* @see <a href=
* "https://docs.mongodb.com/manual/reference/operator/aggregation/dateToParts/">https://docs.mongodb.com/manual/reference/operator/aggregation/dateToParts/</a>
* "https://docs.mongodb.com/manual/reference/operator/aggregation/dateToParts/">https://docs.mongodb.com/manual/reference/operator/aggregation/dateToParts/</a>
* @since 2.1
*/
public static class DateToParts extends TimezonedDateAggregationExpression {
@@ -2343,7 +2603,7 @@ public class DateOperators {
* @author Matt Morrissette
* @author Christoph Strobl
* @see <a href=
* "https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromString/">https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromString/</a>
* "https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromString/">https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromString/</a>
* @since 2.1
*/
public static class DateFromString extends TimezonedDateAggregationExpression {
@@ -2418,6 +2678,290 @@ public class DateOperators {
}
}
/**
* {@link AggregationExpression} for {@code $dateAdd}.<br />
* <strong>NOTE:</strong> Requires MongoDB 5.0 or later.
*
* @author Christoph Strobl
* @since 3.3
*/
public static class DateAdd extends TimezonedDateAggregationExpression {
private DateAdd(Object value) {
super(value);
}
/**
* Add the number of {@literal units} of the result of the given {@link AggregationExpression expression} to a
* {@link #toDate(Object) start date}.
*
* @param expression must not be {@literal null}.
* @param unit must not be {@literal null}.
* @return new instance of {@link DateAdd}.
*/
public static DateAdd addValueOf(AggregationExpression expression, String unit) {
return addValue(expression, unit);
}
/**
* Add the number of {@literal units} from a {@literal field} to a {@link #toDate(Object) start date}.
*
* @param fieldReference must not be {@literal null}.
* @param unit must not be {@literal null}.
* @return new instance of {@link DateAdd}.
*/
public static DateAdd addValueOf(String fieldReference, String unit) {
return addValue(Fields.field(fieldReference), unit);
}
/**
* Add the number of {@literal units} to a {@link #toDate(Object) start date}.
*
* @param value must not be {@literal null}.
* @param unit must not be {@literal null}.
* @return new instance of {@link DateAdd}.
*/
public static DateAdd addValue(Object value, String unit) {
Map<String, Object> args = new HashMap<>();
args.put("unit", unit);
args.put("amount", value);
return new DateAdd(args);
}
/**
* Define the start date, in UTC, for the addition operation.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link DateAdd}.
*/
public DateAdd toDateOf(AggregationExpression expression) {
return toDate(expression);
}
/**
* Define the start date, in UTC, for the addition operation.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link DateAdd}.
*/
public DateAdd toDateOf(String fieldReference) {
return toDate(Fields.field(fieldReference));
}
/**
* Define the start date, in UTC, for the addition operation.
*
* @param dateExpression anything that evaluates to a valid date. Must not be {@literal null}.
* @return new instance of {@link DateAdd}.
*/
public DateAdd toDate(Object dateExpression) {
return new DateAdd(append("startDate", dateExpression));
}
/**
* Optionally set the {@link Timezone} to use. If not specified {@literal UTC} is used.
*
* @param timezone must not be {@literal null}. Consider {@link Timezone#none()} instead.
* @return new instance of {@link DateAdd}.
*/
public DateAdd withTimezone(Timezone timezone) {
return new DateAdd(appendTimezone(argumentMap(), timezone));
}
@Override
protected String getMongoMethod() {
return "$dateAdd";
}
}
/**
* {@link AggregationExpression} for {@code $dateDiff}.<br />
* <strong>NOTE:</strong> Requires MongoDB 5.0 or later.
*
* @author Christoph Strobl
* @since 3.3
*/
public static class DateDiff extends TimezonedDateAggregationExpression {
private DateDiff(Object value) {
super(value);
}
/**
* Add the number of {@literal units} of the result of the given {@link AggregationExpression expression} to a
* {@link #toDate(Object) start date}.
*
* @param expression must not be {@literal null}.
* @param unit must not be {@literal null}.
* @return new instance of {@link DateAdd}.
*/
public static DateDiff diffValueOf(AggregationExpression expression, String unit) {
return diffValue(expression, unit);
}
/**
* Add the number of {@literal units} from a {@literal field} to a {@link #toDate(Object) start date}.
*
* @param fieldReference must not be {@literal null}.
* @param unit must not be {@literal null}.
* @return new instance of {@link DateAdd}.
*/
public static DateDiff diffValueOf(String fieldReference, String unit) {
return diffValue(Fields.field(fieldReference), unit);
}
/**
* Add the number of {@literal units} to a {@link #toDate(Object) start date}.
*
* @param value must not be {@literal null}.
* @param unit must not be {@literal null}.
* @return new instance of {@link DateAdd}.
*/
public static DateDiff diffValue(Object value, String unit) {
Map<String, Object> args = new HashMap<>();
args.put("unit", unit);
args.put("endDate", value);
return new DateDiff(args);
}
/**
* Define the start date, in UTC, for the addition operation.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link DateAdd}.
*/
public DateDiff toDateOf(AggregationExpression expression) {
return toDate(expression);
}
/**
* Define the start date, in UTC, for the addition operation.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link DateAdd}.
*/
public DateDiff toDateOf(String fieldReference) {
return toDate(Fields.field(fieldReference));
}
/**
* Define the start date, in UTC, for the addition operation.
*
* @param dateExpression anything that evaluates to a valid date. Must not be {@literal null}.
* @return new instance of {@link DateAdd}.
*/
public DateDiff toDate(Object dateExpression) {
return new DateDiff(append("startDate", dateExpression));
}
/**
* Optionally set the {@link Timezone} to use. If not specified {@literal UTC} is used.
*
* @param timezone must not be {@literal null}. Consider {@link Timezone#none()} instead.
* @return new instance of {@link DateAdd}.
*/
public DateDiff withTimezone(Timezone timezone) {
return new DateDiff(appendTimezone(argumentMap(), timezone));
}
/**
* Set the start day of the week if the unit if measure is set to {@literal week}. Uses {@literal Sunday} by
* default.
*
* @param day must not be {@literal null}.
* @return new instance of {@link DateDiff}.
*/
public DateDiff startOfWeek(Object day) {
return new DateDiff(append("startOfWeek", day));
}
@Override
protected String getMongoMethod() {
return "$dateDiff";
}
}
/**
* Interface defining a temporal unit for date operators.
*
* @author Mark Paluch
* @since 3.3
*/
public interface TemporalUnit {
String name();
/**
* Converts the given time unit into a {@link TemporalUnit}. Supported units are: days, hours, minutes, seconds, and
* milliseconds.
*
* @param timeUnit the time unit to convert, must not be {@literal null}.
* @return
* @throws IllegalArgumentException if the {@link TimeUnit} is {@literal null} or not supported for conversion.
*/
static TemporalUnit from(TimeUnit timeUnit) {
Assert.notNull(timeUnit, "TimeUnit must not be null");
switch (timeUnit) {
case DAYS:
return TemporalUnits.DAY;
case HOURS:
return TemporalUnits.HOUR;
case MINUTES:
return TemporalUnits.MINUTE;
case SECONDS:
return TemporalUnits.SECOND;
case MILLISECONDS:
return TemporalUnits.MILLISECOND;
}
throw new IllegalArgumentException(String.format("Cannot create TemporalUnit from %s", timeUnit));
}
/**
* Converts the given chrono unit into a {@link TemporalUnit}. Supported units are: years, weeks, months, days,
* hours, minutes, seconds, and millis.
*
* @param chronoUnit the chrono unit to convert, must not be {@literal null}.
* @return
* @throws IllegalArgumentException if the {@link TimeUnit} is {@literal null} or not supported for conversion.
*/
static TemporalUnit from(ChronoUnit chronoUnit) {
switch (chronoUnit) {
case YEARS:
return TemporalUnits.YEAR;
case WEEKS:
return TemporalUnits.WEEK;
case MONTHS:
return TemporalUnits.MONTH;
case DAYS:
return TemporalUnits.DAY;
case HOURS:
return TemporalUnits.HOUR;
case MINUTES:
return TemporalUnits.MINUTE;
case SECONDS:
return TemporalUnits.SECOND;
case MILLIS:
return TemporalUnits.MILLISECOND;
}
throw new IllegalArgumentException(String.format("Cannot create TemporalUnit from %s", chronoUnit));
}
}
/**
* Supported temporal units.
*/
enum TemporalUnits implements TemporalUnit {
YEAR, QUARTER, WEEK, MONTH, DAY, HOUR, MINUTE, SECOND, MILLISECOND
}
@SuppressWarnings("unchecked")
private static <T extends TimezonedDateAggregationExpression> T applyTimezone(T instance, Timezone timezone) {
return !ObjectUtils.nullSafeEquals(Timezone.none(), timezone) && !instance.hasTimezone()

View File

@@ -0,0 +1,222 @@
/*
* Copyright 2021 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
*
* https://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.bson.Document;
/**
* Gateway to {@literal document expressions} such as {@literal $rank, $documentNumber, etc.}
*
* @author Christoph Strobl
* @since 3.3
*/
public class DocumentOperators {
/**
* Obtain the document position (including gaps) relative to others (rank).
*
* @return new instance of {@link Rank}.
* @since 3.3
*/
public static Rank rank() {
return new Rank();
}
/**
* Obtain the document position (without gaps) relative to others (rank).
*
* @return new instance of {@link DenseRank}.
* @since 3.3
*/
public static DenseRank denseRank() {
return new DenseRank();
}
/**
* Take the field referenced by given {@literal fieldReference}.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link DocumentOperatorsFactory}.
*/
public static DocumentOperatorsFactory valueOf(String fieldReference) {
return new DocumentOperatorsFactory(fieldReference);
}
/**
* Take the value resulting from the given {@link AggregationExpression}.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link DocumentOperatorsFactory}.
*/
public static DocumentOperatorsFactory valueOf(AggregationExpression expression) {
return new DocumentOperatorsFactory(expression);
}
/**
* Obtain the current document position.
*
* @return new instance of {@link DocumentNumber}.
* @since 3.3
*/
public static DocumentNumber documentNumber() {
return new DocumentNumber();
}
/**
* @author Christoph Strobl
*/
public static class DocumentOperatorsFactory {
private final Object target;
public DocumentOperatorsFactory(Object target) {
this.target = target;
}
/**
* Creates new {@link AggregationExpression} that applies the expression to a document at specified position
* relative to the current document.
*
* @param by the value to add to the current position.
* @return new instance of {@link Shift}.
*/
public Shift shift(int by) {
Shift shift = usesExpression() ? Shift.shift((AggregationExpression) target) : Shift.shift(target.toString());
return shift.by(by);
}
private boolean usesExpression() {
return target instanceof AggregationExpression;
}
}
/**
* {@link Rank} resolves the current document position (the rank) relative to other documents. If multiple documents
* occupy the same rank, {@literal $rank} places the document with the subsequent value at a rank with a gap.
*
* @author Christoph Strobl
* @since 3.3
*/
public static class Rank implements AggregationExpression {
@Override
public Document toDocument(AggregationOperationContext context) {
return new Document("$rank", new Document());
}
}
/**
* {@link DenseRank} resolves the current document position (the rank) relative to other documents. If multiple
* documents occupy the same rank, {@literal $denseRank} places the document with the subsequent value at the next
* rank without any gaps.
*
* @author Christoph Strobl
* @since 3.3
*/
public static class DenseRank implements AggregationExpression {
@Override
public Document toDocument(AggregationOperationContext context) {
return new Document("$denseRank", new Document());
}
}
/**
* {@link DocumentNumber} resolves the current document position.
*
* @author Christoph Strobl
* @since 3.3
*/
public static class DocumentNumber implements AggregationExpression {
@Override
public Document toDocument(AggregationOperationContext context) {
return new Document("$documentNumber", new Document());
}
}
/**
* Shift applies an expression to a document in a specified position relative to the current document.
*
* @author Christoph Strobl
* @since 3.3
*/
public static class Shift extends AbstractAggregationExpression {
private Shift(Object value) {
super(value);
}
/**
* Specifies the field to evaluate and return.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link Shift}.
*/
public static Shift shift(String fieldReference) {
return new Shift(Collections.singletonMap("output", Fields.field(fieldReference)));
}
/**
* Specifies the {@link AggregationExpression expression} to evaluate and return.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link Shift}.
*/
public static Shift shift(AggregationExpression expression) {
return new Shift(Collections.singletonMap("output", expression));
}
/**
* Shift the document position relative to the current. Use a positive value for follow up documents (eg. 1 for the
* next) or a negative value for the predecessor documents (eg. -1 for the previous).
*
* @param shiftBy value to add to the current position.
* @return new instance of {@link Shift}.
*/
public Shift by(int shiftBy) {
return new Shift(append("by", shiftBy));
}
/**
* Define the default value if the target document is out of range.
*
* @param value must not be {@literal null}.
* @return new instance of {@link Shift}.
*/
public Shift defaultTo(Object value) {
return new Shift(append("default", value));
}
/**
* Define the {@link AggregationExpression expression} to evaluate if the target document is out of range.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link Shift}.
*/
public Shift defaultToValueOf(AggregationExpression expression) {
return defaultTo(expression);
}
@Override
protected String getMongoMethod() {
return "$shift";
}
}
}

View File

@@ -0,0 +1,155 @@
/*
* Copyright 2021 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
*
* https://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 org.bson.Document;
import org.springframework.data.mongodb.core.query.CriteriaDefinition;
import org.springframework.util.Assert;
/**
* Gateway to {@literal evaluation operators} such as {@literal $expr}.
*
* @author Divya Srivastava
* @since 3.3
*/
public class EvaluationOperators {
/**
* Take the value resulting from the given fieldReference.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link EvaluationOperatorFactory}.
*/
public static EvaluationOperatorFactory valueOf(String fieldReference) {
return new EvaluationOperatorFactory(fieldReference);
}
/**
* Take the value resulting from the given {@link AggregationExpression}.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link EvaluationOperatorFactory}.
*/
public static EvaluationOperatorFactory valueOf(AggregationExpression expression) {
return new EvaluationOperatorFactory(expression);
}
public static class EvaluationOperatorFactory {
private final String fieldReference;
private final AggregationExpression expression;
/**
* Creates new {@link EvaluationOperatorFactory} for given {@literal fieldReference}.
*
* @param fieldReference must not be {@literal null}.
*/
public EvaluationOperatorFactory(String fieldReference) {
Assert.notNull(fieldReference, "FieldReference must not be null!");
this.fieldReference = fieldReference;
this.expression = null;
}
/**
* Creates new {@link EvaluationOperatorFactory} for given {@link AggregationExpression}.
*
* @param expression must not be {@literal null}.
*/
public EvaluationOperatorFactory(AggregationExpression expression) {
Assert.notNull(expression, "Expression must not be null!");
this.fieldReference = null;
this.expression = expression;
}
/**
* Creates new {@link AggregationExpression} that is a valid aggregation expression.
*
* @return new instance of {@link Expr}.
*/
public Expr expr() {
return usesFieldRef() ? Expr.valueOf(fieldReference) : Expr.valueOf(expression);
}
/**
* Allows the use of aggregation expressions within the query language.
*/
public static class Expr extends AbstractAggregationExpression {
private Expr(Object value) {
super(value);
}
@Override
protected String getMongoMethod() {
return "$expr";
}
/**
* Creates new {@link Expr}.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link Expr}.
*/
public static Expr valueOf(String fieldReference) {
Assert.notNull(fieldReference, "FieldReference must not be null!");
return new Expr(Fields.field(fieldReference));
}
/**
* Creates new {@link Expr}.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link Expr}.
*/
public static Expr valueOf(AggregationExpression expression) {
Assert.notNull(expression, "Expression must not be null!");
return new Expr(expression);
}
/**
* Creates {@code $expr} as {@link CriteriaDefinition}.
*
* @return the {@link CriteriaDefinition} from this expression.
*/
public CriteriaDefinition toCriteriaDefinition(AggregationOperationContext context) {
Document criteriaObject = toDocument(context);
return new CriteriaDefinition() {
@Override
public Document getCriteriaObject() {
return criteriaObject;
}
@Override
public String getKey() {
return getMongoMethod();
}
};
}
}
private boolean usesFieldRef() {
return fieldReference != null;
}
}
}

View File

@@ -36,8 +36,7 @@ import org.springframework.util.ClassUtils;
* We recommend to use the static factory method {@link Aggregation#graphLookup(String)} instead of creating instances
* of this class directly.
*
* @see <a href=
* "https://docs.mongodb.org/manual/reference/aggregation/graphLookup/">https://docs.mongodb.org/manual/reference/aggregation/graphLookup/</a>
* @see <a href="https://docs.mongodb.org/manual/reference/aggregation/graphLookup/">https://docs.mongodb.org/manual/reference/aggregation/graphLookup/</a>
* @author Mark Paluch
* @author Christoph Strobl
* @since 1.10

View File

@@ -139,7 +139,7 @@ public class GroupOperation implements FieldsExposingAggregationOperation {
* Generates an {@link GroupOperationBuilder} for a {@code $sum}-expression.
* <p>
* Count expressions are emulated via {@code $sum: 1}.
* <p>
* </p>
*
* @return
*/

View File

@@ -16,6 +16,7 @@
package org.springframework.data.mongodb.core.aggregation;
import org.bson.Document;
import org.springframework.data.mongodb.core.query.CriteriaDefinition;
import org.springframework.util.Assert;
@@ -29,6 +30,7 @@ import org.springframework.util.Assert;
* @author Sebastian Herold
* @author Thomas Darimont
* @author Oliver Gierke
* @author Divya Srivastava
* @since 1.3
* @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/match/">MongoDB Aggregation Framework:
* $match</a>
@@ -36,6 +38,7 @@ import org.springframework.util.Assert;
public class MatchOperation implements AggregationOperation {
private final CriteriaDefinition criteriaDefinition;
private final AggregationExpression expression;
/**
* Creates a new {@link MatchOperation} for the given {@link CriteriaDefinition}.
@@ -45,7 +48,23 @@ public class MatchOperation implements AggregationOperation {
public MatchOperation(CriteriaDefinition criteriaDefinition) {
Assert.notNull(criteriaDefinition, "Criteria must not be null!");
this.criteriaDefinition = criteriaDefinition;
this.expression = null;
}
/**
* Creates a new {@link MatchOperation} for the given {@link AggregationExpression}.
*
* @param expression must not be {@literal null}.
* @since 3.3
*/
public MatchOperation(AggregationExpression expression) {
Assert.notNull(expression, "Expression must not be null!");
this.criteriaDefinition = null;
this.expression = expression;
}
/*
@@ -54,7 +73,9 @@ public class MatchOperation implements AggregationOperation {
*/
@Override
public Document toDocument(AggregationOperationContext context) {
return new Document(getOperator(), context.getMappedObject(criteriaDefinition.getCriteriaObject()));
return new Document(getOperator(),
context.getMappedObject(expression != null ? expression.toDocument() : criteriaDefinition.getCriteriaObject()));
}
/*

View File

@@ -30,7 +30,7 @@ import org.springframework.lang.Nullable;
/**
* {@link AggregationOperationContext} implementation prefixing non-command keys on root level with the given prefix.
* Useful when mapping fields to domain specific types while having to prefix keys for query purpose.
* <p />
* <br />
* Fields to be excluded from prefixing my be added to a {@literal denylist}.
*
* @author Christoph Strobl

View File

@@ -33,8 +33,7 @@ import org.springframework.util.Assert;
* </pre>
*
* @author Christoph Strobl
* @see <a href=
* "https://docs.mongodb.com/manual/reference/operator/aggregation/redact/">https://docs.mongodb.com/manual/reference/operator/aggregation/redact/</a>
* @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/redact/">https://docs.mongodb.com/manual/reference/operator/aggregation/redact/</a>
* @since 3.0
*/
public class RedactOperation implements AggregationOperation {

View File

@@ -21,6 +21,7 @@ import java.util.Collections;
import java.util.List;
import org.bson.Document;
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
import org.springframework.expression.spel.ast.Projection;
import org.springframework.util.Assert;

View File

@@ -30,7 +30,7 @@ import org.springframework.util.CollectionUtils;
/**
* Gateway to {@literal $function} and {@literal $accumulator} aggregation operations.
* <p />
* <br />
* Using {@link ScriptOperators} as part of the {@link Aggregation} requires MongoDB server to have
* <a href="https://docs.mongodb.com/master/core/server-side-javascript/">server-side JavaScript</a> execution
* <a href="https://docs.mongodb.com/master/reference/configuration-options/#security.javascriptEnabled">enabled</a>.
@@ -43,7 +43,7 @@ public class ScriptOperators {
/**
* Create a custom aggregation
* <a href="https://docs.mongodb.com/master/reference/operator/aggregation/function/">$function<a /> in JavaScript.
* <a href="https://docs.mongodb.com/master/reference/operator/aggregation/function/">$function</a> in JavaScript.
*
* @param body The function definition. Must not be {@literal null}.
* @return new instance of {@link Function}.
@@ -53,8 +53,8 @@ public class ScriptOperators {
}
/**
* Create a custom <a href="https://docs.mongodb.com/master/reference/operator/aggregation/accumulator/">$accumulator
* operator</a> in Javascript.
* Create a custom <a href="https://docs.mongodb.com/master/reference/operator/aggregation/accumulator/">$accumulator operator</a>
* in Javascript.
*
* @return new instance of {@link AccumulatorInitBuilder}.
*/
@@ -65,7 +65,7 @@ public class ScriptOperators {
/**
* {@link Function} defines a custom aggregation
* <a href="https://docs.mongodb.com/master/reference/operator/aggregation/function/">$function</a> in JavaScript.
* <p />
* <br />
* <code class="java">
* {
* $function: {
@@ -75,7 +75,7 @@ public class ScriptOperators {
* }
* }
* </code>
* <p />
* <br />
* {@link Function} cannot be used as part of {@link org.springframework.data.mongodb.core.schema.MongoJsonSchema
* schema} validation query expression. <br />
* <b>NOTE:</b> <a href="https://docs.mongodb.com/master/core/server-side-javascript/">Server-Side JavaScript</a>
@@ -179,7 +179,7 @@ public class ScriptOperators {
* <a href="https://docs.mongodb.com/master/reference/operator/aggregation/accumulator/">$accumulator operator</a>,
* one that maintains its state (e.g. totals, maximums, minimums, and related data) as documents progress through the
* pipeline, in JavaScript.
* <p />
* <br />
* <code class="java">
* {
* $accumulator: {
@@ -193,7 +193,7 @@ public class ScriptOperators {
* }
* }
* </code>
* <p />
* <br />
* {@link Accumulator} can be used as part of {@link GroupOperation $group}, {@link BucketOperation $bucket} and
* {@link BucketAutoOperation $bucketAuto} pipeline stages. <br />
* <b>NOTE:</b> <a href="https://docs.mongodb.com/master/core/server-side-javascript/">Server-Side JavaScript</a>
@@ -241,7 +241,7 @@ public class ScriptOperators {
/**
* Define the {@code init} {@link Function} for the {@link Accumulator accumulators} initial state. The function
* receives its arguments from the {@link Function#args(Object...) initArgs} array expression.
* <p />
* <br />
* <code class="java">
* function(initArg1, initArg2, ...) {
* ...
@@ -259,7 +259,7 @@ public class ScriptOperators {
/**
* Define the {@code init} function for the {@link Accumulator accumulators} initial state. The function receives
* its arguments from the {@link AccumulatorInitArgsBuilder#initArgs(Object...)} array expression.
* <p />
* <br />
* <code class="java">
* function(initArg1, initArg2, ...) {
* ...
@@ -308,7 +308,7 @@ public class ScriptOperators {
* Set the {@code accumulate} {@link Function} that updates the state for each document. The functions first
* argument is the current {@code state}, additional arguments can be defined via {@link Function#args(Object...)
* accumulateArgs}.
* <p />
* <br />
* <code class="java">
* function(state, accumArg1, accumArg2, ...) {
* ...
@@ -327,7 +327,7 @@ public class ScriptOperators {
* Set the {@code accumulate} function that updates the state for each document. The functions first argument is
* the current {@code state}, additional arguments can be defined via
* {@link AccumulatorAccumulateArgsBuilder#accumulateArgs(Object...)}.
* <p />
* <br />
* <code class="java">
* function(state, accumArg1, accumArg2, ...) {
* ...
@@ -370,7 +370,7 @@ public class ScriptOperators {
* Set the {@code merge} function used to merge two internal states. <br />
* This might be required because the operation is run on a sharded cluster or when the operator exceeds its
* memory limit.
* <p />
* <br />
* <code class="java">
* function(state1, state2) {
* ...
@@ -389,7 +389,7 @@ public class ScriptOperators {
/**
* Set the {@code finalize} function used to update the result of the accumulation when all documents have been
* processed.
* <p />
* <br />
* <code class="java">
* function(state) {
* ...
@@ -425,7 +425,7 @@ public class ScriptOperators {
/**
* Define the {@code init} function for the {@link Accumulator accumulators} initial state. The function receives
* its arguments from the {@link #initArgs(Object...)} array expression.
* <p />
* <br />
* <code class="java">
* function(initArg1, initArg2, ...) {
* ...
@@ -461,7 +461,7 @@ public class ScriptOperators {
/**
* Set the {@code accumulate} function that updates the state for each document. The functions first argument is
* the current {@code state}, additional arguments can be defined via {@link #accumulateArgs(Object...)}.
* <p />
* <br />
* <code class="java">
* function(state, accumArg1, accumArg2, ...) {
* ...
@@ -500,7 +500,7 @@ public class ScriptOperators {
* Set the {@code merge} function used to merge two internal states. <br />
* This might be required because the operation is run on a sharded cluster or when the operator exceeds its
* memory limit.
* <p />
* <br />
* <code class="java">
* function(state1, state2) {
* ...
@@ -537,7 +537,7 @@ public class ScriptOperators {
/**
* Set the {@code finalize} function used to update the result of the accumulation when all documents have been
* processed.
* <p />
* <br />
* <code class="java">
* function(state) {
* ...

View File

@@ -193,5 +193,6 @@ public class SetOperation extends DocumentEnhancingOperation {
*/
SetOperation withValueOfExpression(String operation, Object... values);
}
}
}

View File

@@ -0,0 +1,873 @@
/*
* Copyright 2021 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
*
* https://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.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.bson.Document;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Encapsulates the {@code setWindowFields}-operation.
*
* @author Christoph Strobl
* @since 3.3
* @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/setWindowFields/">https://docs.mongodb.com/manual/reference/operator/aggregation/setWindowFields/</a>
*/
public class SetWindowFieldsOperation
implements AggregationOperation, FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation {
private static final String CURRENT = "current";
private static final String UNBOUNDED = "unbounded";
private final @Nullable Object partitionBy;
private final @Nullable AggregationOperation sortBy;
private final WindowOutput output;
/**
* Create a new {@link SetWindowFieldsOperation} with given args.
*
* @param partitionBy The field or {@link AggregationExpression} to group by.
* @param sortBy the {@link SortOperation operation} to sort the documents by in the partition.
* @param output the {@link WindowOutput} containing the fields to add and the rules to calculate their respective
* values.
*/
protected SetWindowFieldsOperation(@Nullable Object partitionBy, @Nullable AggregationOperation sortBy,
WindowOutput output) {
this.partitionBy = partitionBy;
this.sortBy = sortBy;
this.output = output;
}
/**
* Obtain a {@link SetWindowFieldsOperationBuilder builder} to create a {@link SetWindowFieldsOperation}.
*
* @return new instance of {@link SetWindowFieldsOperationBuilder}.
*/
public static SetWindowFieldsOperationBuilder builder() {
return new SetWindowFieldsOperationBuilder();
}
@Override
public ExposedFields getFields() {
return ExposedFields.nonSynthetic(Fields.from(output.fields.toArray(new Field[0])));
}
@Override
public Document toDocument(AggregationOperationContext context) {
Document $setWindowFields = new Document();
if (partitionBy != null) {
if (partitionBy instanceof AggregationExpression) {
$setWindowFields.append("partitionBy", ((AggregationExpression) partitionBy).toDocument(context));
} else if (partitionBy instanceof Field) {
$setWindowFields.append("partitionBy", context.getReference((Field) partitionBy).toString());
} else {
$setWindowFields.append("partitionBy", partitionBy);
}
}
if (sortBy != null) {
$setWindowFields.append("sortBy", sortBy.toDocument(context).get(sortBy.getOperator()));
}
Document output = new Document();
for (ComputedField field : this.output.fields) {
Document fieldOperation = field.getWindowOperator().toDocument(context);
if (field.window != null) {
fieldOperation.put("window", field.window.toDocument(context));
}
output.append(field.getName(), fieldOperation);
}
$setWindowFields.append("output", output);
return new Document(getOperator(), $setWindowFields);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#getOperator()
*/
@Override
public String getOperator() {
return "$setWindowFields";
}
/**
* {@link WindowOutput} defines output of {@literal $setWindowFields} stage by defining the {@link ComputedField
* field(s)} to append to the documents in the output.
*/
public static class WindowOutput {
private final List<ComputedField> fields;
/**
* Create a new output containing the single given {@link ComputedField field}.
*
* @param outputField must not be {@literal null}.
*/
public WindowOutput(ComputedField outputField) {
Assert.notNull(outputField, "OutputField must not be null!");
this.fields = new ArrayList<>();
this.fields.add(outputField);
}
/**
* Append the given {@link ComputedField field} to the outptut.
*
* @param field must not be {@literal null}.
* @return this.
*/
public WindowOutput append(ComputedField field) {
Assert.notNull(field, "Field must not be null!");
fields.add(field);
return this;
}
/**
* Append the given {@link AggregationExpression} as a {@link ComputedField field} in a fluent way.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link ComputedFieldAppender}.
* @see #append(ComputedField)
*/
public ComputedFieldAppender append(AggregationExpression expression) {
return new ComputedFieldAppender() {
@Nullable private Window window;
@Override
public WindowOutput as(String fieldname) {
return WindowOutput.this.append(new ComputedField(fieldname, expression, window));
}
@Override
public ComputedFieldAppender within(Window window) {
this.window = window;
return this;
}
};
}
/**
* Tiny little helper to allow fluent API usage for {@link #append(ComputedField)}.
*/
interface ComputedFieldAppender {
/**
* Specify the target field name.
*
* @param fieldname the name of field to add to the target document.
* @return the {@link WindowOutput} that started the append operation.
*/
WindowOutput as(String fieldname);
/**
* Specify the window boundaries.
*
* @param window must not be {@literal null}.
* @return this.
*/
ComputedFieldAppender within(Window window);
}
}
/**
* A {@link Field} that the result of a computation done via an {@link AggregationExpression}.
*
* @author Christoph Strobl
*/
public static class ComputedField implements Field {
private final String name;
private final AggregationExpression windowOperator;
private final @Nullable Window window;
/**
* Create a new {@link ComputedField}.
*
* @param name the target field name.
* @param windowOperator the expression to calculate the field value.
*/
public ComputedField(String name, AggregationExpression windowOperator) {
this(name, windowOperator, null);
}
/**
* Create a new {@link ComputedField}.
*
* @param name the target field name.
* @param windowOperator the expression to calculate the field value.
* @param window the boundaries to operate within. Can be {@literal null}.
*/
public ComputedField(String name, AggregationExpression windowOperator, @Nullable Window window) {
this.name = name;
this.windowOperator = windowOperator;
this.window = window;
}
@Override
public String getName() {
return name;
}
@Override
public String getTarget() {
return getName();
}
@Override
public boolean isAliased() {
return false;
}
public AggregationExpression getWindowOperator() {
return windowOperator;
}
public Window getWindow() {
return window;
}
}
/**
* Quick access to {@link DocumentWindow documents} and {@literal RangeWindow range} {@link Window windows}.
*
* @author Christoph Strobl
*/
public interface Windows {
/**
* Create a document window relative to the position of the current document.
*
* @param lower an integer for a position relative to the current document, {@literal current} or
* {@literal unbounded}.
* @param upper an integer for a position relative to the current document, {@literal current} or
* {@literal unbounded}.
* @return new instance of {@link DocumentWindow}.
*/
static DocumentWindow documents(Object lower, Object upper) {
return new DocumentWindow(lower, upper);
}
/**
* Create a range window defined based on sort expression.
*
* @param lower a numeric value to add the sort by field value of the current document, {@literal current} or
* {@literal unbounded}.
* @param upper a numeric value to add the sort by field value of the current document, {@literal current} or
* {@literal unbounded}.
* @return new instance of {@link RangeWindow}.
*/
static RangeWindow range(Object lower, Object upper, @Nullable WindowUnit unit) {
return new RangeWindow(lower, upper, unit == null ? WindowUnits.DEFAULT : unit);
}
/**
* Create a range window based on the {@link Sort sort value} of the current document via a fluent API.
*
* @return new instance of {@link RangeWindowBuilder}.
*/
static RangeWindowBuilder range() {
return new RangeWindowBuilder();
}
/**
* Create a document window relative to the position of the current document via a fluent API.
*
* @return new instance of {@link DocumentWindowBuilder}.
*/
static DocumentWindowBuilder documents() {
return new DocumentWindowBuilder();
}
}
/**
* A {@link Window} to be used for {@link ComputedField#getWindow() ComputedField}.
*/
public interface Window {
/**
* The lower (inclusive) boundary.
*
* @return
*/
Object getLower();
/**
* The upper (inclusive) boundary.
*
* @return
*/
Object getUpper();
/**
* Obtain the document representation of the window in a default {@link AggregationOperationContext context}.
*
* @return never {@literal null}.
*/
default Document toDocument() {
return toDocument(Aggregation.DEFAULT_CONTEXT);
}
/**
* Obtain the document representation of the window in the given {@link AggregationOperationContext context}.
*
* @return never {@literal null}.
*/
Document toDocument(AggregationOperationContext ctx);
}
/**
* Builder API for a {@link RangeWindow}.
*
* @author Christoph Strobl
*/
public static class RangeWindowBuilder {
private @Nullable Object lower;
private @Nullable Object upper;
private @Nullable WindowUnit unit;
/**
* The lower (inclusive) range limit based on the sortBy field.
*
* @param lower eg. {@literal current} or {@literal unbounded}.
* @return this.
*/
public RangeWindowBuilder from(String lower) {
this.lower = lower;
return this;
}
/**
* The upper (inclusive) range limit based on the sortBy field.
*
* @param upper eg. {@literal current} or {@literal unbounded}.
* @return this.
*/
public RangeWindowBuilder to(String upper) {
this.upper = upper;
return this;
}
/**
* The lower (inclusive) range limit value to add to the value based on the sortBy field. Use a negative integer for
* a position before the current document. Use a positive integer for a position after the current document.
* {@code 0} is the current document position.
*
* @param lower
* @return this.
*/
public RangeWindowBuilder from(Number lower) {
this.lower = lower;
return this;
}
/**
* The upper (inclusive) range limit value to add to the value based on the sortBy field. Use a negative integer for
* a position before the current document. Use a positive integer for a position after the current document.
* {@code 0} is the current document position.
*
* @param upper
* @return this.
*/
public RangeWindowBuilder to(Number upper) {
this.upper = upper;
return this;
}
/**
* Use {@literal current} as {@link #from(String) lower} limit.
*
* @return this.
*/
public RangeWindowBuilder fromCurrent() {
return from(CURRENT);
}
/**
* Use {@literal unbounded} as {@link #from(String) lower} limit.
*
* @return this.
*/
public RangeWindowBuilder fromUnbounded() {
return from(UNBOUNDED);
}
/**
* Use {@literal current} as {@link #to(String) upper} limit.
*
* @return this.
*/
public RangeWindowBuilder toCurrent() {
return to(CURRENT);
}
/**
* Use {@literal unbounded} as {@link #to(String) upper} limit.
*
* @return this.
*/
public RangeWindowBuilder toUnbounded() {
return to(UNBOUNDED);
}
/**
* Set the {@link WindowUnit unit} or measure for the given {@link Window}.
*
* @param windowUnit must not be {@literal null}. Can be on of {@link Windows}.
* @return this.
*/
public RangeWindowBuilder unit(WindowUnit windowUnit) {
Assert.notNull(windowUnit, "WindowUnit must not be null");
this.unit = windowUnit;
return this;
}
/**
* Build the {@link RangeWindow}.
*
* @return new instance of {@link RangeWindow}.
*/
public RangeWindow build() {
Assert.notNull(lower, "Lower bound must not be null");
Assert.notNull(upper, "Upper bound must not be null");
Assert.notNull(unit, "WindowUnit bound must not be null");
return new RangeWindow(lower, upper, unit);
}
}
/**
* Builder API for a {@link RangeWindow}.
*
* @author Christoph Strobl
*/
public static class DocumentWindowBuilder {
private @Nullable Object lower;
private @Nullable Object upper;
/**
* The lower (inclusive) range limit based on current document. Use a negative integer for a position before the
* current document. Use a positive integer for a position after the current document. {@code 0} is the current
* document position.
*
* @param lower
* @return this.
*/
public DocumentWindowBuilder from(Number lower) {
this.lower = lower;
return this;
}
public DocumentWindowBuilder fromCurrent() {
return from(CURRENT);
}
public DocumentWindowBuilder fromUnbounded() {
return from(UNBOUNDED);
}
public DocumentWindowBuilder to(String upper) {
this.upper = upper;
return this;
}
/**
* The lower (inclusive) range limit based on current document.
*
* @param lower eg. {@literal current} or {@literal unbounded}.
* @return this.
*/
public DocumentWindowBuilder from(String lower) {
this.lower = lower;
return this;
}
/**
* The upper (inclusive) range limit based on current document. Use a negative integer for a position before the
* current document. Use a positive integer for a position after the current document. {@code 0} is the current
* document position.
*
* @param upper
* @return this.
*/
public DocumentWindowBuilder to(Number upper) {
this.upper = upper;
return this;
}
public DocumentWindowBuilder toCurrent() {
return to(CURRENT);
}
public DocumentWindowBuilder toUnbounded() {
return to(UNBOUNDED);
}
public DocumentWindow build() {
Assert.notNull(lower, "Lower bound must not be null");
Assert.notNull(upper, "Upper bound must not be null");
return new DocumentWindow(lower, upper);
}
}
/**
* Common base class for {@link Window} implementation.
*
* @author Christoph Strobl
*/
static abstract class WindowImpl implements Window {
private final Object lower;
private final Object upper;
protected WindowImpl(Object lower, Object upper) {
this.lower = lower;
this.upper = upper;
}
@Override
public Object getLower() {
return lower;
}
@Override
public Object getUpper() {
return upper;
}
}
/**
* {@link Window} implementation based on the current document.
*
* @author Christoph Strobl
*/
public static class DocumentWindow extends WindowImpl {
DocumentWindow(Object lower, Object upper) {
super(lower, upper);
}
@Override
public Document toDocument(AggregationOperationContext ctx) {
return new Document("documents", Arrays.asList(getLower(), getUpper()));
}
}
/**
* {@link Window} implementation based on the sort fields.
*
* @author Christoph Strobl
*/
public static class RangeWindow extends WindowImpl {
private final WindowUnit unit;
protected RangeWindow(Object lower, Object upper, WindowUnit unit) {
super(lower, upper);
this.unit = unit;
}
@Override
public Document toDocument(AggregationOperationContext ctx) {
Document range = new Document("range", new Object[] { getLower(), getUpper() });
if (unit != null && !WindowUnits.DEFAULT.equals(unit)) {
range.append("unit", unit.name().toLowerCase());
}
return range;
}
}
/**
* The actual time unit to apply to a {@link Window}.
*/
public interface WindowUnit {
String name();
/**
* Converts the given time unit into a {@link WindowUnit}. Supported units are: days, hours, minutes, seconds, and
* milliseconds.
*
* @param timeUnit the time unit to convert, must not be {@literal null}.
* @return
* @throws IllegalArgumentException if the {@link TimeUnit} is {@literal null} or not supported for conversion.
*/
static WindowUnit from(TimeUnit timeUnit) {
Assert.notNull(timeUnit, "TimeUnit must not be null");
switch (timeUnit) {
case DAYS:
return WindowUnits.DAY;
case HOURS:
return WindowUnits.HOUR;
case MINUTES:
return WindowUnits.MINUTE;
case SECONDS:
return WindowUnits.SECOND;
case MILLISECONDS:
return WindowUnits.MILLISECOND;
}
throw new IllegalArgumentException(String.format("Cannot create WindowUnit from %s", timeUnit));
}
/**
* Converts the given chrono unit into a {@link WindowUnit}. Supported units are: years, weeks, months, days, hours,
* minutes, seconds, and millis.
*
* @param chronoUnit the chrono unit to convert, must not be {@literal null}.
* @return
* @throws IllegalArgumentException if the {@link TimeUnit} is {@literal null} or not supported for conversion.
*/
static WindowUnit from(ChronoUnit chronoUnit) {
switch (chronoUnit) {
case YEARS:
return WindowUnits.YEAR;
case WEEKS:
return WindowUnits.WEEK;
case MONTHS:
return WindowUnits.MONTH;
case DAYS:
return WindowUnits.DAY;
case HOURS:
return WindowUnits.HOUR;
case MINUTES:
return WindowUnits.MINUTE;
case SECONDS:
return WindowUnits.SECOND;
case MILLIS:
return WindowUnits.MILLISECOND;
}
throw new IllegalArgumentException(String.format("Cannot create WindowUnit from %s", chronoUnit));
}
}
/**
* Quick access to available {@link WindowUnit units}.
*/
public enum WindowUnits implements WindowUnit {
DEFAULT, YEAR, QUARTER, MONTH, WEEK, DAY, HOUR, MINUTE, SECOND, MILLISECOND
}
/**
* A fluent builder to create a {@link SetWindowFieldsOperation}.
*
* @author Christoph Strobl
*/
public static class SetWindowFieldsOperationBuilder {
private Object partitionBy;
private SortOperation sortOperation;
private WindowOutput output;
/**
* Specify the field to group by.
*
* @param fieldName must not be {@literal null} or null.
* @return this.
*/
public SetWindowFieldsOperationBuilder partitionByField(String fieldName) {
Assert.hasText(fieldName, "Field name must not be empty or null");
return partitionBy(Fields.field("$" + fieldName, fieldName));
}
/**
* Specify the {@link AggregationExpression expression} to group by.
*
* @param expression must not be {@literal null}.
* @return this.
*/
public SetWindowFieldsOperationBuilder partitionByExpression(AggregationExpression expression) {
return partitionBy(expression);
}
/**
* Sort {@link Sort.Direction#ASC ascending} by the given fields.
*
* @param fields must not be {@literal null}.
* @return this.
*/
public SetWindowFieldsOperationBuilder sortBy(String... fields) {
return sortBy(Sort.by(fields));
}
/**
* Set the sort order.
*
* @param sort must not be {@literal null}.
* @return this.
*/
public SetWindowFieldsOperationBuilder sortBy(Sort sort) {
return sortBy(new SortOperation(sort));
}
/**
* Set the {@link SortOperation} to use.
*
* @param sort must not be {@literal null}.
* @return this.
*/
public SetWindowFieldsOperationBuilder sortBy(SortOperation sort) {
Assert.notNull(sort, "SortOperation must not be null");
this.sortOperation = sort;
return this;
}
/**
* Define the actual output computation.
*
* @param output must not be {@literal null}.
* @return this.
*/
public SetWindowFieldsOperationBuilder output(WindowOutput output) {
Assert.notNull(output, "WindowOutput must not be null");
this.output = output;
return this;
}
/**
* Add a field capturing the result of the given {@link AggregationExpression expression} to the output.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link WindowChoice}.
*/
public WindowChoice output(AggregationExpression expression) {
return new WindowChoice() {
@Nullable private Window window;
@Override
public As within(Window window) {
Assert.notNull(window, "Window must not be null");
this.window = window;
return this;
}
@Override
public SetWindowFieldsOperationBuilder as(String targetFieldName) {
Assert.hasText(targetFieldName, "Target field name must not be empty or null");
ComputedField computedField = new ComputedField(targetFieldName, expression, window);
if (SetWindowFieldsOperationBuilder.this.output == null) {
SetWindowFieldsOperationBuilder.this.output = new WindowOutput(computedField);
} else {
SetWindowFieldsOperationBuilder.this.output.append(computedField);
}
return SetWindowFieldsOperationBuilder.this;
}
};
}
/**
* Interface to capture field name used to capture the computation result.
*/
public interface As {
/**
* Define the target name field name to hold the computation result.
*
* @param targetFieldName must not be {@literal null} or empty.
* @return the starting point {@link SetWindowFieldsOperationBuilder builder} instance.
*/
SetWindowFieldsOperationBuilder as(String targetFieldName);
}
/**
* Interface to capture an optional {@link Window} applicable to the field computation.
*/
public interface WindowChoice extends As {
/**
* Specify calculation boundaries.
*
* @param window must not be {@literal null}.
* @return never {@literal null}.
*/
As within(Window window);
}
/**
* Partition by a value that translates to a valid mongodb expression.
*
* @param value must not be {@literal null}.
* @return this.
*/
public SetWindowFieldsOperationBuilder partitionBy(Object value) {
Assert.notNull(value, "Partition By must not be null");
partitionBy = value;
return this;
}
/**
* Obtain a new instance of {@link SetWindowFieldsOperation} with previously set arguments.
*
* @return new instance of {@link SetWindowFieldsOperation}.
*/
public SetWindowFieldsOperation build() {
return new SetWindowFieldsOperation(partitionBy, sortOperation, output);
}
}
}

View File

@@ -21,17 +21,16 @@ import org.springframework.util.Assert;
/**
* Encapsulates the aggregation framework {@code $sortByCount}-operation.
* <p/>
* <br />
* {@code $sortByCount} stage is typically used with {@link Aggregation} and {@code $facet}. Groups incoming documents
* based on the value of a specified expression and computes the count of documents in each distinct group.
* {@link SortByCountOperation} is equivalent to {@code { $group: { _id: <expression>, count: { $sum: 1 } } }, { $sort:
* { count: -1 } }}.
* <p/>
* <br />
* We recommend to use the static factory method {@link Aggregation#sortByCount(String)} instead of creating instances
* of this class directly.
*
* @see <a href=
* "https://docs.mongodb.com/manual/reference/operator/aggregation/sortByCount/">https://docs.mongodb.com/manual/reference/operator/aggregation/sortByCount/</a>
* @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/sortByCount/">https://docs.mongodb.com/manual/reference/operator/aggregation/sortByCount/</a>
* @author Jérôme Guyon
* @author Mark Paluch
* @since 2.1

View File

@@ -102,7 +102,7 @@ class SpelExpressionTransformer implements AggregationExpressionTransformer {
ExpressionState state = new ExpressionState(new StandardEvaluationContext(params), CONFIG);
ExpressionNode node = ExpressionNode.from(spelExpression.getAST(), state);
return transform(new AggregationExpressionTransformationContext<ExpressionNode>(node, null, null, context));
return transform(new AggregationExpressionTransformationContext<>(node, null, null, context));
}
/*
@@ -500,7 +500,10 @@ class SpelExpressionTransformer implements AggregationExpressionTransformer {
dbo.put(methodReference.getArgumentMap()[i++], transform(child, context));
}
args = dbo;
} else {
} else if (ObjectUtils.nullSafeEquals(methodReference.getArgumentType(), ArgumentType.EMPTY_DOCUMENT)) {
args = new Document();
}
else {
List<Object> argList = new ArrayList<Object>();

View File

@@ -18,8 +18,11 @@ package org.springframework.data.mongodb.core.aggregation;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import org.springframework.data.domain.Range;
import org.springframework.data.mongodb.util.RegexFlags;
import org.springframework.util.Assert;
/**
@@ -27,6 +30,7 @@ import org.springframework.util.Assert;
*
* @author Christoph Strobl
* @author Mark Paluch
* @author Divya Srivastava
* @since 1.10
*/
public class StringOperators {
@@ -516,6 +520,173 @@ public class StringOperators {
return usesFieldRef() ? RTrim.valueOf(fieldReference) : RTrim.valueOf(expression);
}
/**
* Creates new {@link AggregationExpression} that takes the associated string representation and applies the given
* regular expression to find the document with the first match.<br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @param regex must not be {@literal null}.
* @return new instance of {@link RegexFind}.
* @since 3.3
*/
public RegexFind regexFind(String regex) {
return createRegexFind().regex(regex);
}
/**
* Creates new {@link AggregationExpression} that takes the associated string representation and applies the regular
* expression resulting from the given {@link AggregationExpression} to find the document with the first
* match.<br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link RegexFind}.
* @since 3.3
*/
public RegexFind regexFind(AggregationExpression expression) {
return createRegexFind().regexOf(expression);
}
/**
* Creates new {@link AggregationExpression} that takes the {@link Pattern} and applies the regular expression with
* the options specified in the argument to find the document with the first match.
*
* @param pattern the pattern object to apply.
* @return new instance of {@link RegexFind}.
* @since 3.3
*/
public RegexFind regexFind(Pattern pattern) {
return createRegexFind().pattern(pattern);
}
/**
* Creates new {@link AggregationExpression} that takes the associated string representation and applies the regular
* expression with the options specified in the argument to find the document with the first match.
*
* @param regex the regular expression to apply.
* @param options the options to use.
* @return new instance of {@link RegexFind}.
* @since 3.3
*/
public RegexFind regexFind(String regex, String options) {
return createRegexFind().regex(regex).options(options);
}
private RegexFind createRegexFind() {
return usesFieldRef() ? RegexFind.valueOf(fieldReference) : RegexFind.valueOf(expression);
}
/**
* Creates new {@link AggregationExpression} that takes the associated string representation and applies the given
* regular expression to find all the documents with the match.<br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @param regex must not be {@literal null}.
* @return new instance of {@link RegexFindAll}.
* @since 3.3
*/
public RegexFindAll regexFindAll(String regex) {
return createRegexFindAll().regex(regex);
}
/**
* Creates new {@link AggregationExpression} that takes the associated string representation and applies the regular
* expression resulting from the given {@link AggregationExpression} to find all the documents with the
* match..<br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link RegexFindAll}.
* @since 3.3
*/
public RegexFindAll regexFindAll(AggregationExpression expression) {
return createRegexFindAll().regexOf(expression);
}
/**
* Creates new {@link AggregationExpression} that takes a {@link Pattern} and applies the regular expression with
* the options specified in the argument to find all the documents with the match.
*
* @param pattern the pattern object to apply.
* @return new instance of {@link RegexFindAll}.
* @since 3.3
*/
public RegexFindAll regexFindAll(Pattern pattern) {
return createRegexFindAll().pattern(pattern);
}
/**
* Creates new {@link AggregationExpression} that takes the associated string representation and applies the regular
* expression with the options specified in the argument to find all the documents with the match.
*
* @param regex the regular expression to apply.
* @param options the options to use.
* @return new instance of {@link RegexFindAll}.
* @since 3.3
*/
public RegexFindAll regexFindAll(String regex, String options) {
return createRegexFindAll().regex(regex).options(options);
}
private RegexFindAll createRegexFindAll() {
return usesFieldRef() ? RegexFindAll.valueOf(fieldReference) : RegexFindAll.valueOf(expression);
}
/**
* Creates new {@link AggregationExpression} that takes the associated string representation and applies the given
* regular expression to find if a match is found or not.<br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @param regex must not be {@literal null}.
* @return new instance of {@link RegexMatch}.
* @since 3.3
*/
public RegexMatch regexMatch(String regex) {
return createRegexMatch().regex(regex);
}
/**
* Creates new {@link AggregationExpression} that takes the associated string representation and applies the regular
* expression resulting from the given {@link AggregationExpression} to find if a match is found or not.<br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link RegexMatch}.
* @since 3.3
*/
public RegexMatch regexMatch(AggregationExpression expression) {
return createRegexMatch().regexOf(expression);
}
/**
* Creates new {@link AggregationExpression} that takes a {@link Pattern} and applies the regular expression with
* the options specified in the argument to find if a match is found or not.
*
* @param pattern the pattern object to apply.
* @return new instance of {@link RegexMatch}.
* @since 3.3
*/
public RegexMatch regexMatch(Pattern pattern) {
return createRegexMatch().pattern(pattern);
}
/**
* Creates new {@link AggregationExpression} that takes the associated string representation and applies the regular
* expression with the options specified in the argument to find if a match is found or not.
*
* @param regex the regular expression to apply.
* @param options the options to use.
* @return new instance of {@link RegexMatch}.
* @since 3.3
*/
public RegexMatch regexMatch(String regex, String options) {
return createRegexMatch().regex(regex).options(options);
}
private RegexMatch createRegexMatch() {
return usesFieldRef() ? RegexMatch.valueOf(fieldReference) : RegexMatch.valueOf(expression);
}
private boolean usesFieldRef() {
return fieldReference != null;
}
@@ -1477,4 +1648,434 @@ public class StringOperators {
return "$rtrim";
}
}
/**
* {@link AggregationExpression} for {@code $regexFind} which applies a regular expression (regex) to a string and
* returns information on the first matched substring. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @author Divya Srivastava
* @since 3.3
*/
public static class RegexFind extends AbstractAggregationExpression {
protected RegexFind(Object value) {
super(value);
}
/**
* Creates new {@link RegexFind} 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 RegexFind}.
*/
public static RegexFind valueOf(String fieldReference) {
Assert.notNull(fieldReference, "FieldReference must not be null!");
return new RegexFind(Collections.singletonMap("input", Fields.field(fieldReference)));
}
/**
* Creates new {@link RegexFind} using the result of the provided {@link AggregationExpression} as {@literal input}
* value.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link RegexFind}.
*/
public static RegexFind valueOf(AggregationExpression expression) {
Assert.notNull(expression, "Expression must not be null!");
return new RegexFind(Collections.singletonMap("input", expression));
}
/**
* Optional specify the options to use with the regular expression.
*
* @param options must not be {@literal null}.
* @return new instance of {@link RegexFind}.
*/
public RegexFind options(String options) {
Assert.notNull(options, "Options must not be null!");
return new RegexFind(append("options", options));
}
/**
* Optional specify the reference to the {@link Field field} holding the options values to use with the regular
* expression.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link RegexFind}.
*/
public RegexFind optionsOf(String fieldReference) {
Assert.notNull(fieldReference, "FieldReference must not be null!");
return new RegexFind(append("options", Fields.field(fieldReference)));
}
/**
* Optional specify the {@link AggregationExpression} evaluating to the options values to use with the regular
* expression.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link RegexFind}.
*/
public RegexFind optionsOf(AggregationExpression expression) {
Assert.notNull(expression, "Expression must not be null!");
return new RegexFind(append("options", expression));
}
/**
* Specify the regular expression to apply.
*
* @param regex must not be {@literal null}.
* @return new instance of {@link RegexFind}.
*/
public RegexFind regex(String regex) {
Assert.notNull(regex, "Regex must not be null!");
return new RegexFind(append("regex", regex));
}
/**
* Apply a {@link Pattern} into {@code regex} and {@code options} fields.
*
* @param pattern must not be {@literal null}.
* @return new instance of {@link RegexFind}.
*/
public RegexFind pattern(Pattern pattern) {
Assert.notNull(pattern, "Pattern must not be null!");
Map<String, Object> regex = append("regex", pattern.pattern());
regex.put("options", RegexFlags.toRegexOptions(pattern.flags()));
return new RegexFind(regex);
}
/**
* Specify the reference to the {@link Field field} holding the regular expression to apply.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link RegexFind}.
*/
public RegexFind regexOf(String fieldReference) {
Assert.notNull(fieldReference, "fieldReference must not be null!");
return new RegexFind(append("regex", Fields.field(fieldReference)));
}
/**
* Specify the {@link AggregationExpression} evaluating to the regular expression to apply.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link RegexFind}.
*/
public RegexFind regexOf(AggregationExpression expression) {
Assert.notNull(expression, "Expression must not be null!");
return new RegexFind(append("regex", expression));
}
@Override
protected String getMongoMethod() {
return "$regexFind";
}
}
/**
* {@link AggregationExpression} for {@code $regexFindAll} which applies a regular expression (regex) to a string and
* returns information on all the matched substrings. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @author Divya Srivastava
* @since 3.3
*/
public static class RegexFindAll extends AbstractAggregationExpression {
protected RegexFindAll(Object value) {
super(value);
}
/**
* Creates new {@link RegexFindAll} 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 RegexFindAll}.
*/
public static RegexFindAll valueOf(String fieldReference) {
Assert.notNull(fieldReference, "FieldReference must not be null!");
return new RegexFindAll(Collections.singletonMap("input", Fields.field(fieldReference)));
}
/**
* Creates new {@link RegexFindAll} using the result of the provided {@link AggregationExpression} as
* {@literal input} value.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link RegexFindAll}.
*/
public static RegexFindAll valueOf(AggregationExpression expression) {
Assert.notNull(expression, "Expression must not be null!");
return new RegexFindAll(Collections.singletonMap("input", expression));
}
/**
* Optional specify the options to use with the regular expression.
*
* @param options must not be {@literal null}.
* @return new instance of {@link RegexFindAll}.
*/
public RegexFindAll options(String options) {
Assert.notNull(options, "Options must not be null!");
return new RegexFindAll(append("options", options));
}
/**
* Optional specify the reference to the {@link Field field} holding the options values to use with the regular
* expression.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link RegexFindAll}.
*/
public RegexFindAll optionsOf(String fieldReference) {
Assert.notNull(fieldReference, "fieldReference must not be null!");
return new RegexFindAll(append("options", Fields.field(fieldReference)));
}
/**
* Optional specify the {@link AggregationExpression} evaluating to the options values to use with the regular
* expression.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link RegexFindAll}.
*/
public RegexFindAll optionsOf(AggregationExpression expression) {
Assert.notNull(expression, "Expression must not be null!");
return new RegexFindAll(append("options", expression));
}
/**
* Apply a {@link Pattern} into {@code regex} and {@code options} fields.
*
* @param pattern must not be {@literal null}.
* @return new instance of {@link RegexFindAll}.
*/
public RegexFindAll pattern(Pattern pattern) {
Assert.notNull(pattern, "Pattern must not be null!");
Map<String, Object> regex = append("regex", pattern.pattern());
regex.put("options", RegexFlags.toRegexOptions(pattern.flags()));
return new RegexFindAll(regex);
}
/**
* Specify the regular expression to apply.
*
* @param regex must not be {@literal null}.
* @return new instance of {@link RegexFindAll}.
*/
public RegexFindAll regex(String regex) {
Assert.notNull(regex, "Regex must not be null!");
return new RegexFindAll(append("regex", regex));
}
/**
* Specify the reference to the {@link Field field} holding the regular expression to apply.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link RegexFindAll}.
*/
public RegexFindAll regexOf(String fieldReference) {
Assert.notNull(fieldReference, "fieldReference must not be null!");
return new RegexFindAll(append("regex", Fields.field(fieldReference)));
}
/**
* Specify the {@link AggregationExpression} evaluating to the regular expression to apply.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link RegexFindAll}.
*/
public RegexFindAll regexOf(AggregationExpression expression) {
Assert.notNull(expression, "Expression must not be null!");
return new RegexFindAll(append("regex", expression));
}
@Override
protected String getMongoMethod() {
return "$regexFindAll";
}
}
/**
* {@link AggregationExpression} for {@code $regexMatch} which applies a regular expression (regex) to a string and
* returns a boolean that indicates if a match is found or not. <br />
* <strong>NOTE:</strong> Requires MongoDB 4.0 or later.
*
* @author Divya Srivastava
* @since 3.3
*/
public static class RegexMatch extends AbstractAggregationExpression {
protected RegexMatch(Object value) {
super(value);
}
/**
* Creates new {@link RegexMatch} 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 RegexMatch}.
*/
public static RegexMatch valueOf(String fieldReference) {
Assert.notNull(fieldReference, "FieldReference must not be null!");
return new RegexMatch(Collections.singletonMap("input", Fields.field(fieldReference)));
}
/**
* Creates new {@link RegexMatch} using the result of the provided {@link AggregationExpression} as {@literal input}
* value.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link RegexMatch}.
*/
public static RegexMatch valueOf(AggregationExpression expression) {
Assert.notNull(expression, "Expression must not be null!");
return new RegexMatch(Collections.singletonMap("input", expression));
}
/**
* Optional specify the options to use with the regular expression.
*
* @param options must not be {@literal null}.
* @return new instance of {@link RegexMatch}.
*/
public RegexMatch options(String options) {
Assert.notNull(options, "Options must not be null!");
return new RegexMatch(append("options", options));
}
/**
* Optional specify the reference to the {@link Field field} holding the options values to use with the regular
* expression.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link RegexMatch}.
*/
public RegexMatch optionsOf(String fieldReference) {
Assert.notNull(fieldReference, "FieldReference must not be null!");
return new RegexMatch(append("options", Fields.field(fieldReference)));
}
/**
* Optional specify the {@link AggregationExpression} evaluating to the options values to use with the regular
* expression.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link RegexMatch}.
*/
public RegexMatch optionsOf(AggregationExpression expression) {
Assert.notNull(expression, "Expression must not be null!");
return new RegexMatch(append("options", expression));
}
/**
* Apply a {@link Pattern} into {@code regex} and {@code options} fields.
*
* @param pattern must not be {@literal null}.
* @return new instance of {@link RegexMatch}.
*/
public RegexMatch pattern(Pattern pattern) {
Assert.notNull(pattern, "Pattern must not be null!");
Map<String, Object> regex = append("regex", pattern.pattern());
regex.put("options", RegexFlags.toRegexOptions(pattern.flags()));
return new RegexMatch(regex);
}
/**
* Specify the regular expression to apply.
*
* @param regex must not be {@literal null}.
* @return new instance of {@link RegexMatch}.
*/
public RegexMatch regex(String regex) {
Assert.notNull(regex, "Regex must not be null!");
return new RegexMatch(append("regex", regex));
}
/**
* Specify the reference to the {@link Field field} holding the regular expression to apply.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link RegexMatch}.
*/
public RegexMatch regexOf(String fieldReference) {
Assert.notNull(fieldReference, "FieldReference must not be null!");
return new RegexMatch(append("regex", Fields.field(fieldReference)));
}
/**
* Optional specify the {@link AggregationExpression} evaluating to the regular expression to apply.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link RegexMatch}.
*/
public RegexMatch regexOf(AggregationExpression expression) {
Assert.notNull(expression, "Expression must not be null!");
return new RegexMatch(append("regex", expression));
}
@Override
protected String getMongoMethod() {
return "$regexMatch";
}
}
}

View File

@@ -28,7 +28,7 @@ import org.springframework.util.Assert;
* containing duplicates, into a single result set that is handed over to the next stage. <br />
* In order to remove duplicates it is possible to append a {@link GroupOperation} right after
* {@link UnionWithOperation}.
* <p />
* <br />
* If the {@link UnionWithOperation} uses a
* <a href="https://docs.mongodb.com/master/reference/operator/aggregation/unionWith/#unionwith-pipeline">pipeline</a>
* to process documents, field names within the pipeline will be treated as is. In order to map domain type property

View File

@@ -35,7 +35,7 @@ import com.mongodb.DBRef;
* @author Mark Paluch
* @since 1.4
*/
public interface DbRefResolver {
public interface DbRefResolver extends ReferenceResolver {
/**
* Resolves the given {@link DBRef} into an object of the given {@link MongoPersistentProperty}'s type. The method

View File

@@ -15,13 +15,6 @@
*/
package org.springframework.data.mongodb.core.convert;
import static org.springframework.util.ReflectionUtils.*;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -29,28 +22,18 @@ import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.bson.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.cglib.proxy.Callback;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.Factory;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.mongodb.ClientSessionException;
import org.springframework.data.mongodb.LazyLoadingException;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.MongoDatabaseUtils;
import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery;
import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentProperty;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.lang.Nullable;
import org.springframework.objenesis.ObjenesisStd;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import com.mongodb.DBRef;
@@ -67,13 +50,11 @@ import com.mongodb.client.model.Filters;
* @author Mark Paluch
* @since 1.4
*/
public class DefaultDbRefResolver implements DbRefResolver {
public class DefaultDbRefResolver extends DefaultReferenceResolver implements DbRefResolver, ReferenceResolver {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultDbRefResolver.class);
private final MongoDatabaseFactory mongoDbFactory;
private final PersistenceExceptionTranslator exceptionTranslator;
private final ObjenesisStd objenesis;
/**
* Creates a new {@link DefaultDbRefResolver} with the given {@link MongoDatabaseFactory}.
@@ -82,11 +63,11 @@ public class DefaultDbRefResolver implements DbRefResolver {
*/
public DefaultDbRefResolver(MongoDatabaseFactory mongoDbFactory) {
super(new MongoDatabaseFactoryReferenceLoader(mongoDbFactory), mongoDbFactory.getExceptionTranslator());
Assert.notNull(mongoDbFactory, "MongoDbFactory translator must not be null!");
this.mongoDbFactory = mongoDbFactory;
this.exceptionTranslator = mongoDbFactory.getExceptionTranslator();
this.objenesis = new ObjenesisStd(true);
}
/*
@@ -114,17 +95,8 @@ public class DefaultDbRefResolver implements DbRefResolver {
*/
@Override
public Document fetch(DBRef dbRef) {
MongoCollection<Document> mongoCollection = getCollection(dbRef);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Fetching DBRef '{}' from {}.{}.", dbRef.getId(),
StringUtils.hasText(dbRef.getDatabaseName()) ? dbRef.getDatabaseName()
: mongoCollection.getNamespace().getDatabaseName(),
dbRef.getCollectionName());
}
return mongoCollection.find(Filters.eq("_id", dbRef.getId())).first();
return getReferenceLoader().fetchOne(DocumentReferenceQuery.forSingleDocument(Filters.eq("_id", dbRef.getId())),
ReferenceCollection.fromDBRef(dbRef));
}
/*
@@ -165,7 +137,7 @@ public class DefaultDbRefResolver implements DbRefResolver {
}
List<Document> result = mongoCollection //
.find(new Document("_id", new Document("$in", ids))) //
.find(new Document(BasicMongoPersistentProperty.ID_FIELD_NAME, new Document("$in", ids))) //
.into(new ArrayList<>());
return ids.stream() //
@@ -185,44 +157,9 @@ public class DefaultDbRefResolver implements DbRefResolver {
private Object createLazyLoadingProxy(MongoPersistentProperty property, @Nullable DBRef dbref,
DbRefResolverCallback callback, DbRefProxyHandler handler) {
Class<?> propertyType = property.getType();
LazyLoadingInterceptor interceptor = new LazyLoadingInterceptor(property, dbref, exceptionTranslator, callback);
Object lazyLoadingProxy = getProxyFactory().createLazyLoadingProxy(property, callback, dbref);
if (!propertyType.isInterface()) {
Factory factory = (Factory) objenesis.newInstance(getEnhancedTypeFor(propertyType));
factory.setCallbacks(new Callback[] { interceptor });
return handler.populateId(property, dbref, factory);
}
ProxyFactory proxyFactory = new ProxyFactory();
for (Class<?> type : propertyType.getInterfaces()) {
proxyFactory.addInterface(type);
}
proxyFactory.addInterface(LazyLoadingProxy.class);
proxyFactory.addInterface(propertyType);
proxyFactory.addAdvice(interceptor);
return handler.populateId(property, dbref, proxyFactory.getProxy(LazyLoadingProxy.class.getClassLoader()));
}
/**
* Returns the CGLib enhanced type for the given source type.
*
* @param type
* @return
*/
private Class<?> getEnhancedTypeFor(Class<?> type) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(type);
enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class);
enhancer.setInterfaces(new Class[] { LazyLoadingProxy.class });
return enhancer.createClass();
return handler.populateId(property, dbref, lazyLoadingProxy);
}
/**
@@ -245,253 +182,10 @@ public class DefaultDbRefResolver implements DbRefResolver {
private static Stream<Document> documentWithId(Object identifier, Collection<Document> documents) {
return documents.stream() //
.filter(it -> it.get("_id").equals(identifier)) //
.filter(it -> it.get(BasicMongoPersistentProperty.ID_FIELD_NAME).equals(identifier)) //
.limit(1);
}
/**
* A {@link MethodInterceptor} that is used within a lazy loading proxy. The property resolving is delegated to a
* {@link DbRefResolverCallback}. The resolving process is triggered by a method invocation on the proxy and is
* guaranteed to be performed only once.
*
* @author Thomas Darimont
* @author Oliver Gierke
* @author Christoph Strobl
*/
static class LazyLoadingInterceptor
implements MethodInterceptor, org.springframework.cglib.proxy.MethodInterceptor, Serializable {
private static final Method INITIALIZE_METHOD, TO_DBREF_METHOD, FINALIZE_METHOD;
private final DbRefResolverCallback callback;
private final MongoPersistentProperty property;
private final PersistenceExceptionTranslator exceptionTranslator;
private volatile boolean resolved;
private final @Nullable DBRef dbref;
private @Nullable Object result;
static {
try {
INITIALIZE_METHOD = LazyLoadingProxy.class.getMethod("getTarget");
TO_DBREF_METHOD = LazyLoadingProxy.class.getMethod("toDBRef");
FINALIZE_METHOD = Object.class.getDeclaredMethod("finalize");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Creates a new {@link LazyLoadingInterceptor} for the given {@link MongoPersistentProperty},
* {@link PersistenceExceptionTranslator} and {@link DbRefResolverCallback}.
*
* @param property must not be {@literal null}.
* @param dbref can be {@literal null}.
* @param callback must not be {@literal null}.
*/
public LazyLoadingInterceptor(MongoPersistentProperty property, @Nullable DBRef dbref,
PersistenceExceptionTranslator exceptionTranslator, DbRefResolverCallback callback) {
Assert.notNull(property, "Property must not be null!");
Assert.notNull(exceptionTranslator, "Exception translator must not be null!");
Assert.notNull(callback, "Callback must not be null!");
this.dbref = dbref;
this.callback = callback;
this.exceptionTranslator = exceptionTranslator;
this.property = property;
}
/*
* (non-Javadoc)
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
*/
@Override
public Object invoke(@Nullable MethodInvocation invocation) throws Throwable {
return intercept(invocation.getThis(), invocation.getMethod(), invocation.getArguments(), null);
}
/*
* (non-Javadoc)
* @see org.springframework.cglib.proxy.MethodInterceptor#intercept(java.lang.Object, java.lang.reflect.Method, java.lang.Object[], org.springframework.cglib.proxy.MethodProxy)
*/
@Nullable
@Override
public Object intercept(Object obj, Method method, Object[] args, @Nullable MethodProxy proxy) throws Throwable {
if (INITIALIZE_METHOD.equals(method)) {
return ensureResolved();
}
if (TO_DBREF_METHOD.equals(method)) {
return this.dbref;
}
if (isObjectMethod(method) && Object.class.equals(method.getDeclaringClass())) {
if (ReflectionUtils.isToStringMethod(method)) {
return proxyToString(proxy);
}
if (ReflectionUtils.isEqualsMethod(method)) {
return proxyEquals(proxy, args[0]);
}
if (ReflectionUtils.isHashCodeMethod(method)) {
return proxyHashCode(proxy);
}
// DATAMONGO-1076 - finalize methods should not trigger proxy initialization
if (FINALIZE_METHOD.equals(method)) {
return null;
}
}
Object target = ensureResolved();
if (target == null) {
return null;
}
ReflectionUtils.makeAccessible(method);
return method.invoke(target, args);
}
/**
* Returns a to string representation for the given {@code proxy}.
*
* @param proxy
* @return
*/
private String proxyToString(@Nullable Object proxy) {
StringBuilder description = new StringBuilder();
if (dbref != null) {
description.append(dbref.getCollectionName());
description.append(":");
description.append(dbref.getId());
} else {
description.append(System.identityHashCode(proxy));
}
description.append("$").append(LazyLoadingProxy.class.getSimpleName());
return description.toString();
}
/**
* Returns the hashcode for the given {@code proxy}.
*
* @param proxy
* @return
*/
private int proxyHashCode(@Nullable Object proxy) {
return proxyToString(proxy).hashCode();
}
/**
* Performs an equality check for the given {@code proxy}.
*
* @param proxy
* @param that
* @return
*/
private boolean proxyEquals(@Nullable Object proxy, Object that) {
if (!(that instanceof LazyLoadingProxy)) {
return false;
}
if (that == proxy) {
return true;
}
return proxyToString(proxy).equals(that.toString());
}
/**
* Will trigger the resolution if the proxy is not resolved already or return a previously resolved result.
*
* @return
*/
@Nullable
private Object ensureResolved() {
if (!resolved) {
this.result = resolve();
this.resolved = true;
}
return this.result;
}
/**
* Callback method for serialization.
*
* @param out
* @throws IOException
*/
private void writeObject(ObjectOutputStream out) throws IOException {
ensureResolved();
out.writeObject(this.result);
}
/**
* Callback method for deserialization.
*
* @param in
* @throws IOException
*/
private void readObject(ObjectInputStream in) throws IOException {
try {
this.resolved = true;
this.result = in.readObject();
} catch (ClassNotFoundException e) {
throw new LazyLoadingException("Could not deserialize result", e);
}
}
/**
* Resolves the proxy into its backing object.
*
* @return
*/
@Nullable
private synchronized Object resolve() {
if (resolved) {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Accessing already resolved lazy loading property {}.{}",
property.getOwner() != null ? property.getOwner().getName() : "unknown", property.getName());
}
return result;
}
try {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Resolving lazy loading property {}.{}",
property.getOwner() != null ? property.getOwner().getName() : "unknown", property.getName());
}
return callback.resolve(property);
} catch (RuntimeException ex) {
DataAccessException translatedException = this.exceptionTranslator.translateExceptionIfPossible(ex);
if (translatedException instanceof ClientSessionException) {
throw new LazyLoadingException("Unable to lazily resolve DBRef! Invalid session state.", ex);
}
throw new LazyLoadingException("Unable to lazily resolve DBRef!",
translatedException != null ? translatedException : ex);
}
}
}
/**
* Customization hook for obtaining the {@link MongoCollection} for a given {@link DBRef}.
*
@@ -504,4 +198,10 @@ public class DefaultDbRefResolver implements DbRefResolver {
return MongoDatabaseUtils.getDatabase(dbref.getDatabaseName(), mongoDbFactory)
.getCollection(dbref.getCollectionName(), Document.class);
}
protected MongoCollection<Document> getCollection(ReferenceCollection context) {
return MongoDatabaseUtils.getDatabase(context.getDatabase(), mongoDbFactory).getCollection(context.getCollection(),
Document.class);
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2021 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
*
* https://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 static org.springframework.data.mongodb.core.convert.ReferenceLookupDelegate.*;
import java.util.Collections;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.DocumentReference;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.util.Assert;
/**
* {@link ReferenceResolver} implementation that uses a given {@link ReferenceLookupDelegate} to load and convert entity
* associations expressed via a {@link MongoPersistentProperty persitent property}. Creates {@link LazyLoadingProxy
* proxies} for associations that should be lazily loaded.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 3.3
*/
public class DefaultReferenceResolver implements ReferenceResolver {
private final ReferenceLoader referenceLoader;
private final LazyLoadingProxyFactory proxyFactory;
private final LookupFunction collectionLookupFunction = (filter, ctx) -> getReferenceLoader().fetchMany(filter, ctx);
private final LookupFunction singleValueLookupFunction = (filter, ctx) -> {
Object target = getReferenceLoader().fetchOne(filter, ctx);
return target == null ? Collections.emptyList() : Collections.singleton(getReferenceLoader().fetchOne(filter, ctx));
};
/**
* Create a new instance of {@link DefaultReferenceResolver}.
*
* @param referenceLoader must not be {@literal null}.
* @param exceptionTranslator must not be {@literal null}.
*/
public DefaultReferenceResolver(ReferenceLoader referenceLoader, PersistenceExceptionTranslator exceptionTranslator) {
Assert.notNull(referenceLoader, "ReferenceLoader must not be null!");
Assert.notNull(exceptionTranslator, "ExceptionTranslator must not be null!");
this.referenceLoader = referenceLoader;
this.proxyFactory = new LazyLoadingProxyFactory(exceptionTranslator);
}
@Override
public Object resolveReference(MongoPersistentProperty property, Object source,
ReferenceLookupDelegate referenceLookupDelegate, MongoEntityReader entityReader) {
LookupFunction lookupFunction = (property.isCollectionLike() || property.isMap()) ? collectionLookupFunction
: singleValueLookupFunction;
if (isLazyReference(property)) {
return createLazyLoadingProxy(property, source, referenceLookupDelegate, lookupFunction, entityReader);
}
return referenceLookupDelegate.readReference(property, source, lookupFunction, entityReader);
}
/**
* Check if the association expressed by the given {@link MongoPersistentProperty property} should be resolved lazily.
*
* @param property
* @return return {@literal true} if the defined association is lazy.
* @see DBRef#lazy()
* @see DocumentReference#lazy()
*/
protected boolean isLazyReference(MongoPersistentProperty property) {
if (property.isDocumentReference()) {
return property.getDocumentReference().lazy();
}
return property.getDBRef() != null && property.getDBRef().lazy();
}
/**
* The {@link ReferenceLoader} executing the lookup.
*
* @return never {@literal null}.
*/
protected ReferenceLoader getReferenceLoader() {
return referenceLoader;
}
LazyLoadingProxyFactory getProxyFactory() {
return proxyFactory;
}
private Object createLazyLoadingProxy(MongoPersistentProperty property, Object source,
ReferenceLookupDelegate referenceLookupDelegate, LookupFunction lookupFunction, MongoEntityReader entityReader) {
return proxyFactory.createLazyLoadingProxy(property, it -> {
return referenceLookupDelegate.readReference(it, source, lookupFunction, entityReader);
}, source instanceof DocumentReferenceSource ? ((DocumentReferenceSource)source).getTargetSource() : source);
}
}

View File

@@ -135,7 +135,7 @@ class DocumentAccessor {
*/
@Nullable
public Object getRawId(MongoPersistentEntity<?> entity) {
return entity.hasIdProperty() ? get(entity.getRequiredIdProperty()) : BsonUtils.asMap(document).get("_id");
return entity.hasIdProperty() ? get(entity.getRequiredIdProperty()) : BsonUtils.get(document, "_id");
}
/**

View File

@@ -0,0 +1,259 @@
/*
* Copyright 2021 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
*
* https://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.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.WeakHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.bson.Document;
import org.bson.types.ObjectId;
import org.springframework.core.convert.ConversionService;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.annotation.Reference;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.BeanWrapperPropertyAccessorFactory;
import org.springframework.data.mongodb.core.mapping.DocumentPointer;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
/**
* Internal API to construct {@link DocumentPointer} for a given property. Considers {@link LazyLoadingProxy},
* registered {@link Object} to {@link DocumentPointer} {@link org.springframework.core.convert.converter.Converter},
* simple {@literal _id} lookups and cases where the {@link DocumentPointer} needs to be computed via a lookup query.
*
* @author Christoph Strobl
* @since 3.3
*/
class DocumentPointerFactory {
private final ConversionService conversionService;
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
private final Map<String, LinkageDocument> cache;
/**
* A {@link Pattern} matching quoted and unquoted variants (with/out whitespaces) of
* <code>{'_id' : ?#{#target} }</code>.
*/
private static final Pattern DEFAULT_LOOKUP_PATTERN = Pattern.compile("\\{\\s?" + // document start (whitespace opt)
"['\"]?_id['\"]?" + // followed by an optionally quoted _id. Like: _id, '_id' or "_id"
"?\\s?:\\s?" + // then a colon optionally wrapped inside whitespaces
"['\"]?\\?#\\{#target\\}['\"]?" + // leading to the potentially quoted ?#{#target} expression
"\\s*}"); // some optional whitespaces and document close
DocumentPointerFactory(ConversionService conversionService,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
this.conversionService = conversionService;
this.mappingContext = mappingContext;
this.cache = new WeakHashMap<>();
}
DocumentPointer<?> computePointer(
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
MongoPersistentProperty property, Object value, Class<?> typeHint) {
if (value instanceof LazyLoadingProxy) {
return () -> ((LazyLoadingProxy) value).getSource();
}
if (conversionService.canConvert(typeHint, DocumentPointer.class)) {
return conversionService.convert(value, DocumentPointer.class);
}
MongoPersistentEntity<?> persistentEntity = mappingContext
.getRequiredPersistentEntity(property.getAssociationTargetType());
if (usesDefaultLookup(property)) {
MongoPersistentProperty idProperty = persistentEntity.getIdProperty();
Object idValue = persistentEntity.getIdentifierAccessor(value).getIdentifier();
if (idProperty.hasExplicitWriteTarget()
&& conversionService.canConvert(idValue.getClass(), idProperty.getFieldType())) {
return () -> conversionService.convert(idValue, idProperty.getFieldType());
}
if (idValue instanceof String && ObjectId.isValid((String) idValue)) {
return () -> new ObjectId((String) idValue);
}
return () -> idValue;
}
MongoPersistentEntity<?> valueEntity = mappingContext.getPersistentEntity(value.getClass());
PersistentPropertyAccessor<Object> propertyAccessor;
if (valueEntity == null) {
propertyAccessor = BeanWrapperPropertyAccessorFactory.INSTANCE.getPropertyAccessor(property.getOwner(), value);
} else {
propertyAccessor = valueEntity.getPropertyPathAccessor(value);
}
return cache.computeIfAbsent(property.getDocumentReference().lookup(), LinkageDocument::from)
.getDocumentPointer(mappingContext, persistentEntity, propertyAccessor);
}
private boolean usesDefaultLookup(MongoPersistentProperty property) {
if (property.isDocumentReference()) {
return DEFAULT_LOOKUP_PATTERN.matcher(property.getDocumentReference().lookup()).matches();
}
Reference atReference = property.findAnnotation(Reference.class);
if (atReference != null) {
return true;
}
throw new IllegalStateException(String.format("%s does not seem to be define Reference", property));
}
/**
* Value object that computes a document pointer from a given lookup query by identifying SpEL expressions and
* inverting it.
*
* <pre class="code">
* // source
* { 'firstname' : ?#{fn}, 'lastname' : '?#{ln} }
*
* // target
* { 'fn' : ..., 'ln' : ... }
* </pre>
*
* The actual pointer is the computed via
* {@link #getDocumentPointer(MappingContext, MongoPersistentEntity, PersistentPropertyAccessor)} applying values from
* the provided {@link PersistentPropertyAccessor} to the target document by looking at the keys of the expressions
* from the source.
*/
static class LinkageDocument {
static final Pattern EXPRESSION_PATTERN = Pattern.compile("\\?#\\{#?(?<fieldName>[\\w\\d\\.\\-)]*)\\}");
static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("###_(?<index>\\d*)_###");
private final String lookup;
private final org.bson.Document documentPointer;
private final Map<String, String> placeholderMap;
private final boolean isSimpleTargetPointer;
static LinkageDocument from(String lookup) {
return new LinkageDocument(lookup);
}
private LinkageDocument(String lookup) {
this.lookup = lookup;
this.placeholderMap = new LinkedHashMap<>();
int index = 0;
Matcher matcher = EXPRESSION_PATTERN.matcher(lookup);
String targetLookup = lookup;
while (matcher.find()) {
String expression = matcher.group();
String fieldName = matcher.group("fieldName").replace("target.", "");
String placeholder = placeholder(index);
placeholderMap.put(placeholder, fieldName);
targetLookup = targetLookup.replace(expression, "'" + placeholder + "'");
index++;
}
this.documentPointer = org.bson.Document.parse(targetLookup);
this.isSimpleTargetPointer = placeholderMap.size() == 1 && placeholderMap.containsValue("target")
&& lookup.contains("#target");
}
private String placeholder(int index) {
return "###_" + index + "_###";
}
private boolean isPlaceholder(String key) {
return PLACEHOLDER_PATTERN.matcher(key).matches();
}
DocumentPointer<Object> getDocumentPointer(
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
MongoPersistentEntity<?> persistentEntity, PersistentPropertyAccessor<?> propertyAccessor) {
return () -> updatePlaceholders(documentPointer, new Document(), mappingContext, persistentEntity,
propertyAccessor);
}
Object updatePlaceholders(org.bson.Document source, org.bson.Document target,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
MongoPersistentEntity<?> persistentEntity, PersistentPropertyAccessor<?> propertyAccessor) {
for (Entry<String, Object> entry : source.entrySet()) {
if (entry.getKey().startsWith("$")) {
throw new InvalidDataAccessApiUsageException(String.format(
"Cannot derive document pointer from lookup '%s' using query operator (%s). Please consider registering a custom converter.",
lookup, entry.getKey()));
}
if (entry.getValue() instanceof Document) {
MongoPersistentProperty persistentProperty = persistentEntity.getPersistentProperty(entry.getKey());
if (persistentProperty != null && persistentProperty.isEntity()) {
MongoPersistentEntity<?> nestedEntity = mappingContext.getPersistentEntity(persistentProperty.getType());
target.put(entry.getKey(), updatePlaceholders((Document) entry.getValue(), new Document(), mappingContext,
nestedEntity, nestedEntity.getPropertyAccessor(propertyAccessor.getProperty(persistentProperty))));
} else {
target.put(entry.getKey(), updatePlaceholders((Document) entry.getValue(), new Document(), mappingContext,
persistentEntity, propertyAccessor));
}
continue;
}
if (placeholderMap.containsKey(entry.getValue())) {
String attribute = placeholderMap.get(entry.getValue());
if (attribute.contains(".")) {
attribute = attribute.substring(attribute.lastIndexOf('.') + 1);
}
String fieldName = entry.getKey().equals("_id") ? "id" : entry.getKey();
if (!fieldName.contains(".")) {
Object targetValue = propertyAccessor.getProperty(persistentEntity.getPersistentProperty(fieldName));
target.put(attribute, targetValue);
continue;
}
PersistentPropertyPath<?> path = mappingContext
.getPersistentPropertyPath(PropertyPath.from(fieldName, persistentEntity.getTypeInformation()));
Object targetValue = propertyAccessor.getProperty(path);
target.put(attribute, targetValue);
continue;
}
target.put(entry.getKey(), entry.getValue());
}
if (target.size() == 1 && isSimpleTargetPointer) {
return target.values().iterator().next();
}
return target;
}
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2021 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
*
* https://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 org.springframework.lang.Nullable;
/**
* The source object to resolve document references upon. Encapsulates the actual source and the reference specific
* values.
*
* @author Christoph Strobl
* @since 3.3
*/
public class DocumentReferenceSource {
private final Object self;
private final @Nullable Object targetSource;
/**
* Create a new instance of {@link DocumentReferenceSource}.
*
* @param self the entire wrapper object holding references. Must not be {@literal null}.
* @param targetSource the reference value source.
*/
DocumentReferenceSource(Object self, @Nullable Object targetSource) {
this.self = self;
this.targetSource = targetSource;
}
/**
* Get the outer document.
*
* @return never {@literal null}.
*/
public Object getSelf() {
return self;
}
/**
* Get the actual (property specific) reference value.
*
* @return can be {@literal null}.
*/
@Nullable
public Object getTargetSource() {
return targetSource;
}
/**
* Dereference a {@code targetSource} if it is a {@link DocumentReferenceSource} or return {@code source} otherwise.
*
* @param source
* @return
*/
@Nullable
static Object getTargetSource(Object source) {
return source instanceof DocumentReferenceSource ? ((DocumentReferenceSource) source).getTargetSource() : source;
}
/**
* Dereference a {@code self} object if it is a {@link DocumentReferenceSource} or return {@code self} otherwise.
*
* @param self
* @return
*/
static Object getSelf(Object self) {
return self instanceof DocumentReferenceSource ? ((DocumentReferenceSource) self).getSelf() : self;
}
}

View File

@@ -15,18 +15,18 @@
*/
package org.springframework.data.mongodb.core.convert;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver.LazyLoadingInterceptor;
import org.springframework.lang.Nullable;
import com.mongodb.DBRef;
/**
* Allows direct interaction with the underlying {@link LazyLoadingInterceptor}.
* Allows direct interaction with the underlying {@code LazyLoadingInterceptor}.
*
* @author Thomas Darimont
* @author Christoph Strobl
* @author Mark Paluch
* @since 1.5
* @see LazyLoadingProxyFactory
*/
public interface LazyLoadingProxy {
@@ -46,4 +46,15 @@ public interface LazyLoadingProxy {
*/
@Nullable
DBRef toDBRef();
/**
* Returns the raw {@literal source} object that defines the reference.
*
* @return can be {@literal null}.
* @since 3.3
*/
@Nullable
default Object getSource() {
return toDBRef();
}
}

View File

@@ -0,0 +1,303 @@
/*
* Copyright 2021 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
*
* https://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 static org.springframework.util.ReflectionUtils.*;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Method;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.cglib.proxy.Callback;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.Factory;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.mongodb.ClientSessionException;
import org.springframework.data.mongodb.LazyLoadingException;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.lang.Nullable;
import org.springframework.objenesis.ObjenesisStd;
import org.springframework.util.ReflectionUtils;
import com.mongodb.DBRef;
/**
* {@link ProxyFactory} to create a proxy for {@link MongoPersistentProperty#getType()} to resolve a reference lazily.
*
* @author Christoph Strobl
* @author Mark Paluch
*/
class LazyLoadingProxyFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(LazyLoadingProxyFactory.class);
private final ObjenesisStd objenesis;
private final PersistenceExceptionTranslator exceptionTranslator;
public LazyLoadingProxyFactory(PersistenceExceptionTranslator exceptionTranslator) {
this.exceptionTranslator = exceptionTranslator;
this.objenesis = new ObjenesisStd(true);
}
public Object createLazyLoadingProxy(MongoPersistentProperty property, DbRefResolverCallback callback,
Object source) {
Class<?> propertyType = property.getType();
LazyLoadingInterceptor interceptor = new LazyLoadingInterceptor(property, callback, source, exceptionTranslator);
if (!propertyType.isInterface()) {
Factory factory = (Factory) objenesis.newInstance(getEnhancedTypeFor(propertyType));
factory.setCallbacks(new Callback[] { interceptor });
return factory;
}
ProxyFactory proxyFactory = new ProxyFactory();
for (Class<?> type : propertyType.getInterfaces()) {
proxyFactory.addInterface(type);
}
proxyFactory.addInterface(LazyLoadingProxy.class);
proxyFactory.addInterface(propertyType);
proxyFactory.addAdvice(interceptor);
return proxyFactory.getProxy(LazyLoadingProxy.class.getClassLoader());
}
/**
* Returns the CGLib enhanced type for the given source type.
*
* @param type
* @return
*/
private Class<?> getEnhancedTypeFor(Class<?> type) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(type);
enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class);
enhancer.setInterfaces(new Class[] { LazyLoadingProxy.class });
return enhancer.createClass();
}
public static class LazyLoadingInterceptor
implements MethodInterceptor, org.springframework.cglib.proxy.MethodInterceptor, Serializable {
private static final Method INITIALIZE_METHOD, TO_DBREF_METHOD, FINALIZE_METHOD, GET_SOURCE_METHOD;
static {
try {
INITIALIZE_METHOD = LazyLoadingProxy.class.getMethod("getTarget");
TO_DBREF_METHOD = LazyLoadingProxy.class.getMethod("toDBRef");
FINALIZE_METHOD = Object.class.getDeclaredMethod("finalize");
GET_SOURCE_METHOD = LazyLoadingProxy.class.getMethod("getSource");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private final MongoPersistentProperty property;
private final DbRefResolverCallback callback;
private final Object source;
private final PersistenceExceptionTranslator exceptionTranslator;
private volatile boolean resolved;
private @Nullable Object result;
public LazyLoadingInterceptor(MongoPersistentProperty property, DbRefResolverCallback callback, Object source,
PersistenceExceptionTranslator exceptionTranslator) {
this.property = property;
this.callback = callback;
this.source = source;
this.exceptionTranslator = exceptionTranslator;
}
@Nullable
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
return intercept(invocation.getThis(), invocation.getMethod(), invocation.getArguments(), null);
}
@Nullable
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy proxy) throws Throwable {
if (INITIALIZE_METHOD.equals(method)) {
return ensureResolved();
}
if (TO_DBREF_METHOD.equals(method)) {
return source instanceof DBRef ? source : null;
}
if (GET_SOURCE_METHOD.equals(method)) {
return source;
}
if (isObjectMethod(method) && Object.class.equals(method.getDeclaringClass())) {
if (ReflectionUtils.isToStringMethod(method)) {
return proxyToString(source);
}
if (ReflectionUtils.isEqualsMethod(method)) {
return proxyEquals(o, args[0]);
}
if (ReflectionUtils.isHashCodeMethod(method)) {
return proxyHashCode();
}
// DATAMONGO-1076 - finalize methods should not trigger proxy initialization
if (FINALIZE_METHOD.equals(method)) {
return null;
}
}
Object target = ensureResolved();
if (target == null) {
return null;
}
ReflectionUtils.makeAccessible(method);
return method.invoke(target, args);
}
@Nullable
private Object ensureResolved() {
if (!resolved) {
this.result = resolve();
this.resolved = true;
}
return this.result;
}
private String proxyToString(@Nullable Object source) {
StringBuilder description = new StringBuilder();
if (source != null) {
if (source instanceof DBRef) {
description.append(((DBRef) source).getCollectionName());
description.append(":");
description.append(((DBRef) source).getId());
} else {
description.append(source);
}
} else {
description.append(System.identityHashCode(source));
}
description.append("$").append(LazyLoadingProxy.class.getSimpleName());
return description.toString();
}
private boolean proxyEquals(@Nullable Object proxy, Object that) {
if (!(that instanceof LazyLoadingProxy)) {
return false;
}
if (that == proxy) {
return true;
}
return proxyToString(proxy).equals(that.toString());
}
private int proxyHashCode() {
return proxyToString(source).hashCode();
}
/**
* Callback method for serialization.
*
* @param out
* @throws IOException
*/
private void writeObject(ObjectOutputStream out) throws IOException {
ensureResolved();
out.writeObject(this.result);
}
/**
* Callback method for deserialization.
*
* @param in
* @throws IOException
*/
private void readObject(ObjectInputStream in) throws IOException {
try {
this.resolved = true;
this.result = in.readObject();
} catch (ClassNotFoundException e) {
throw new LazyLoadingException("Could not deserialize result", e);
}
}
@Nullable
private synchronized Object resolve() {
if (resolved) {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Accessing already resolved lazy loading property {}.{}",
property.getOwner() != null ? property.getOwner().getName() : "unknown", property.getName());
}
return result;
}
try {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Resolving lazy loading property {}.{}",
property.getOwner() != null ? property.getOwner().getName() : "unknown", property.getName());
}
return callback.resolve(property);
} catch (RuntimeException ex) {
DataAccessException translatedException = exceptionTranslator.translateExceptionIfPossible(ex);
if (translatedException instanceof ClientSessionException) {
throw new LazyLoadingException("Unable to lazily resolve DBRef! Invalid session state.", ex);
}
throw new LazyLoadingException("Unable to lazily resolve DBRef!",
translatedException != null ? translatedException : ex);
}
}
}
}

View File

@@ -25,9 +25,9 @@ import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.bson.Document;
import org.bson.codecs.Codec;
@@ -43,6 +43,7 @@ import org.springframework.context.ApplicationContextAware;
import org.springframework.core.CollectionFactory;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.annotation.Reference;
import org.springframework.data.convert.TypeMapper;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.MappingException;
@@ -62,6 +63,8 @@ import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.data.mapping.model.SpELExpressionParameterValueProvider;
import org.springframework.data.mongodb.CodecRegistryProvider;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentProperty;
import org.springframework.data.mongodb.core.mapping.DocumentPointer;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.mapping.Unwrapped;
@@ -98,6 +101,7 @@ import com.mongodb.DBRef;
* @author Mark Paluch
* @author Roman Puchkovskiy
* @author Heesu Jung
* @author Divya Srivastava
*/
public class MappingMongoConverter extends AbstractMongoConverter implements ApplicationContextAware {
@@ -112,6 +116,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
protected final QueryMapper idMapper;
protected final DbRefResolver dbRefResolver;
protected final DefaultDbRefProxyHandler dbRefProxyHandler;
protected final ReferenceLookupDelegate referenceLookupDelegate;
protected @Nullable ApplicationContext applicationContext;
protected MongoTypeMapper typeMapper;
@@ -120,6 +125,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
private SpELContext spELContext;
private @Nullable EntityCallbacks entityCallbacks;
private final DocumentPointerFactory documentPointerFactory;
/**
* Creates a new {@link MappingMongoConverter} given the new {@link DbRefResolver} and {@link MappingContext}.
@@ -136,12 +142,12 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
Assert.notNull(mappingContext, "MappingContext must not be null!");
this.dbRefResolver = dbRefResolver;
this.mappingContext = mappingContext;
this.typeMapper = new DefaultMongoTypeMapper(DefaultMongoTypeMapper.DEFAULT_TYPE_KEY, mappingContext,
this::getWriteTarget);
this.idMapper = new QueryMapper(this);
this.spELContext = new SpELContext(DocumentPropertyAccessor.INSTANCE);
this.dbRefProxyHandler = new DefaultDbRefProxyHandler(spELContext, mappingContext,
(prop, bson, evaluator, path) -> {
@@ -149,6 +155,9 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
ConversionContext context = getConversionContext(path);
return MappingMongoConverter.this.getValueInternal(context, prop, bson, evaluator);
});
this.referenceLookupDelegate = new ReferenceLookupDelegate(mappingContext, spELContext);
this.documentPointerFactory = new DocumentPointerFactory(conversionService, mappingContext);
}
/**
@@ -161,8 +170,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
Assert.notNull(path, "ObjectPath must not be null");
return new ConversionContext(path, this::readDocument, this::readCollectionOrArray, this::readMap, this::readDBRef,
this::getPotentiallyConvertedSimpleRead);
return new ConversionContext(conversions, path, this::readDocument, this::readCollectionOrArray, this::readMap,
this::readDBRef, this::getPotentiallyConvertedSimpleRead);
}
/**
@@ -253,7 +262,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
/**
* Set the {@link EntityCallbacks} instance to use when invoking
* {@link org.springframework.data.mapping.callback.EntityCallback callbacks} like the {@link AfterConvertCallback}.
* <p />
* <br />
* Overrides potentially existing {@link EntityCallbacks}.
*
* @param entityCallbacks must not be {@literal null}.
@@ -270,7 +279,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.core.MongoReader#read(java.lang.Class, com.mongodb.Document)
*/
public <S extends Object> S read(Class<S> clazz, final Bson bson) {
public <S extends Object> S read(Class<S> clazz, Bson bson) {
return read(ClassTypeInformation.from(clazz), bson);
}
@@ -354,11 +363,18 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
parameterProvider);
}
private <S extends Object> S read(ConversionContext context, MongoPersistentEntity<S> entity, Document bson) {
private <S> S read(ConversionContext context, MongoPersistentEntity<S> entity, Document bson) {
SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(bson, spELContext);
DocumentAccessor documentAccessor = new DocumentAccessor(bson);
if (hasIdentifier(bson)) {
S existing = findContextualEntity(context, entity, bson);
if (existing != null) {
return existing;
}
}
PreferredConstructor<S, MongoPersistentProperty> persistenceConstructor = entity.getPersistenceConstructor();
ParameterValueProvider<MongoPersistentProperty> provider = persistenceConstructor != null
@@ -369,15 +385,25 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
S instance = instantiator.createInstance(entity, provider);
if (entity.requiresPropertyPopulation()) {
return populateProperties(context, entity, documentAccessor, evaluator, instance);
}
return instance;
}
private boolean hasIdentifier(Document bson) {
return bson.get(BasicMongoPersistentProperty.ID_FIELD_NAME) != null;
}
@Nullable
private <S> S findContextualEntity(ConversionContext context, MongoPersistentEntity<S> entity, Document bson) {
return context.getPath().getPathItem(bson.get(BasicMongoPersistentProperty.ID_FIELD_NAME), entity.getCollection(),
entity.getType());
}
private <S> S populateProperties(ConversionContext context, MongoPersistentEntity<S> entity,
DocumentAccessor documentAccessor,
SpELExpressionEvaluator evaluator, S instance) {
DocumentAccessor documentAccessor, SpELExpressionEvaluator evaluator, S instance) {
PersistentPropertyAccessor<S> accessor = new ConvertingPropertyAccessor<>(entity.getPropertyAccessor(instance),
conversionService);
@@ -423,8 +449,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
@Nullable
private Object readIdValue(ConversionContext context, SpELExpressionEvaluator evaluator,
MongoPersistentProperty idProperty,
Object rawId) {
MongoPersistentProperty idProperty, Object rawId) {
String expression = idProperty.getSpelExpression();
Object resolvedValue = expression != null ? evaluator.evaluate(expression) : rawId;
@@ -434,8 +459,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
private void readProperties(ConversionContext context, MongoPersistentEntity<?> entity,
PersistentPropertyAccessor<?> accessor, DocumentAccessor documentAccessor,
MongoDbPropertyValueProvider valueProvider,
SpELExpressionEvaluator evaluator) {
MongoDbPropertyValueProvider valueProvider, SpELExpressionEvaluator evaluator) {
DbRefResolverCallback callback = null;
@@ -447,7 +471,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
callback = getDbRefResolverCallback(context, documentAccessor, evaluator);
}
readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback);
readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback, context,
evaluator);
continue;
}
@@ -474,7 +499,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
callback = getDbRefResolverCallback(context, documentAccessor, evaluator);
}
readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback);
readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback, context,
evaluator);
continue;
}
@@ -490,23 +516,49 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
private void readAssociation(Association<MongoPersistentProperty> association, PersistentPropertyAccessor<?> accessor,
DocumentAccessor documentAccessor, DbRefProxyHandler handler, DbRefResolverCallback callback) {
DocumentAccessor documentAccessor, DbRefProxyHandler handler, DbRefResolverCallback callback,
ConversionContext context, SpELExpressionEvaluator evaluator) {
MongoPersistentProperty property = association.getInverse();
Object value = documentAccessor.get(property);
if (property.isDocumentReference()
|| (!property.isDbReference() && property.findAnnotation(Reference.class) != null)) {
// quite unusual but sounds like worth having?
if (conversionService.canConvert(DocumentPointer.class, property.getActualType())) {
if (value == null) {
return;
}
DocumentPointer<?> pointer = () -> value;
// collection like special treatment
accessor.setProperty(property, conversionService.convert(pointer, property.getActualType()));
} else {
accessor.setProperty(property,
dbRefResolver.resolveReference(property,
new DocumentReferenceSource(documentAccessor.getDocument(), documentAccessor.get(property)),
referenceLookupDelegate, context::convert));
}
return;
}
if (value == null) {
return;
}
DBRef dbref = value instanceof DBRef ? (DBRef) value : null;
accessor.setProperty(property, dbRefResolver.resolveDbRef(property, dbref, callback, handler));
}
@Nullable
private Object readUnwrapped(ConversionContext context, DocumentAccessor documentAccessor,
MongoPersistentProperty prop,
MongoPersistentEntity<?> unwrappedEntity) {
MongoPersistentProperty prop, MongoPersistentEntity<?> unwrappedEntity) {
if (prop.findAnnotation(Unwrapped.class).onEmpty().equals(OnEmpty.USE_EMPTY)) {
return read(context, unwrappedEntity, (Document) documentAccessor.getDocument());
@@ -541,6 +593,49 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return createDBRef(object, referringProperty);
}
@Override
public DocumentPointer toDocumentPointer(Object source, @Nullable MongoPersistentProperty referringProperty) {
if (source instanceof LazyLoadingProxy) {
return () -> ((LazyLoadingProxy) source).getSource();
}
Assert.notNull(referringProperty, "Cannot create DocumentReference. The referringProperty must not be null!");
if (referringProperty.isDbReference()) {
return () -> toDBRef(source, referringProperty);
}
if (referringProperty.isDocumentReference() || referringProperty.findAnnotation(Reference.class) != null) {
return createDocumentPointer(source, referringProperty);
}
throw new IllegalArgumentException("The referringProperty is neither a DBRef nor a document reference");
}
DocumentPointer<?> createDocumentPointer(Object source, @Nullable MongoPersistentProperty referringProperty) {
if (referringProperty == null) {
return () -> source;
}
if (source instanceof DocumentPointer) {
return (DocumentPointer<?>) source;
}
if (ClassUtils.isAssignableValue(referringProperty.getType(), source)
&& conversionService.canConvert(referringProperty.getType(), DocumentPointer.class)) {
return conversionService.convert(source, DocumentPointer.class);
}
if (ClassUtils.isAssignableValue(referringProperty.getAssociationTargetType(), source)) {
return documentPointerFactory.computePointer(mappingContext, referringProperty, source,
referringProperty.getActualType());
}
return () -> source;
}
/**
* Root entry method into write conversion. Adds a type discriminator to the {@link Document}. Shouldn't be called for
* nested conversions.
@@ -648,6 +743,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
continue;
}
if (prop.isAssociation()) {
writeAssociation(prop.getRequiredAssociation(), accessor, dbObjectAccessor);
continue;
}
@@ -655,10 +751,10 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
Object value = accessor.getProperty(prop);
if (value == null) {
continue;
}
if (!conversions.isSimpleType(value.getClass())) {
if (prop.writeNullValues()) {
dbObjectAccessor.put(prop, null);
}
} else if (!conversions.isSimpleType(value.getClass())) {
writePropertyInternal(value, dbObjectAccessor, prop);
} else {
writeSimpleInternal(value, bson, prop);
@@ -671,7 +767,14 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
MongoPersistentProperty inverseProp = association.getInverse();
writePropertyInternal(accessor.getProperty(inverseProp), dbObjectAccessor, inverseProp);
Object value = accessor.getProperty(inverseProp);
if (value == null && !inverseProp.isUnwrapped() && inverseProp.writeNullValues()) {
dbObjectAccessor.put(inverseProp, null);
return;
}
writePropertyInternal(value, dbObjectAccessor, inverseProp);
}
@SuppressWarnings({ "unchecked" })
@@ -725,6 +828,13 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return;
}
if (prop.isAssociation() && prop.isAnnotationPresent(Reference.class)) {
accessor.put(prop, new DocumentPointerFactory(conversionService, mappingContext)
.computePointer(mappingContext, prop, obj, valueType.getType()).getPointer());
return;
}
/*
* If we have a LazyLoadingProxy we make sure it is initialized first.
*/
@@ -763,10 +873,22 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
if (!property.isDbReference()) {
if (property.isAssociation()) {
List<Object> targetCollection = collection.stream().map(it -> {
return documentPointerFactory.computePointer(mappingContext, property, it, property.getActualType())
.getPointer();
}).collect(Collectors.toList());
return writeCollectionInternal(targetCollection, ClassTypeInformation.from(DocumentPointer.class),
new ArrayList<>());
}
if (property.hasExplicitWriteTarget()) {
return writeCollectionInternal(collection, new FieldTypeInformation<>(property), new ArrayList<>());
}
return writeCollectionInternal(collection, property.getTypeInformation(), new BasicDBList());
return writeCollectionInternal(collection, property.getTypeInformation(), new ArrayList<>());
}
List<Object> dbList = new ArrayList<>(collection.size());
@@ -795,7 +917,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
Assert.notNull(map, "Given map must not be null!");
Assert.notNull(property, "PersistentProperty must not be null!");
if (!property.isDbReference()) {
if (!property.isAssociation()) {
return writeMapInternal(map, new Document(), property.getTypeInformation());
}
@@ -809,7 +931,12 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
if (conversions.isSimpleType(key.getClass())) {
String simpleKey = prepareMapKey(key.toString());
document.put(simpleKey, value != null ? createDBRef(value, property) : null);
if (property.isDbReference()) {
document.put(simpleKey, value != null ? createDBRef(value, property) : null);
} else {
document.put(simpleKey, documentPointerFactory
.computePointer(mappingContext, property, value, property.getActualType()).getPointer());
}
} else {
throw new MappingException("Cannot use a complex object as a key value.");
@@ -846,7 +973,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
collection.add(getPotentiallyConvertedSimpleWrite(element,
componentType != null ? componentType.getType() : Object.class));
} else if (element instanceof Collection || elementType.isArray()) {
collection.add(writeCollectionInternal(BsonUtils.asCollection(element), componentType, new BasicDBList()));
collection.add(writeCollectionInternal(BsonUtils.asCollection(element), componentType, new ArrayList<>()));
} else {
Document document = new Document();
writeInternal(element, document, componentType);
@@ -878,7 +1005,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
writeSimpleInternal(val, bson, simpleKey);
} else if (val instanceof Collection || val.getClass().isArray()) {
BsonUtils.addToMap(bson, simpleKey,
writeCollectionInternal(BsonUtils.asCollection(val), propertyType.getMapValueType(), new BasicDBList()));
writeCollectionInternal(BsonUtils.asCollection(val), propertyType.getMapValueType(), new ArrayList<>()));
} else {
Document document = new Document();
TypeInformation<?> valueTypeInfo = propertyType.isMap() ? propertyType.getMapValueType()
@@ -1045,7 +1172,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
@SuppressWarnings({ "rawtypes", "unchecked" })
private Object getPotentiallyConvertedSimpleRead(Object value, @Nullable Class<?> target) {
if (target == null || ClassUtils.isAssignableValue(target, value)) {
if (target == null) {
return value;
}
@@ -1053,6 +1180,10 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return doConvert(value, target);
}
if (ClassUtils.isAssignableValue(target, value)) {
return value;
}
if (Enum.class.isAssignableFrom(target)) {
return Enum.valueOf((Class<Enum>) target, value.toString());
}
@@ -1141,7 +1272,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
for (Object element : source) {
items.add(context.convert(element, componentType));
items.add(element != null ? context.convert(element, componentType) : element);
}
return getPotentiallyConvertedSimpleRead(items, targetType.getType());
@@ -1193,21 +1324,22 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return map;
}
for (Entry<String, Object> entry : sourceMap.entrySet()) {
sourceMap.forEach((k, v) -> {
if (typeMapper.isTypeKey(entry.getKey())) {
continue;
if (typeMapper.isTypeKey(k)) {
return;
}
Object key = potentiallyUnescapeMapKey(entry.getKey());
Object key = potentiallyUnescapeMapKey(k);
if (!rawKeyType.isAssignableFrom(key.getClass())) {
key = doConvert(key, rawKeyType);
}
Object value = entry.getValue();
map.put(key, context.convert(value, valueType));
}
Object value = v;
map.put(key, value == null ? value : context.convert(value, valueType));
});
return map;
}
@@ -1447,8 +1579,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
T target = null;
if (document != null) {
maybeEmitEvent(
new AfterLoadEvent<>(document, (Class<T>) type.getType(), collectionName));
maybeEmitEvent(new AfterLoadEvent<>(document, (Class<T>) type.getType(), collectionName));
target = (T) readDocument(context, document, type);
}
@@ -1541,9 +1672,10 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
@SuppressWarnings("ConstantConditions")
private <T extends Object> T doConvert(Object value, Class<? extends T> target, @Nullable Class<? extends T> fallback) {
private <T extends Object> T doConvert(Object value, Class<? extends T> target,
@Nullable Class<? extends T> fallback) {
if(conversionService.canConvert(value.getClass(), target) || fallback == null) {
if (conversionService.canConvert(value.getClass(), target) || fallback == null) {
return conversionService.convert(value, target);
}
return conversionService.convert(value, fallback);
@@ -1687,6 +1819,11 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return (T) dbRefResolver.resolveDbRef(property, dbref, callback, dbRefProxyHandler);
}
if (property.isDocumentReference()) {
return (T) dbRefResolver.resolveReference(property, accessor.get(property), referenceLookupDelegate,
context::convert);
}
return super.getPropertyValue(property);
}
}
@@ -1853,6 +1990,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
*/
protected static class ConversionContext {
private final org.springframework.data.convert.CustomConversions conversions;
private final ObjectPath path;
private final ContainerValueConverter<Bson> documentConverter;
private final ContainerValueConverter<Collection<?>> collectionConverter;
@@ -1860,10 +1998,12 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
private final ContainerValueConverter<DBRef> dbRefConverter;
private final ValueConverter<Object> elementConverter;
ConversionContext(ObjectPath path, ContainerValueConverter<Bson> documentConverter,
ContainerValueConverter<Collection<?>> collectionConverter, ContainerValueConverter<Bson> mapConverter,
ContainerValueConverter<DBRef> dbRefConverter, ValueConverter<Object> elementConverter) {
ConversionContext(org.springframework.data.convert.CustomConversions customConversions, ObjectPath path,
ContainerValueConverter<Bson> documentConverter, ContainerValueConverter<Collection<?>> collectionConverter,
ContainerValueConverter<Bson> mapConverter, ContainerValueConverter<DBRef> dbRefConverter,
ValueConverter<Object> elementConverter) {
this.conversions = customConversions;
this.path = path;
this.documentConverter = documentConverter;
this.collectionConverter = collectionConverter;
@@ -1882,8 +2022,13 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
@SuppressWarnings("unchecked")
public <S extends Object> S convert(Object source, TypeInformation<? extends S> typeHint) {
Assert.notNull(source, "Source must not be null");
Assert.notNull(typeHint, "TypeInformation must not be null");
if (conversions.hasCustomReadTarget(source.getClass(), typeHint.getType())) {
return (S) elementConverter.convert(source, typeHint);
}
if (source instanceof Collection) {
Class<?> rawType = typeHint.getType();
@@ -1900,7 +2045,17 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
if (typeHint.isMap()) {
return (S) mapConverter.convert(this, (Bson) source, typeHint);
if (ClassUtils.isAssignable(Document.class, typeHint.getType())) {
return (S) documentConverter.convert(this, BsonUtils.asBson(source), typeHint);
}
if (BsonUtils.supportsBson(source)) {
return (S) mapConverter.convert(this, BsonUtils.asBson(source), typeHint);
}
throw new IllegalArgumentException(
String.format("Expected map like structure but found %s", source.getClass()));
}
if (source instanceof DBRef) {
@@ -1912,8 +2067,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
String.format(INCOMPATIBLE_TYPES, source, BasicDBList.class, typeHint.getType(), getPath()));
}
if (source instanceof Bson) {
return (S) documentConverter.convert(this, (Bson) source, typeHint);
if (BsonUtils.supportsBson(source)) {
return (S) documentConverter.convert(this, BsonUtils.asBson(source), typeHint);
}
return (S) elementConverter.convert(source, typeHint);
@@ -1929,8 +2084,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
Assert.notNull(currentPath, "ObjectPath must not be null");
return new ConversionContext(currentPath, documentConverter, collectionConverter, mapConverter, dbRefConverter,
elementConverter);
return new ConversionContext(conversions, currentPath, documentConverter, collectionConverter, mapConverter,
dbRefConverter, elementConverter);
}
public ObjectPath getPath() {

View File

@@ -40,13 +40,14 @@ import com.mongodb.DBRef;
* @author Thomas Darimont
* @author Christoph Strobl
* @author Mark Paluch
* @author Ryan Gibb
*/
public interface MongoConverter
extends EntityConverter<MongoPersistentEntity<?>, MongoPersistentProperty, Object, Bson>, MongoWriter<Object>,
EntityReader<Object, Bson> {
/**
* Returns thw {@link TypeMapper} being used to write type information into {@link Document}s created with that
* Returns the {@link TypeMapper} being used to write type information into {@link Document}s created with that
* converter.
*
* @return will never be {@literal null}.
@@ -139,6 +140,9 @@ public interface MongoConverter
if (ObjectId.isValid(id.toString())) {
return new ObjectId(id.toString());
}
// avoid ConversionException as convertToMongoType will return String anyways.
return id;
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2021 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
*
* https://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 org.bson.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.MongoDatabaseUtils;
import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceCollection;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.mongodb.client.MongoCollection;
/**
* {@link ReferenceLoader} implementation using a {@link MongoDatabaseFactory} to obtain raw {@link Document documents}
* for linked entities via a {@link ReferenceLoader.DocumentReferenceQuery}.
*
* @author Christoph Strobl
* @since 3.3
*/
public class MongoDatabaseFactoryReferenceLoader implements ReferenceLoader {
private static final Logger LOGGER = LoggerFactory.getLogger(MongoDatabaseFactoryReferenceLoader.class);
private final MongoDatabaseFactory mongoDbFactory;
/**
* @param mongoDbFactory must not be {@literal null}.
*/
public MongoDatabaseFactoryReferenceLoader(MongoDatabaseFactory mongoDbFactory) {
Assert.notNull(mongoDbFactory, "MongoDbFactory translator must not be null!");
this.mongoDbFactory = mongoDbFactory;
}
@Override
public Iterable<Document> fetchMany(DocumentReferenceQuery referenceQuery, ReferenceCollection context) {
MongoCollection<Document> collection = getCollection(context);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Bulk fetching {} from {}.{}.", referenceQuery,
StringUtils.hasText(context.getDatabase()) ? context.getDatabase()
: collection.getNamespace().getDatabaseName(),
context.getCollection());
}
return referenceQuery.apply(collection);
}
/**
* Obtain the {@link MongoCollection} for a given {@link ReferenceCollection} from the underlying
* {@link MongoDatabaseFactory}.
*
* @param context must not be {@literal null}.
* @return the {@link MongoCollection} targeted by the {@link ReferenceCollection}.
*/
protected MongoCollection<Document> getCollection(ReferenceCollection context) {
return MongoDatabaseUtils.getDatabase(context.getDatabase(), mongoDbFactory).getCollection(context.getCollection(),
Document.class);
}
}

View File

@@ -17,6 +17,8 @@ package org.springframework.data.mongodb.core.convert;
import org.bson.conversions.Bson;
import org.springframework.data.convert.EntityWriter;
import org.springframework.data.mongodb.core.mapping.DocumentPointer;
import org.springframework.data.mongodb.core.mapping.DocumentReference;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.util.TypeInformation;
@@ -61,6 +63,7 @@ public interface MongoWriter<T> extends EntityWriter<T, Bson> {
default Object convertToMongoType(@Nullable Object obj, MongoPersistentEntity<?> entity) {
return convertToMongoType(obj, entity.getTypeInformation());
}
/**
* Creates a {@link DBRef} to refer to the given object.
*
@@ -70,4 +73,17 @@ public interface MongoWriter<T> extends EntityWriter<T, Bson> {
* @return will never be {@literal null}.
*/
DBRef toDBRef(Object object, @Nullable MongoPersistentProperty referingProperty);
/**
* Creates a the {@link DocumentPointer} representing the link to another entity.
*
* @param source the object to create a document link to.
* @param referringProperty the client-side property referring to the object which might carry additional metadata for
* the {@link DBRef} object to create. Can be {@literal null}.
* @return will never be {@literal null}.
* @since 3.3
*/
default DocumentPointer<?> toDocumentPointer(Object source, @Nullable MongoPersistentProperty referringProperty) {
return () -> toDBRef(source, referringProperty);
}
}

View File

@@ -18,6 +18,7 @@ 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;
@@ -69,4 +70,11 @@ public enum NoOpDbRefResolver implements DbRefResolver {
private <T> T handle() throws UnsupportedOperationException {
throw new UnsupportedOperationException("DBRef resolution is not supported!");
}
@Nullable
@Override
public Object resolveReference(MongoPersistentProperty property, Object source,
ReferenceLookupDelegate referenceLookupDelegate, MongoEntityReader entityReader) {
return null;
}
}

View File

@@ -19,13 +19,17 @@ import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.bson.BsonValue;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.bson.types.ObjectId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.annotation.Reference;
import org.springframework.data.domain.Example;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.MappingException;
@@ -65,9 +69,13 @@ import com.mongodb.DBRef;
* @author Thomas Darimont
* @author Christoph Strobl
* @author Mark Paluch
* @author David Julia
* @author Divya Srivastava
*/
public class QueryMapper {
protected static final Logger LOGGER = LoggerFactory.getLogger(QueryMapper.class);
private static final List<String> DEFAULT_ID_NAMES = Arrays.asList("id", "_id");
private static final Document META_TEXT_SCORE = new Document("$meta", "textScore");
static final ClassTypeInformation<?> NESTED_DOCUMENT = ClassTypeInformation.from(NestedDocument.class);
@@ -186,24 +194,11 @@ public class QueryMapper {
Assert.notNull(sortObject, "SortObject must not be null!");
if (sortObject.isEmpty()) {
return new Document();
return BsonUtils.EMPTY_DOCUMENT;
}
sortObject = filterUnwrappedObjects(sortObject, entity);
Document mappedSort = new Document();
for (Map.Entry<String, Object> entry : BsonUtils.asMap(sortObject).entrySet()) {
Field field = createPropertyField(entity, entry.getKey(), mappingContext);
if (field.getProperty() != null && field.getProperty().isUnwrapped()) {
continue;
}
mappedSort.put(field.getMappedKey(), entry.getValue());
}
mapMetaAttributes(mappedSort, entity, MetaMapping.WHEN_PRESENT);
return mappedSort;
Document mappedSort = mapFieldsToPropertyNames(sortObject, entity);
return mapMetaAttributes(mappedSort, entity, MetaMapping.WHEN_PRESENT);
}
/**
@@ -219,26 +214,52 @@ public class QueryMapper {
Assert.notNull(fieldsObject, "FieldsObject must not be null!");
fieldsObject = filterUnwrappedObjects(fieldsObject, entity);
Document mappedFields = getMappedObject(fieldsObject, entity);
mapMetaAttributes(mappedFields, entity, MetaMapping.FORCE);
return mappedFields;
Document mappedFields = mapFieldsToPropertyNames(fieldsObject, entity);
return mapMetaAttributes(mappedFields, entity, MetaMapping.FORCE);
}
private void mapMetaAttributes(Document source, @Nullable MongoPersistentEntity<?> entity, MetaMapping metaMapping) {
private Document mapFieldsToPropertyNames(Document fields, @Nullable MongoPersistentEntity<?> entity) {
if (fields.isEmpty()) {
return BsonUtils.EMPTY_DOCUMENT;
}
Document target = new Document();
BsonUtils.asMap(filterUnwrappedObjects(fields, entity)).forEach((k, v) -> {
Field field = createPropertyField(entity, k, mappingContext);
if (field.getProperty() != null && field.getProperty().isUnwrapped()) {
return;
}
target.put(field.getMappedKey(), v);
});
return target;
}
private Document mapMetaAttributes(Document source, @Nullable MongoPersistentEntity<?> entity,
MetaMapping metaMapping) {
if (entity == null) {
return;
return source;
}
if (entity.hasTextScoreProperty() && !MetaMapping.IGNORE.equals(metaMapping)) {
if (source == BsonUtils.EMPTY_DOCUMENT) {
source = new Document();
}
MongoPersistentProperty textScoreProperty = entity.getTextScoreProperty();
if (MetaMapping.FORCE.equals(metaMapping)
|| (MetaMapping.WHEN_PRESENT.equals(metaMapping) && source.containsKey(textScoreProperty.getFieldName()))) {
source.putAll(getMappedTextScoreField(textScoreProperty));
}
}
return source;
}
private Document filterUnwrappedObjects(Document fieldsObject, @Nullable MongoPersistentEntity<?> entity) {
@@ -444,6 +465,10 @@ public class QueryMapper {
}
}
if (value == null) {
return null;
}
if (isNestedKeyword(value)) {
return getMappedKeyword(new Keyword((Bson) value), documentField.getPropertyEntity());
}
@@ -492,6 +517,10 @@ public class QueryMapper {
return true;
}
if (property.isDocumentReference()) {
return true;
}
MongoPersistentEntity<?> entity = documentField.getPropertyEntity();
return entity.hasIdProperty()
&& (type.equals(DBRef.class) || entity.getRequiredIdProperty().getActualType().isAssignableFrom(type));
@@ -605,7 +634,7 @@ public class QueryMapper {
if (source instanceof Iterable) {
BasicDBList result = new BasicDBList();
for (Object element : (Iterable<?>) source) {
result.add(createDbRefFor(element, property));
result.add(createReferenceFor(element, property));
}
return result;
}
@@ -614,12 +643,12 @@ public class QueryMapper {
Document result = new Document();
Document dbObject = (Document) source;
for (String key : dbObject.keySet()) {
result.put(key, createDbRefFor(dbObject.get(key), property));
result.put(key, createReferenceFor(dbObject.get(key), property));
}
return result;
}
return createDbRefFor(source, property);
return createReferenceFor(source, property);
}
/**
@@ -663,15 +692,20 @@ public class QueryMapper {
private Entry<String, Object> createMapEntry(String key, @Nullable Object value) {
Assert.hasText(key, "Key must not be null or empty!");
return Collections.singletonMap(key, value).entrySet().iterator().next();
return new AbstractMap.SimpleEntry<>(key, value);
}
private DBRef createDbRefFor(Object source, MongoPersistentProperty property) {
private Object createReferenceFor(Object source, MongoPersistentProperty property) {
if (source instanceof DBRef) {
return (DBRef) source;
}
if (property != null && (property.isDocumentReference()
|| (!property.isDbReference() && property.findAnnotation(Reference.class) != null))) {
return converter.toDocumentPointer(source, property).getPointer();
}
return converter.toDBRef(source, property);
}
@@ -706,19 +740,19 @@ public class QueryMapper {
* @param candidate
* @return
*/
protected boolean isNestedKeyword(Object candidate) {
protected boolean isNestedKeyword(@Nullable Object candidate) {
if (!(candidate instanceof Document)) {
return false;
}
Set<String> keys = BsonUtils.asMap((Bson) candidate).keySet();
Map<String, Object> map = BsonUtils.asMap((Bson) candidate);
if (keys.size() != 1) {
if (map.size() != 1) {
return false;
}
return isKeyword(keys.iterator().next());
return isKeyword(map.entrySet().iterator().next().getKey());
}
/**
@@ -735,7 +769,7 @@ public class QueryMapper {
/**
* Returns whether the given {@link String} is a MongoDB keyword. The default implementation will check against the
* set of registered keywords returned by {@link #getKeywords()}.
* set of registered keywords.
*
* @param candidate
* @return
@@ -751,12 +785,14 @@ public class QueryMapper {
* converted one by one.
*
* @param documentField the field and its meta data
* @param value the actual value
* @param value the actual value. Can be {@literal null}.
* @return the potentially converted target value.
*/
private Object applyFieldTargetTypeHintToValue(Field documentField, Object value) {
@Nullable
private Object applyFieldTargetTypeHintToValue(Field documentField, @Nullable Object value) {
if (documentField.getProperty() == null || !documentField.getProperty().hasExplicitWriteTarget()) {
if (value == null || documentField.getProperty() == null || !documentField.getProperty().hasExplicitWriteTarget()
|| value instanceof Document || value instanceof DBObject) {
return value;
}
@@ -787,7 +823,6 @@ public class QueryMapper {
*/
static class Keyword {
private static final String N_OR_PATTERN = "\\$.*or";
private static final Set<String> NON_DBREF_CONVERTING_KEYWORDS = new HashSet<>(
Arrays.asList("$", "$size", "$slice", "$gt", "$lt"));
@@ -801,11 +836,14 @@ public class QueryMapper {
public Keyword(Bson bson) {
Set<String> keys = BsonUtils.asMap(bson).keySet();
Assert.isTrue(keys.size() == 1, "Can only use a single value Document!");
Map<String, Object> map = BsonUtils.asMap(bson);
Assert.isTrue(map.size() == 1, "Can only use a single value Document!");
this.key = keys.iterator().next();
this.value = BsonUtils.get(bson, key);
Set<Entry<String, Object>> entries = map.entrySet();
Entry<String, Object> entry = entries.iterator().next();
this.key = entry.getKey();
this.value = entry.getValue();
}
/**
@@ -818,7 +856,7 @@ public class QueryMapper {
}
public boolean isOrOrNor() {
return key.matches(N_OR_PATTERN);
return key.equalsIgnoreCase("$or") || key.equalsIgnoreCase("$nor");
}
/**
@@ -999,8 +1037,8 @@ public class QueryMapper {
*/
protected static class MetadataBackedField extends Field {
private static final Pattern POSITIONAL_PARAMETER_PATTERN = Pattern.compile("\\.\\$(\\[.*?\\])?|\\.\\d+");
private static final Pattern DOT_POSITIONAL_PATTERN = Pattern.compile("\\.\\d+");
private static final Pattern POSITIONAL_PARAMETER_PATTERN = Pattern.compile("\\.\\$(\\[.*?\\])?");
private static final Pattern DOT_POSITIONAL_PATTERN = Pattern.compile("\\.\\d+(?!$)");
private static final String INVALID_ASSOCIATION_REFERENCE = "Invalid path reference %s! Associations can only be pointed to directly or via their id property!";
private final MongoPersistentEntity<?> entity;
@@ -1169,8 +1207,8 @@ public class QueryMapper {
removePlaceholders(DOT_POSITIONAL_PATTERN, pathExpression));
if (sourceProperty != null && sourceProperty.getOwner().equals(entity)) {
return mappingContext
.getPersistentPropertyPath(PropertyPath.from(Pattern.quote(sourceProperty.getName()), entity.getTypeInformation()));
return mappingContext.getPersistentPropertyPath(
PropertyPath.from(Pattern.quote(sourceProperty.getName()), entity.getTypeInformation()));
}
PropertyPath path = forName(rawPath);
@@ -1178,29 +1216,47 @@ public class QueryMapper {
return null;
}
try {
PersistentPropertyPath<MongoPersistentProperty> propertyPath = tryToResolvePersistentPropertyPath(path);
PersistentPropertyPath<MongoPersistentProperty> propertyPath = mappingContext.getPersistentPropertyPath(path);
if (propertyPath == null) {
Iterator<MongoPersistentProperty> iterator = propertyPath.iterator();
boolean associationDetected = false;
if (QueryMapper.LOGGER.isInfoEnabled()) {
while (iterator.hasNext()) {
String types = StringUtils.collectionToDelimitedString(
path.stream().map(it -> it.getType().getSimpleName()).collect(Collectors.toList()), " -> ");
QueryMapper.LOGGER.info(
"Could not map '{}'. Maybe a fragment in '{}' is considered a simple type. Mapper continues with {}.",
path, types, pathExpression);
}
return null;
}
MongoPersistentProperty property = iterator.next();
Iterator<MongoPersistentProperty> iterator = propertyPath.iterator();
boolean associationDetected = false;
if (property.isAssociation()) {
associationDetected = true;
continue;
}
while (iterator.hasNext()) {
if (associationDetected && !property.isIdProperty()) {
throw new MappingException(String.format(INVALID_ASSOCIATION_REFERENCE, pathExpression));
}
MongoPersistentProperty property = iterator.next();
if (property.isAssociation()) {
associationDetected = true;
continue;
}
return propertyPath;
} catch (InvalidPersistentPropertyPath e) {
if (associationDetected && !property.isIdProperty()) {
throw new MappingException(String.format(INVALID_ASSOCIATION_REFERENCE, pathExpression));
}
}
return propertyPath;
}
@Nullable
private PersistentPropertyPath<MongoPersistentProperty> tryToResolvePersistentPropertyPath(PropertyPath path) {
try {
return mappingContext.getPersistentPropertyPath(path);
} catch (MappingException e) {
return null;
}
}
@@ -1329,12 +1385,17 @@ public class QueryMapper {
static class KeyMapper {
private final Iterator<String> iterator;
private int currentIndex;
private String currentPropertyRoot;
private final List<String> pathParts;
public KeyMapper(String key,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
this.iterator = Arrays.asList(key.split("\\.")).iterator();
this.iterator.next();
this.pathParts = Arrays.asList(key.split("\\."));
this.iterator = pathParts.iterator();
this.currentPropertyRoot = iterator.next();
this.currentIndex = 0;
}
/**
@@ -1346,21 +1407,31 @@ public class QueryMapper {
protected String mapPropertyName(MongoPersistentProperty property) {
StringBuilder mappedName = new StringBuilder(PropertyToFieldNameConverter.INSTANCE.convert(property));
boolean inspect = iterator.hasNext();
while (inspect) {
String partial = iterator.next();
currentIndex++;
boolean isPositional = isPositionalParameter(partial) && property.isCollectionLike();
if (property.isMap() && currentPropertyRoot.equals(partial) && iterator.hasNext()) {
partial = iterator.next();
currentIndex++;
}
if (isPositional || property.isMap()) {
if (isPositional || property.isMap() && !currentPropertyRoot.equals(partial)) {
mappedName.append(".").append(partial);
}
inspect = isPositional && iterator.hasNext();
}
if (currentIndex + 1 < pathParts.size()) {
currentIndex++;
currentPropertyRoot = pathParts.get(currentIndex);
}
return mappedName.toString();
}

View File

@@ -0,0 +1,130 @@
/*
* Copyright 2021 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
*
* https://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.Collections;
import java.util.Iterator;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceCollection;
import org.springframework.lang.Nullable;
import com.mongodb.client.MongoCollection;
/**
* The {@link ReferenceLoader} obtains raw {@link Document documents} for linked entities via a
* {@link ReferenceLoader.DocumentReferenceQuery}.
*
* @author Christoph Strobl
* @since 3.3
*/
public interface ReferenceLoader {
/**
* Obtain a single {@link Document} matching the given {@literal referenceQuery} in the {@literal context}.
*
* @param referenceQuery must not be {@literal null}.
* @param context must not be {@literal null}.
* @return the matching {@link Document} or {@literal null} if none found.
*/
@Nullable
default Document fetchOne(DocumentReferenceQuery referenceQuery, ReferenceCollection context) {
Iterator<Document> it = fetchMany(referenceQuery, context).iterator();
return it.hasNext() ? it.next() : null;
}
/**
* Obtain multiple {@link Document} matching the given {@literal referenceQuery} in the {@literal context}.
*
* @param referenceQuery must not be {@literal null}.
* @param context must not be {@literal null}.
* @return the matching {@link Document} or {@literal null} if none found.
*/
Iterable<Document> fetchMany(DocumentReferenceQuery referenceQuery, ReferenceCollection context);
/**
* The {@link DocumentReferenceQuery} defines the criteria by which {@link Document documents} should be matched
* applying potentially given order criteria.
*/
interface DocumentReferenceQuery {
/**
* Get the query to obtain matching {@link Document documents}.
*
* @return never {@literal null}.
*/
Bson getQuery();
/**
* Get the sort criteria for ordering results.
*
* @return an empty {@link Document} by default. Never {@literal null}.
*/
default Bson getSort() {
return new Document();
}
default Iterable<Document> apply(MongoCollection<Document> collection) {
return restoreOrder(collection.find(getQuery()).sort(getSort()));
}
/**
* Restore the order of fetched documents.
*
* @param documents must not be {@literal null}.
* @return never {@literal null}.
*/
default Iterable<Document> restoreOrder(Iterable<Document> documents) {
return documents;
}
static DocumentReferenceQuery forSingleDocument(Bson bson) {
return new DocumentReferenceQuery() {
@Override
public Bson getQuery() {
return bson;
}
@Override
public Iterable<Document> apply(MongoCollection<Document> collection) {
Document result = collection.find(getQuery()).sort(getSort()).limit(1).first();
return result != null ? Collections.singleton(result) : Collections.emptyList();
}
};
}
static DocumentReferenceQuery forManyDocuments(Bson bson) {
return new DocumentReferenceQuery() {
@Override
public Bson getQuery() {
return bson;
}
@Override
public Iterable<Document> apply(MongoCollection<Document> collection) {
return collection.find(getQuery()).sort(getSort());
}
};
}
}
}

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