diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4d0c51fa5a55..e97f0f63beb8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -17,6 +17,7 @@ /sdk/core/ @alzimmermsft @jianghaolu @srnagar @hemanttanwar @anuchandy /sdk/core/azure-core-tracing-opentelemetry/ @samvaity @alzimmermsft /sdk/cosmos/ @moderakh @kushagraThapar @David-Noble-at-work @kirankumarkolli @mbhaskar +/sdk/cosmos/azure-spring-data-cosmosdb/ @kushagraThapar /sdk/eventhubs/ @conniey @srnagar @mssfang /sdk/formrecognizer/ @samvaity @mssfang @sima-zhu /sdk/identity/ @schaabs @g2vinay @jianghaolu diff --git a/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml b/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml index 886abb829987..143fd1dc5216 100755 --- a/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml +++ b/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml @@ -457,6 +457,10 @@ + + @@ -464,4 +468,9 @@ + + + + + diff --git a/eng/jacoco-test-coverage/pom.xml b/eng/jacoco-test-coverage/pom.xml index b07e392a1911..13940326009b 100644 --- a/eng/jacoco-test-coverage/pom.xml +++ b/eng/jacoco-test-coverage/pom.xml @@ -243,6 +243,11 @@ azure-data-gremlin-spring-boot-starter 2.3.3-beta.1 + + com.microsoft.azure + spring-data-cosmosdb + 2.3.1-beta.1 + diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index 93839243d0a7..37b8010866ff 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -4,7 +4,9 @@ com.fasterxml.jackson.core:jackson-annotations;2.10.1 com.fasterxml.jackson.core:jackson-core;2.10.1 com.fasterxml.jackson.core:jackson-databind;2.10.1 com.fasterxml.jackson.dataformat:jackson-dataformat-xml;2.10.1 +com.fasterxml.jackson.datatype:jackson-datatype-jdk8;2.10.0 com.fasterxml.jackson.datatype:jackson-datatype-jsr310;2.10.1 +com.fasterxml.jackson.module:jackson-module-parameter-names;2.10.0 com.github.spotbugs:spotbugs;4.0.0-beta3 com.github.spotbugs:spotbugs-maven-plugin;3.1.12.2 com.google.code.gson:gson;2.8.5 @@ -15,15 +17,15 @@ com.microsoft.azure:azure-arm-client-runtime;1.7.3 com.microsoft.azure:azure-client-authentication;1.7.3 com.microsoft.azure:azure-client-runtime;1.7.3 com.microsoft.azure:azure-core;0.9.8 -com.microsoft.azure:azure-cosmos;3.7.1 +com.microsoft.azure:azure-cosmos;3.7.3 com.microsoft.azure:azure-keyvault-cryptography;1.2.2 com.microsoft.azure:azure-media;0.9.8 com.microsoft.azure:azure-servicebus-jms;0.0.2 com.microsoft.azure:qpid-proton-j-extensions;1.2.3 -com.microsoft.azure:spring-data-cosmosdb;2.2.3.FIX1 com.microsoft.rest:client-runtime;1.7.4 com.microsoft.rest.v2:client-runtime;2.1.1 com.microsoft.spring.data.gremlin:spring-data-gremlin;2.2.3 +com.microsoft.azure:spring-data-cosmosdb;2.3.0 com.squareup.okhttp3:okhttp;4.2.2 commons-codec:commons-codec;1.13 io.micrometer:micrometer-core;1.2.0 @@ -45,6 +47,7 @@ javax.annotation:javax.annotation-api;1.3.2 javax.servlet:javax.servlet-api;4.0.1 javax.validation:validation-api;2.0.1.Final net.minidev:json-smart;2.3 +org.apache.ant:ant;1.9.4 org.apache.avro:avro;1.9.2 org.apache.httpcomponents:httpclient;4.3.6 org.apache.logging.log4j:log4j-api;2.11.1 @@ -55,6 +58,8 @@ org.asynchttpclient:async-http-client;2.10.5 org.codehaus.groovy:groovy-eclipse-batch;2.5.8-01 org.codehaus.groovy:groovy-eclipse-compiler;3.4.0-01 org.hibernate.validator:hibernate-validator;6.0.17.Final +org.javatuples:javatuples;1.2 +org.json:json;20140107 org.linguafranca.pwdb:KeePassJava2;2.1.4 org.powermock:powermock-api-mockito2;2.0.2 org.powermock:powermock-module-junit4;2.0.2 @@ -82,6 +87,11 @@ org.springframework.security:spring-security-oauth2-core;5.3.2.RELEASE org.springframework.security:spring-security-oauth2-jose;5.3.2.RELEASE org.springframework:spring-web;5.2.6.RELEASE org.springframework:spring-jms;5.2.6.RELEASE +org.springframework.data:spring-data-commons;2.3.0.RELEASE +org.springframework:spring-beans;5.2.6.RELEASE +org.springframework:spring-core;5.2.6.RELEASE +org.springframework:spring-expression;5.2.6.RELEASE +org.springframework:spring-tx;5.2.6.RELEASE pl.pragmatists:JUnitParams;1.1.1 ## Test dependency versions @@ -183,7 +193,9 @@ org.apache.maven.plugins:maven-source-plugin;3.0.1 org.apache.maven.plugins:maven-surefire-plugin;3.0.0-M3 org.apidesign.javadoc:codesnippet-doclet;0.53 org.codehaus.mojo:build-helper-maven-plugin;3.0.0 +org.codehaus.mojo:cobertura-maven-plugin;2.7 org.codehaus.mojo:exec-maven-plugin;1.2.1 +org.codehaus.mojo:findbugs-maven-plugin;3.0.5 org.codehaus.mojo:properties-maven-plugin;1.0.0 org.codehaus.mojo:xml-maven-plugin;1.0 org.eclipse.jetty:jetty-maven-plugin;9.3.22.v20171030 @@ -254,3 +266,4 @@ storage_com.microsoft.azure:azure-storage;8.4.0 spring_io.micrometer:micrometer-core;1.3.0 spring_io.micrometer:micrometer-registry-azure-monitor;1.3.0 spring_com.microsoft.azure:azure;1.34.0 + diff --git a/eng/versioning/version_client.txt b/eng/versioning/version_client.txt index 39f82e3969b6..98a8f44ed8da 100644 --- a/eng/versioning/version_client.txt +++ b/eng/versioning/version_client.txt @@ -55,7 +55,7 @@ com.microsoft.azure:azure-servicebus-jms-spring-boot-starter;2.3.2;2.3.3-beta.1 com.microsoft.azure:azure-spring-boot-metrics-starter;2.3.2;2.3.3-beta.1 com.microsoft.azure:azure-spring-boot-tests;2.3.2;2.3.3-beta.1 com.microsoft.azure:azure-spring-boot-test-core;2.3.2;2.3.3-beta.1 - +com.microsoft.azure:spring-data-cosmosdb;2.3.0;2.3.1-beta.1 # Unreleased dependencies: Copy the entry from above, prepend "unreleased_" and remove the current # version. Unreleased dependencies are only valid for dependency versions. diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/CHANGELOG.md b/sdk/cosmos/azure-spring-data-cosmosdb/CHANGELOG.md new file mode 100644 index 000000000000..d98c3d0f2282 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/CHANGELOG.md @@ -0,0 +1,3 @@ +# Release History + +## 2.3.1-beta.1 (Unreleased) diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/README.md b/sdk/cosmos/azure-spring-data-cosmosdb/README.md new file mode 100644 index 000000000000..c181d5b5ff53 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/README.md @@ -0,0 +1,342 @@ +[![Travis CI](https://travis-ci.org/Microsoft/spring-data-cosmosdb.svg?branch=master)](https://travis-ci.org/Microsoft/spring-data-cosmosdb) +[![codecov](https://codecov.io/gh/Microsoft/spring-data-cosmosdb/branch/master/graph/badge.svg)](https://codecov.io/gh/Microsoft/spring-data-cosmosdb) +[![MIT License](http://img.shields.io/badge/license-MIT-green.svg) ](https://github.com/Microsoft/spring-data-cosmosdb/blob/master/LICENSE) + + +#Azure Cosmos DB client library for Java + +## Getting started +[Azure Cosmos DB](https://docs.microsoft.com/en-us/azure/cosmos-db/introduction) is a globally-distributed database service that allows developers to work with data using a variety of standard APIs, such as SQL, MongoDB, Cassandra, Graph, and Table. + +**Spring Data Azure Cosmos DB** provides initial Spring Data support for Azure Cosmos DB using the [SQL API](https://docs.microsoft.com/en-us/azure/cosmos-db/sql-api-introduction), based on Spring Data framework. Currently it only supports SQL API, the other APIs are in the plan. + +## TOC + +* [Examples](#Examples) +* [Spring data version support](#spring-data-version-support) +* [Feature List](#feature-list) +* [Quick Start](#quick-start) +* [Query Partitioned Collection](QueryPartitionedCollection.md) +* [Snapshots](#snapshots) +* [Troubleshooting](#Troubleshooting) +* [Contributing](#Contributing) +* [Code of Conduct](#code-of-conduct) +* [Key concepts](#Key concepts) +* [Next steps](#Next steps) + +## Examples +Please refer to [sample project here](./samplecode). + +## Spring Data Version Support +Version mapping between spring boot and spring-data-cosmosdb: + +| Spring boot version | spring-data-cosmosdb version | +| :----------------------------------------------------------:|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:| +| ![version](https://img.shields.io/badge/version-2.3.x-blue) | [![Maven Central](https://img.shields.io/maven-central/v/com.microsoft.azure/spring-data-cosmosdb/2.3.svg)](https://search.maven.org/search?q=g:com.microsoft.azure%20AND%20a:spring-data-cosmosdb%20AND%20v:2.3.*) | +| ![version](https://img.shields.io/badge/version-2.2.x-blue) | [![Maven Central](https://img.shields.io/maven-central/v/com.microsoft.azure/spring-data-cosmosdb/2.2.svg)](https://search.maven.org/search?q=g:com.microsoft.azure%20AND%20a:spring-data-cosmosdb%20AND%20v:2.2.*) | +| ![version](https://img.shields.io/badge/version-2.1.x-blue) | [![Maven Central](https://img.shields.io/maven-central/v/com.microsoft.azure/spring-data-cosmosdb/2.1.svg)](https://search.maven.org/search?q=g:com.microsoft.azure%20AND%20a:spring-data-cosmosdb%20AND%20v:2.1.*) | +| ![version](https://img.shields.io/badge/version-2.0.x-blue) | [![Maven Central](https://img.shields.io/maven-central/v/com.microsoft.azure/spring-data-cosmosdb/2.0.svg)](https://search.maven.org/search?q=g:com.microsoft.azure%20AND%20a:spring-data-cosmosdb%20AND%20v:2.0.*) | + +## Feature List +- Spring Data ReactiveCrudRepository CrudRepository basic CRUD functionality + - save + - findAll + - findOne by Id + - deleteAll + - delete by Id + - delete entity +- Spring Data [@Id](https://github.com/spring-projects/spring-data-commons/blob/db62390de90c93a78743c97cc2cc9ccd964994a5/src/main/java/org/springframework/data/annotation/Id.java) annotation. + There're 2 ways to map a field in domain class to `id` field of Azure Cosmos DB document. + - annotate a field in domain class with `@Id`, this field will be mapped to document `id` in Cosmos DB. + - set name of this field to `id`, this field will be mapped to document `id` in Azure Cosmos DB. +- Custom collection Name. + By default, collection name will be class name of user domain class. To customize it, add the `@Document(collection="myCustomCollectionName")` annotation to the domain class. The collection field also supports SpEL expressions (eg. `collection = "${dynamic.collection.name}"` or `collection = "#{@someBean.getContainerName()}"`) in order to provide collection names programmatically/via configuration properties. +- Custom IndexingPolicy + By default, IndexingPolicy will be set by azure service. To customize it add annotation `@DocumentIndexingPolicy` to domain class. This annotation has 4 attributes to customize, see following: +```java + boolean automatic; // Indicate if indexing policy use automatic or not + IndexingMode mode; // Indexing policy mode, option Consistent|Lazy|None. + String[] includePaths; // Included paths for indexing + String[] excludePaths; // Excluded paths for indexing +``` +- Supports Optimistic Locking for specific collections, which means upserts/deletes by document will fail with an exception in case the document was modified by another process in the meanwhile. To enable Optimistic Locking for a collection, just create a string `_etag` field and mark it with the `@Version` annotation. See the following: + +```java +@Document(collection = "myCollection") +class MyDocument { + String id; + String data; + @Version + String _etag; +} +``` +- Supports [Azure Cosmos DB partition](https://docs.microsoft.com/en-us/azure/cosmos-db/partition-data). To specify a field of domain class to be partition key field, just annotate it with `@PartitionKey`. When you do CRUD operation, pls specify your partition value. For more sample on partition CRUD, pls refer to [test here](./src/test/java/com/microsoft/azure/spring/data/cosmosdb/repository/integration/AddressRepositoryIT.java) +- Supports [Spring Data custom query](https://docs.spring.io/spring-data/commons/docs/current/reference/html/#repositories.query-methods.details) find operation, e.g., `findByAFieldAndBField` +- Supports [Spring Data pagable and sort](https://docs.spring.io/spring-data/commons/docs/current/reference/html/#repositories.special-parameters). + - Based on available RUs on the database account, cosmosDB can return documents less than or equal to the requested size. + - Due to this variable number of returned documents in every iteration, user should not rely on the totalPageSize, and instead iterating over pageable should be done in this way. +```java + final CosmosPageRequest pageRequest = new CosmosPageRequest(0, pageSize, null); + Page page = tRepository.findAll(pageRequest); + List pageContent = page.getContent(); + while(page.hasNext()) { + Pageable nextPageable = page.nextPageable(); + page = repository.findAll(nextPageable); + pageContent = page.getContent(); + } +``` +- Supports [spring-boot-starter-data-rest](https://projects.spring.io/spring-data-rest/). +- Supports List and nested type in domain class. +- Configurable ObjectMapper bean with unique name `cosmosdbObjectMapper`, only configure customized ObjectMapper if you really need to. e.g., +```java + @Bean(name = "cosmosdbObjectMapper") + public ObjectMapper objectMapper() { + return new ObjectMapper(); // Do configuration to the ObjectMapper if required + } +``` + +## Quick Start + +### Add the dependency +`spring-data-cosmosdb` is published on Maven Central Repository. +If you are using Maven, add the following dependency. + +```xml + + com.microsoft.azure + spring-data-cosmosdb + 2.2.4 + +``` + +### Setup Configuration +Setup configuration class. + +CosmosKeyCredential feature provides capability to rotate keys on the fly. You can switch keys using switchToSecondaryKey(). +For more information on this, see the Sample Application code. + +### Sync and Reactive Repository support +2.2.x supports both sync and reactive repository support. + +Use `@EnableCosmosRepositories` to enable sync repository support. + +For reactive repository support, use `@EnableReactiveCosmosRepositories` + +### Response Diagnostics String and Query Metrics +2.2.x supports Response Diagnostics String and Query Metrics. +Set `populateQueryMetrics` flag to true in application.properties to enable query metrics. +In addition to setting the flag, implement `ResponseDiagnosticsProcessor` to log diagnostics information. + +```java +@Configuration +@EnableCosmosRepositories +@Slf4j +public class AppConfiguration extends AbstractCosmosConfiguration { + + @Value("${azure.cosmosdb.uri}") + private String uri; + + @Value("${azure.cosmosdb.key}") + private String key; + + @Value("${azure.cosmosdb.secondaryKey}") + private String secondaryKey; + + @Value("${azure.cosmosdb.database}") + private String dbName; + + @Value("${azure.cosmosdb.populateQueryMetrics}") + private boolean populateQueryMetrics; + + private CosmosKeyCredential cosmosKeyCredential; + + public CosmosDBConfig getConfig() { + this.cosmosKeyCredential = new CosmosKeyCredential(key); + CosmosDbConfig cosmosdbConfig = CosmosDBConfig.builder(uri, + this.cosmosKeyCredential, dbName).build(); + cosmosdbConfig.setPopulateQueryMetrics(populateQueryMetrics); + cosmosdbConfig.setResponseDiagnosticsProcessor(new ResponseDiagnosticsProcessorImplementation()); + return cosmosdbConfig; + } + + public void switchToSecondaryKey() { + this.cosmosKeyCredential.key(secondaryKey); + } + + private static class ResponseDiagnosticsProcessorImplementation implements ResponseDiagnosticsProcessor { + + @Override + public void processResponseDiagnostics(@Nullable ResponseDiagnostics responseDiagnostics) { + log.info("Response Diagnostics {}", responseDiagnostics); + } + } + +} +``` +Or if you want to customize your config: +```java +public CosmosDBConfig getConfig() { + this.cosmosKeyCredential = new CosmosKeyCredential(key); + CosmosDBConfig cosmosDbConfig = CosmosDBConfig.builder(uri, this.cosmosKeyCredential, dbName).build(); + cosmosDbConfig.getConnectionPolicy().setConnectionMode(ConnectionMode.DIRECT); + cosmosDbConfig.getConnectionPolicy().setMaxPoolSize(1000); + return cosmosDbConfig; +} +``` +By default, `@EnableCosmosRepositories` will scan the current package for any interfaces that extend one of Spring Data's repository interfaces. Using it to annotate your Configuration class to scan a different root package by type if your project layout has multiple projects and it's not finding your repositories. +```java +@Configuration +@EnableCosmosRepositories(basePackageClass=UserRepository.class) +public class AppConfiguration extends AbstractCosmosConfiguration { + // configuration code +} +``` + + +### Define an entity +Define a simple entity as Document in Azure Cosmos DB. + +You can define entities by adding the `@Document` annotation and specifying properties related to the container, such as the container name, request units (RUs), time to live, and auto-create container. + +Containers are created automatically unless you don't want them to: Set `autoCreateCollection` to false in `@Document` annotation to disable auto creation of containers. + +Note: By default request units assigned to newly created containers is 4000. Specify different ru value to customize request units for container created by the SDK (minimum RU value is 400). + +```java +@Document(collection = "myCollection", ru = "400") +public class User { + private String id; + private String firstName; + + @PartitionKey + private String lastName; + + ... // setters and getters + + public User() { + // If you do not want to create a default constructor, + // use annotation @JsonCreator and @JsonProperty in the full args constructor + } + + public User(String id, String firstName, String lastName) { + this.id = id; + this.firstName = firstName; + this.lastName = lastName; + } + + @Override + public String toString() { + return String.format("User: %s %s, %s", firstName, lastName, id); + } +} +``` +`id` field will be used as document id in Azure Cosmos DB. If you want use another field like `emailAddress` as document `id`, just annotate that field with `@Id` annotation. + +Annotation `@Document(collection="mycollection")` is used to specify collection name in Azure Cosmos DB. +Annotation `@PartitionKey` on `lastName` field is used to specify this field be partition key in Azure Cosmos DB. + +```java +@Document(collection = "mycollection") +public class User { + @Id + private String emailAddress; + + ... +} +``` + +### Create repositories +Extends CosmosRepository interface, which provides Spring Data repository support. + +```java +import CosmosRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRepository extends CosmosRepository { + List findByFirstName(String firstName); +} +``` + +`findByFirstName` method is custom query method, it will find documents per FirstName. + +### Create an Application class +Here create an application class with all the components + +```java +@SpringBootApplication +public class SampleApplication implements CommandLineRunner { + + @Autowired + private UserRepository repository; + + @Autowired + private ApplicationContext applicationContext; + + public static void main(String[] args) { + SpringApplication.run(SampleApplication.class, args); + } + + public void run(String... var1) throws Exception { + + final User testUser = new User("testId", "testFirstName", "testLastName"); + + repository.deleteAll(); + repository.save(testUser); + + // to find by Id, please specify partition key value if collection is partitioned + final User result = repository.findOne(testUser.getId(), testUser.getLastName); + // if emailAddress is mapped to id, then + // final User result = respository.findOne(testUser.getEmailAddress(), testUser.getLastName()); + + // Switch to secondary key + UserRepositoryConfiguration bean = + applicationContext.getBean(UserRepositoryConfiguration.class); + bean.switchToSecondaryKey(); + + // Now repository will use secondary key + repository.save(testUser); + + } +} +``` +Autowired UserRepository interface, then can do save, delete and find operations. Spring Data Azure Cosmos DB uses the CosmosTemplate to execute the queries behind *find*, *save* methods. You can use the template yourself for more complex queries. + +## Snapshots +[![Nexus OSS](https://img.shields.io/nexus/snapshots/https/oss.sonatype.org/com.microsoft.azure/spring-data-cosmosdb.svg)](https://oss.sonatype.org/content/repositories/snapshots/com/microsoft/azure/spring-data-cosmosdb/) + +Snapshots built from `master` branch are available, add [maven repositories](https://maven.apache.org/settings.html#Repositories) configuration to your pom file as below. +```xml + + + nexus-snapshots + https://oss.sonatype.org/content/repositories/snapshots/ + + true + always + + + +``` + +## Troubleshooting + +If you encounter any bug, please file an issue [here](https://github.com/Microsoft/spring-data-cosmosdb/issues/new). + +To suggest a new feature or changes that could be made, file an issue the same way you would for a bug. + +## Contributing + +Contribution is welcome. Please follow [this instruction](https://github.com/Azure/azure-sdk-for-java/blob/master/CONTRIBUTING.md) to contribute code. + +## Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +### Data/Telemetry + + This project collects usage data and sends it to Microsoft to help improve our products and services. Read our [privacy](https://privacy.microsoft.com/en-us/privacystatement) statement to learn more. + +## Key concepts + +## Next steps diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/pom.xml b/sdk/cosmos/azure-spring-data-cosmosdb/pom.xml new file mode 100644 index 000000000000..c173ad25dd9a --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/pom.xml @@ -0,0 +1,275 @@ + + + 4.0.0 + + com.azure + azure-client-sdk-parent + 1.7.0 + ../../parents/azure-client-sdk-parent + + + com.microsoft.azure + spring-data-cosmosdb + 2.3.1-beta.1 + + Spring Data for Azure Cosmos DB SQL API + Spring Data for Azure Cosmos DB SQL API + https://github.com/Microsoft/spring-data-cosmosdb + + + MM-dd-HH-mm-ss + 0.17 + 0.18 + + spring-data-cosmosdb-test + testdb-${maven.build.timestamp} + true + false + false + + + + + org.springframework + spring-core + 5.2.6.RELEASE + + + commons-logging + commons-logging + + + + + + org.springframework + spring-web + 5.2.6.RELEASE + + + + org.springframework + spring-beans + 5.2.6.RELEASE + + + + org.springframework + spring-context + 5.2.6.RELEASE + + + + org.springframework + spring-tx + 5.2.6.RELEASE + + + + org.springframework.data + spring-data-commons + 2.3.0.RELEASE + + + + org.springframework + spring-expression + 5.2.6.RELEASE + + + com.microsoft.azure + azure-cosmos + 3.7.3 + + + + com.fasterxml.jackson.module + jackson-module-parameter-names + 2.10.0 + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + 2.10.0 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.10.1 + + + org.json + json + 20140107 + + + + org.javatuples + javatuples + 1.2 + + + + javax.annotation + javax.annotation-api + 1.3.2 + + + + + org.mockito + mockito-core + 3.0.0 + test + + + org.powermock + powermock-module-junit4 + 2.0.2 + test + + + org.powermock + powermock-api-mockito2 + 2.0.2 + test + + + org.springframework.boot + spring-boot-starter-test + 2.3.0.RELEASE + test + + + com.vaadin.external.google + android-json + + + + + + io.projectreactor + reactor-test + 3.3.5.RELEASE + test + + + + org.slf4j + slf4j-simple + 1.7.25 + test + + + + com.google.code.gson + gson + 2.8.5 + test + + + + + + + src/main/resources + true + + META-INF/project.properties + telemetry.config + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + + + com.azure:* + org.springframework:spring-beans:[5.2.6.RELEASE] + org.springframework:spring-web:[5.2.6.RELEASE] + org.springframework:spring-tx:[5.2.6.RELEASE] + org.springframework:spring-expression:[5.2.6.RELEASE] + org.springframework:spring-core:[5.2.6.RELEASE] + org.springframework:spring-context:[5.2.6.RELEASE] + org.springframework.data:spring-data-commons:[2.3.0.RELEASE] + com.microsoft.azure:azure-cosmos:[3.7.3] + org.javatuples:javatuples:[1.2] + com.fasterxml.jackson.datatype:jackson-datatype-jdk8:[2.10.0] + com.fasterxml.jackson.datatype:jackson-datatype-jsr310:[2.10.1] + org.json:json:[20140107] + com.fasterxml.jackson.module:jackson-module-parameter-names:[2.10.0] + javax.annotation:javax.annotation-api:[1.3.2] + org.slf4j:slf4j-simple:[1.7.25] + + + + + + + + org.codehaus.mojo + cobertura-maven-plugin + 2.7 + + + html + xml + + + true + 65 + 65 + + + + com/microsoft/azure/**/GetHashMac.class + com/microsoft/azure/**/Constants.class + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.22.0 + + src/test/resources/application.properties + ${skip.integration.tests} + + + + integration-test + + integration-test + verify + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.1.1 + + private + + + BasicCosmosPersistentProperty.java + + + ${basedir}/src/main/java/com/microsoft/azure/spring/data/cosmosdb/core/mapping/ + + + + + + + diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/Constants.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/Constants.java new file mode 100644 index 000000000000..63001bddc981 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/Constants.java @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb; + +import com.azure.data.cosmos.IndexingMode; + +/** + * Constants class of CosmosDB properties + */ +public final class Constants { + + public static final String DEFAULT_COLLECTION_NAME = ""; + public static final String DEFAULT_REQUEST_UNIT = "4000"; + public static final boolean DEFAULT_INDEXINGPOLICY_AUTOMATIC = true; + public static final IndexingMode DEFAULT_INDEXINGPOLICY_MODE = IndexingMode.CONSISTENT; + public static final String DEFAULT_REPOSITORY_IMPLEMENT_POSTFIX = "Impl"; + public static final int DEFAULT_TIME_TO_LIVE = -1; // Indicates never expire + public static final boolean DEFAULT_AUTO_CREATE_CONTAINER = true; + + public static final String ID_PROPERTY_NAME = "id"; + + public static final String COSMOSDB_MODULE_NAME = "cosmosdb"; + public static final String COSMOSDB_MODULE_PREFIX = "cosmosdb"; + public static final String COSMOS_MAPPING_CONTEXT = "cosmosMappingContext"; + + public static final String USER_AGENT_SUFFIX = "spring-data/"; + + public static final String OBJECTMAPPER_BEAN_NAME = "cosmosdbObjectMapper"; + + public static final String ISO_8601_COMPATIBLE_DATE_PATTERN = "yyyy-MM-dd'T'HH:mm:ss:SSSXXX"; + + private Constants() { + } +} + diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/CosmosDbFactory.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/CosmosDbFactory.java new file mode 100644 index 000000000000..20ca21753504 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/CosmosDbFactory.java @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb; + +import com.azure.data.cosmos.ConnectionPolicy; +import com.azure.data.cosmos.CosmosClient; +import com.azure.data.cosmos.sync.CosmosSyncClient; +import com.microsoft.azure.spring.data.cosmosdb.common.MacAddress; +import com.microsoft.azure.spring.data.cosmosdb.common.PropertyLoader; +import com.microsoft.azure.spring.data.cosmosdb.common.TelemetrySender; +import com.microsoft.azure.spring.data.cosmosdb.config.CosmosDBConfig; +import org.springframework.lang.NonNull; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import javax.annotation.PostConstruct; + +/** + * Factory class for cosmosdb to create client + */ +public class CosmosDbFactory { + + private final CosmosDBConfig config; + + private static final boolean IS_TELEMETRY_ALLOWED = PropertyLoader.isApplicationTelemetryAllowed(); + + private static final String USER_AGENT_SUFFIX = Constants.USER_AGENT_SUFFIX + PropertyLoader.getProjectVersion(); + + private String getUserAgentSuffix() { + String suffix = ";" + USER_AGENT_SUFFIX; + + if (IS_TELEMETRY_ALLOWED || config.isAllowTelemetry()) { + suffix += ";" + MacAddress.getHashMac(); + } + + return suffix; + } + + /** + * Validate config and initialization + * + * @param config cosmosdb config + */ + public CosmosDbFactory(@NonNull CosmosDBConfig config) { + validateConfig(config); + + this.config = config; + } + + /** + * To create a CosmosClient + * + * @return CosmosClient + */ + public CosmosClient getCosmosClient() { + final ConnectionPolicy policy = config.getConnectionPolicy(); + final String userAgent = getUserAgentSuffix() + ";" + policy.userAgentSuffix(); + + policy.userAgentSuffix(userAgent); + return CosmosClient.builder() + .endpoint(config.getUri()) + .key(config.getKey()) + .cosmosKeyCredential(config.getCosmosKeyCredential()) + .connectionPolicy(policy) + .consistencyLevel(config.getConsistencyLevel()) + .build(); + } + + /** + * To create a CosmosSyncClient + * + * @return CosmosSyncClient + */ + public CosmosSyncClient getCosmosSyncClient() { + final ConnectionPolicy policy = config.getConnectionPolicy(); + final String userAgent = getUserAgentSuffix() + ";" + policy.userAgentSuffix(); + + policy.userAgentSuffix(userAgent); + return CosmosClient.builder() + .endpoint(config.getUri()) + .key(config.getKey()) + .cosmosKeyCredential(config.getCosmosKeyCredential()) + .connectionPolicy(policy) + .consistencyLevel(config.getConsistencyLevel()) + .buildSyncClient(); + } + + private void validateConfig(@NonNull CosmosDBConfig config) { + Assert.hasText(config.getUri(), "cosmosdb host url should have text!"); + if (config.getCosmosKeyCredential() == null) { + Assert.hasText(config.getKey(), "cosmosdb host key should have text!"); + } else if (StringUtils.isEmpty(config.getKey())) { + Assert.hasText(config.getCosmosKeyCredential().key(), + "cosmosdb credential host key should have text!"); + } + Assert.hasText(config.getDatabase(), "cosmosdb database should have text!"); + Assert.notNull(config.getConnectionPolicy(), "cosmosdb connection policy should not be null!"); + } + + @PostConstruct + private void sendTelemetry() { + // If any one of them is enabled, send telemetry data + if (IS_TELEMETRY_ALLOWED || config.isAllowTelemetry()) { + final TelemetrySender sender = new TelemetrySender(); + + sender.send(this.getClass().getSimpleName()); + } + } + + /** + * To get config object of cosmosdb + * + * @return CosmosDBConfig + */ + public CosmosDBConfig getConfig() { + return config; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/common/CosmosdbUtils.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/common/CosmosdbUtils.java new file mode 100644 index 000000000000..446a67539437 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/common/CosmosdbUtils.java @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.common; + +import com.azure.data.cosmos.CosmosResponse; +import com.azure.data.cosmos.CosmosResponseDiagnostics; +import com.azure.data.cosmos.FeedResponse; +import com.azure.data.cosmos.FeedResponseDiagnostics; +import com.azure.data.cosmos.Resource; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.azure.spring.data.cosmosdb.core.ResponseDiagnostics; +import com.microsoft.azure.spring.data.cosmosdb.core.ResponseDiagnosticsProcessor; +import com.microsoft.azure.spring.data.cosmosdb.core.convert.ObjectMapperFactory; +import com.microsoft.azure.spring.data.cosmosdb.exception.ConfigurationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.lang.NonNull; + +import java.io.IOException; + +/** + * Util class to fill and process response diagnostics + */ +public class CosmosdbUtils { + + private static final Logger LOGGER = LoggerFactory.getLogger(CosmosdbUtils.class); + + /** + * Get a copy of an existing instance + * @param instance the known instance + * @param type of instance + * @return copy instance + * @throws ConfigurationException if the class type is invalid + */ + @SuppressWarnings("unchecked") + public static T getCopyFrom(@NonNull T instance) { + final ObjectMapper mapper = ObjectMapperFactory.getObjectMapper(); + + try { + final String s = mapper.writeValueAsString(instance); + return (T) mapper.readValue(s, instance.getClass()); + } catch (IOException e) { + throw new ConfigurationException("failed to get copy from " + + instance.getClass().getName(), e); + } + } + + /** + * Generate ResponseDiagnostics with cosmos and feed response diagnostics + * + * @param type of cosmosResponse + * @param responseDiagnosticsProcessor collect Response Diagnostics from API responses and + * then set in {@link ResponseDiagnostics} object. + * @param cosmosResponse response from cosmos + * @param feedResponse response from feed + */ + public static void fillAndProcessResponseDiagnostics( + ResponseDiagnosticsProcessor responseDiagnosticsProcessor, + CosmosResponse cosmosResponse, FeedResponse feedResponse) { + if (responseDiagnosticsProcessor == null) { + return; + } + CosmosResponseDiagnostics cosmosResponseDiagnostics = null; + if (cosmosResponse != null) { + cosmosResponseDiagnostics = cosmosResponse.cosmosResponseDiagnosticsString(); + } + FeedResponseDiagnostics feedResponseDiagnostics = null; + ResponseDiagnostics.CosmosResponseStatistics cosmosResponseStatistics = null; + if (feedResponse != null) { + feedResponseDiagnostics = feedResponse.feedResponseDiagnostics(); + cosmosResponseStatistics = new ResponseDiagnostics.CosmosResponseStatistics(feedResponse); + } + if (cosmosResponseDiagnostics == null + && (feedResponseDiagnostics == null || feedResponseDiagnostics.toString().isEmpty()) + && cosmosResponseStatistics == null) { + LOGGER.debug("Empty response diagnostics"); + return; + } + final ResponseDiagnostics responseDiagnostics = + new ResponseDiagnostics(cosmosResponseDiagnostics, feedResponseDiagnostics, cosmosResponseStatistics); + + // Process response diagnostics + responseDiagnosticsProcessor.processResponseDiagnostics(responseDiagnostics); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/common/ExpressionResolver.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/common/ExpressionResolver.java new file mode 100644 index 000000000000..c992ae5eaa4a --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/common/ExpressionResolver.java @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.common; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.EmbeddedValueResolver; + +/** + * + * @author Domenico Sibilio + * + */ +public class ExpressionResolver { + + private static EmbeddedValueResolver embeddedValueResolver; + + /** + * Initialize ExpressionResolver with ConfigurableBeanFactory + * @param beanFactory used to initialize the embeddedValueResolver + */ + public ExpressionResolver(BeanFactory beanFactory) { + if (beanFactory instanceof ConfigurableBeanFactory) { + setEmbeddedValueResolver(new EmbeddedValueResolver((ConfigurableBeanFactory) beanFactory)); + } + } + + /** + * Resolve the given string value via an {@link EmbeddedValueResolver} + * @param expression the expression to be resolved + * @return the resolved expression, may be {@literal null} + */ + public static String resolveExpression(String expression) { + return embeddedValueResolver != null + ? embeddedValueResolver.resolveStringValue(expression) + : expression; + } + + private static void setEmbeddedValueResolver(EmbeddedValueResolver embeddedValueResolver) { + ExpressionResolver.embeddedValueResolver = embeddedValueResolver; + } + +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/common/MacAddress.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/common/MacAddress.java new file mode 100644 index 000000000000..9db4d22a09ef --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/common/MacAddress.java @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/* + * Disclaimer: + * This class is copied from https://github.com/Microsoft/azure-tools-for-java/ with minor modification (fixing + * static analysis error). + * Location in the repo: /Utils/azuretools-core/src/com/microsoft/azuretools/azurecommons/util/MacAddress.java + */ + +package com.microsoft.azure.spring.data.cosmosdb.common; + +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Mac address class to transfer mac address to hash mac address. + */ +public final class MacAddress { + + private static final String UNKNOWN_MAC_ADDRESS = "Unknown-Mac-Address"; + private static final String MAC_REGEX = "([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}"; + private static final String MAC_REGEX_ZERO = "([0]{2}[:-]){5}[0]{2}"; + private static final String HASHED_MAC_REGEX = "[0-9a-f]{64}"; + + private MacAddress() { + } + + private static boolean isValidHashMacFormat(@NonNull String hashMac) { + if (hashMac.isEmpty()) { + return false; + } + + return Pattern.compile(HASHED_MAC_REGEX).matcher(hashMac).matches(); + } + + private static String getRawMac() { + final List commands; + final String os = System.getProperty("os.name"); + final StringBuilder macBuilder = new StringBuilder(); + + if (os != null + && !os.isEmpty() + && os.toLowerCase(Locale.US).startsWith("win")) { + commands = Collections.singletonList("getmac"); + } else { + commands = Arrays.asList("ifconfig", "-a"); + } + + try { + String tmp; + final ProcessBuilder builder = new ProcessBuilder(commands); + final Process process = builder.start(); + final InputStreamReader streamReader = new InputStreamReader(process.getInputStream(), + StandardCharsets.UTF_8); + + try { + final BufferedReader reader = new BufferedReader(streamReader); + try { + while ((tmp = reader.readLine()) != null) { + macBuilder.append(tmp); + } + } finally { + reader.close(); + } + } finally { + streamReader.close(); + } + } catch (IOException e) { + return ""; + } + + return macBuilder.toString(); + } + + private static String getHexDigest(byte digest) { + final String hex = Integer.toString((digest & 0xff) + 0x100, 16); + + return hex.substring(1); + } + + private static String hash(@NonNull String mac) { + if (mac.isEmpty()) { + return ""; + } + + final StringBuilder builder = new StringBuilder(); + + try { + final MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); + + messageDigest.update(mac.getBytes(StandardCharsets.UTF_8)); + + final byte[] digestBytes = messageDigest.digest(); + + for (final byte digest : digestBytes) { + builder.append(getHexDigest(digest)); + } + } catch (NoSuchAlgorithmException ex) { + return ""; + } + + Assert.isTrue(isValidHashMacFormat(builder.toString()), "Invalid format for HashMac"); + + return builder.toString(); + } + + /** + * To get a hash Mac address. + * + * @return String Hash mac address + */ + public static String getHashMac() { + final String rawMac = getRawMac(); + + if (rawMac.isEmpty()) { + return UNKNOWN_MAC_ADDRESS; + } + + final Pattern pattern = Pattern.compile(MAC_REGEX); + final Pattern patternZero = Pattern.compile(MAC_REGEX_ZERO); + final Matcher matcher = pattern.matcher(rawMac); + + String mac = ""; + + while (matcher.find()) { + mac = matcher.group(0); + + if (!patternZero.matcher(mac).matches()) { + break; + } + } + + final String hashMac = hash(mac); + + if (StringUtils.hasText(hashMac)) { + return hashMac; + } + + return UNKNOWN_MAC_ADDRESS; + } +} + diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/common/Memoizer.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/common/Memoizer.java new file mode 100644 index 000000000000..ce3530647e72 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/common/Memoizer.java @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.common; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +/** + * Memoize function computation results + */ +public final class Memoizer { + + private final Map cache = new ConcurrentHashMap<>(); + + private Memoizer() { + } + + /** + * Put function computation results into Memoizer + * + * @param the type of the input to the function + * @param the type of the output of the function + * @param function represents a function that accepts one argument and produces a result + * @return Function + */ + public static Function memoize(Function function) { + return new Memoizer().internalMemoize(function); + } + + private Function internalMemoize(Function function) { + return input -> cache.computeIfAbsent(input, function); + } + +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/common/PropertyLoader.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/common/PropertyLoader.java new file mode 100644 index 000000000000..e01c5bba3221 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/common/PropertyLoader.java @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.common; + +import org.springframework.lang.NonNull; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +/** + * Load properties from files + */ +public final class PropertyLoader { + + private static final String PROJECT_PROPERTY_FILE = "/META-INF/project.properties"; + + private static final String APPLICATION_PROPERTY_FILE = "/application.properties"; + + private static final String APPLICATION_YML_FILE = "/application.yml"; + + private static final String TELEMETRY_CONFIG_FILE = "/telemetry.config"; + + private PropertyLoader() { + } + + /** + * Get project version from /META-INF/project.properties + * + * @return String project version + */ + public static String getProjectVersion() { + return getPropertyByName("project.version", PROJECT_PROPERTY_FILE); + } + + /** + * Get telemetry instrumentation key from /telemetry.config + * + * @return String telemetry instrumentation key + */ + public static String getTelemetryInstrumentationKey() { + return getPropertyByName("telemetry.instrumentationKey", TELEMETRY_CONFIG_FILE); + } + + /** + * Check if telemetry is allowed + * + * @return boolean if telemetry is allowed + */ + public static boolean isApplicationTelemetryAllowed() { + String allowed = getPropertyByName("cosmosdb.telemetryAllowed", APPLICATION_PROPERTY_FILE); + + if (allowed == null) { + allowed = getPropertyByName("telemetryAllowed", APPLICATION_YML_FILE); + } + + // Default, no telemetry + if (allowed == null) { + return false; + } else { + return !allowed.equalsIgnoreCase("false"); + } + } + + private static String getPropertyByName(@NonNull String name, @NonNull String filename) { + final Properties properties = new Properties(); + final InputStream inputStream = PropertyLoader.class.getResourceAsStream(filename); + + if (inputStream == null) { + return null; + } + + try { + properties.load(inputStream); + } catch (IOException e) { + // Omitted + } finally { + try { + inputStream.close(); + } catch (IOException e) { + // Omitted + } + } + + return properties.getProperty(name); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/common/TelemetryEventData.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/common/TelemetryEventData.java new file mode 100644 index 000000000000..56a980dcd3d0 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/common/TelemetryEventData.java @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.common; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; + +import java.time.Instant; +import java.util.Map; + +/** + * Data format class for telemetry event. + */ +public class TelemetryEventData { + + private final String name; + + @JsonProperty("iKey") + private final String instrumentationKey; + + private final Tags tags = new Tags("Spring-on-azure", "Java-maven-plugin"); + + private final EventData data = new EventData("EventData"); + + private final String time; + + /** + * Initialize data of a telemetry event + * + * @param eventName specify an event + * @param properties properties of event + */ + public TelemetryEventData(String eventName, @NonNull Map properties) { + Assert.hasText(eventName, "Event name should contain text."); + + name = "Microsoft.ApplicationInsights.Event"; + instrumentationKey = PropertyLoader.getTelemetryInstrumentationKey(); + + data.getBaseData().setName(eventName); + data.getBaseData().setProperties(properties); + time = Instant.now().toString(); + } + + /** + * Get name of event + * + * @return name value + */ + public String getName() { + return name; + } + + /** + * Get instrumentationKey of event + * + * @return instrumentationKey value + */ + public String getInstrumentationKey() { + return instrumentationKey; + } + + /** + * Get tags of event + * + * @return Tags value + */ + public Tags getTags() { + return tags; + } + + /** + * Get data of event + * + * @return EventData value + */ + public EventData getData() { + return data; + } + + /** + * Get time of event + * + * @return time value + */ + public String getTime() { + return time; + } + + private static class Tags { + + @JsonProperty("ai.cloud.roleInstance") + private final String aiCloudRoleInstance; + + @JsonProperty("ai.internal.sdkVersion") + private final String aiInternalSdkVersion; + + Tags(String instance, String sdkVersion) { + aiCloudRoleInstance = instance; + aiInternalSdkVersion = sdkVersion; + } + + public String getAiCloudRoleInstance() { + return aiCloudRoleInstance; + } + + public String getAiInternalSdkVersion() { + return aiInternalSdkVersion; + } + } + + private static class EventData { + + private final String baseType; + + private final CustomData baseData = new CustomData(); + + EventData(String baseType) { + this.baseType = baseType; + } + + public String getBaseType() { + return baseType; + } + + public CustomData getBaseData() { + return baseData; + } + + private static class CustomData { + + private final Integer ver = 2; + + private String name; + + private Map properties; + + public Integer getVer() { + return ver; + } + + public String getName() { + return name; + } + + private void setName(String name) { + this.name = name; + } + + public Map getProperties() { + return properties; + } + + private void setProperties(Map properties) { + this.properties = properties; + } + } + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/common/TelemetrySender.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/common/TelemetrySender.java new file mode 100644 index 000000000000..d6ef8baac173 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/common/TelemetrySender.java @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.common; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.util.Assert; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.lang.NonNull; +import java.util.HashMap; +import java.util.Map; + +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON; + +/** + * Class for telemetry sender to send request and event data + */ +public class TelemetrySender { + + private static final Logger LOGGER = LoggerFactory.getLogger(TelemetrySender.class); + + private static final String PROPERTY_INSTALLATION_ID = "installationId"; + + private static final String PROPERTY_VERSION = "version"; + + private static final String PROPERTY_SERVICE_NAME = "serviceName"; + + private static final String PROJECT_INFO = "spring-data-cosmosdb/" + + PropertyLoader.getProjectVersion(); + + private static final String TELEMETRY_TARGET_URL = "https://dc.services.visualstudio.com/v2/track"; + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private static final int RETRY_LIMIT = 3; // Align the retry times with sdk + + private ResponseEntity executeRequest(final TelemetryEventData eventData) { + final HttpHeaders headers = new HttpHeaders(); + + headers.add(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON.toString()); + + try { + final RestTemplate restTemplate = new RestTemplate(); + final HttpEntity body = new HttpEntity<>(MAPPER.writeValueAsString(eventData), headers); + + return restTemplate.exchange(TELEMETRY_TARGET_URL, HttpMethod.POST, body, String.class); + } catch (JsonProcessingException | HttpClientErrorException ignore) { + LOGGER.warn("Failed to exchange telemetry request, {}.", ignore.getMessage()); + } + + return null; + } + + private void sendTelemetryData(@NonNull TelemetryEventData eventData) { + ResponseEntity response = null; + + for (int i = 0; i < RETRY_LIMIT; i++) { + response = executeRequest(eventData); + + if (response != null + && response.getStatusCode() == HttpStatus.OK) { + return; + } + } + + if (response != null + && response.getStatusCode() != HttpStatus.OK) { + LOGGER.warn("Failed to send telemetry data, response status code {}.", response.getStatusCode().toString()); + } + } + + /** + * Send telemetry data according to event name + * + * @param name event name + */ + public void send(String name) { + Assert.hasText(name, "Event name should contain text."); + + sendTelemetryData(new TelemetryEventData(name, getProperties())); + } + + private Map getProperties() { + final Map properties = new HashMap<>(); + + properties.put(PROPERTY_VERSION, PROJECT_INFO); + properties.put(PROPERTY_SERVICE_NAME, "cosmosdb"); + properties.put(PROPERTY_INSTALLATION_ID, MacAddress.getHashMac()); + + return properties; + } +} + diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/common/package-info.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/common/package-info.java new file mode 100644 index 000000000000..bbfa061b299a --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/common/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * This package contains the classes of utils for cosmosdb + */ +package com.microsoft.azure.spring.data.cosmosdb.common; diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/config/AbstractCosmosConfiguration.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/config/AbstractCosmosConfiguration.java new file mode 100644 index 000000000000..005398857889 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/config/AbstractCosmosConfiguration.java @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.config; + +import com.azure.data.cosmos.CosmosClient; +import com.azure.data.cosmos.sync.CosmosSyncClient; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.azure.spring.data.cosmosdb.Constants; +import com.microsoft.azure.spring.data.cosmosdb.CosmosDbFactory; +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosTemplate; +import com.microsoft.azure.spring.data.cosmosdb.core.ReactiveCosmosTemplate; +import com.microsoft.azure.spring.data.cosmosdb.core.convert.MappingCosmosConverter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * To configure cosmos with client, cosmoscdb factory and template + */ +@Configuration +public abstract class AbstractCosmosConfiguration extends CosmosConfigurationSupport { + + /** + * Declare CosmosClient bean. + * @param config of cosmosDbFactory + * @return CosmosClient bean + */ + @Bean + public CosmosClient cosmosClient(CosmosDBConfig config) { + return this.cosmosDbFactory(config).getCosmosClient(); + } + + /** + * Declare CosmosSyncClient bean. + * @param config of cosmosDbFactory + * @return CosmosSyncClient bean + */ + @Bean + public CosmosSyncClient cosmosSyncClient(CosmosDBConfig config) { + return this.cosmosDbFactory(config).getCosmosSyncClient(); + } + + @Qualifier(Constants.OBJECTMAPPER_BEAN_NAME) + @Autowired(required = false) + private ObjectMapper objectMapper; + + /** + * Declare CosmosDbFactory bean. + * @param config of cosmosDbFactory + * @return CosmosDbFactory bean + */ + @Bean + public CosmosDbFactory cosmosDbFactory(CosmosDBConfig config) { + return new CosmosDbFactory(config); + } + + /** + * Declare CosmosTemplate bean. + * @param config of cosmosDbFactory + * @return CosmosTemplate bean + * @throws ClassNotFoundException if the class type is invalid + */ + @Bean + public CosmosTemplate cosmosTemplate(CosmosDBConfig config) throws ClassNotFoundException { + return new CosmosTemplate(this.cosmosDbFactory(config), this.mappingCosmosConverter(), + config.getDatabase()); + } + + /** + * Declare ReactiveCosmosTemplate bean. + * @param config of cosmosDbFactory + * @return ReactiveCosmosTemplate bean + * @throws ClassNotFoundException if the class type is invalid + */ + @Bean + public ReactiveCosmosTemplate reactiveCosmosTemplate(CosmosDBConfig config) throws ClassNotFoundException { + return new ReactiveCosmosTemplate(this.cosmosDbFactory(config), this.mappingCosmosConverter(), + config.getDatabase()); + } + + /** + * Declare MappingCosmosConverter bean. + * @return MappingCosmosConverter bean + * @throws ClassNotFoundException if the class type is invalid + */ + @Bean + public MappingCosmosConverter mappingCosmosConverter() throws ClassNotFoundException { + return new MappingCosmosConverter(this.cosmosMappingContext(), objectMapper); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/config/CosmosConfigurationSupport.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/config/CosmosConfigurationSupport.java new file mode 100644 index 000000000000..458cc27cb253 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/config/CosmosConfigurationSupport.java @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.config; + +import com.microsoft.azure.spring.data.cosmosdb.common.ExpressionResolver; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.CosmosMappingContext; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.data.annotation.Persistent; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * A support class for cosmos configuration to scan beans and get initial entities + */ +public abstract class CosmosConfigurationSupport { + + /** + * Declare ExpressionResolver bean. + * @param beanFactory used to initialize the embeddedValueResolver + * @return ExpressionResolver bean + */ + @Bean + public ExpressionResolver expressionResolver(BeanFactory beanFactory) { + return new ExpressionResolver(beanFactory); + } + + /** + * Declare CosmosMappingContext bean. + * @return CosmosMappingContext bean + * @throws ClassNotFoundException if the class type is invalid + */ + @Bean + public CosmosMappingContext cosmosMappingContext() throws ClassNotFoundException { + final CosmosMappingContext mappingContext = new CosmosMappingContext(); + mappingContext.setInitialEntitySet(getInitialEntitySet()); + + return mappingContext; + } + + protected Collection getMappingBasePackages() { + final Package mappingBasePackage = getClass().getPackage(); + return Collections.singleton(mappingBasePackage == null ? null : mappingBasePackage.getName()); + } + + /** + * Scan all base packages and get all beans + * @return initial entity set + * @throws ClassNotFoundException if the class type is invalid + */ + protected Set> getInitialEntitySet() throws ClassNotFoundException { + final Set> initialEntitySet = new HashSet<>(); + + for (final String basePackage : getMappingBasePackages()) { + initialEntitySet.addAll(scanForEntities(basePackage)); + } + + return initialEntitySet; + } + + /** + * Scan all beans under the given base package + * @param basePackage set the base location of beans + * @return initial entity set for found beans + * @throws ClassNotFoundException if the class type is invalid + */ + protected Set> scanForEntities(String basePackage) throws ClassNotFoundException { + if (!StringUtils.hasText(basePackage)) { + return Collections.emptySet(); + } + + final Set> initialEntitySet = new HashSet<>(); + + if (StringUtils.hasText(basePackage)) { + final ClassPathScanningCandidateComponentProvider componentProvider = + new ClassPathScanningCandidateComponentProvider(false); + + componentProvider.addIncludeFilter(new AnnotationTypeFilter(Persistent.class)); + + for (final BeanDefinition candidate : componentProvider.findCandidateComponents(basePackage)) { + final String className = candidate.getBeanClassName(); + Assert.notNull(className, "Bean class name is null."); + + initialEntitySet + .add(ClassUtils.forName(className, CosmosConfigurationSupport.class.getClassLoader())); + } + } + + return initialEntitySet; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/config/CosmosDBConfig.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/config/CosmosDBConfig.java new file mode 100644 index 000000000000..d8b607986dd7 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/config/CosmosDBConfig.java @@ -0,0 +1,397 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.config; + +import com.azure.data.cosmos.ConnectionPolicy; +import com.azure.data.cosmos.ConsistencyLevel; +import com.azure.data.cosmos.CosmosKeyCredential; +import com.azure.data.cosmos.internal.RequestOptions; +import com.microsoft.azure.spring.data.cosmosdb.core.ResponseDiagnosticsProcessor; +import com.microsoft.azure.spring.data.cosmosdb.exception.CosmosDBAccessException; +import org.springframework.util.Assert; + +import java.beans.ConstructorProperties; + +/** + * Config properties of CosmosDB + */ +public class CosmosDBConfig { + private String uri; + + private String key; + + private String database; + + private ConnectionPolicy connectionPolicy; + + private ConsistencyLevel consistencyLevel; + + private boolean allowTelemetry; + + private RequestOptions requestOptions; + + private CosmosKeyCredential cosmosKeyCredential; + + private ResponseDiagnosticsProcessor responseDiagnosticsProcessor; + + private boolean populateQueryMetrics; + + /** + * Initialization + * @param uri must not be {@literal null} + * @param key must not be {@literal null} + * @param database must not be {@literal null} + * @param connectionPolicy must not be {@literal null} + * @param consistencyLevel must not be {@literal null} + * @param allowTelemetry must not be {@literal null} + * @param requestOptions must not be {@literal null} + * @param cosmosKeyCredential must not be {@literal null} + * @param responseDiagnosticsProcessor must not be {@literal null} + * @param populateQueryMetrics must not be {@literal null} + */ + @ConstructorProperties({"uri", "key", "database", "connectionPolicy", "consistencyLevel", "allowTelemetry", + "requestOptions", "cosmosKeyCredential", "responseDiagnosticsProcessor", + "populateQueryMetrics"}) + public CosmosDBConfig(String uri, String key, String database, ConnectionPolicy connectionPolicy, + ConsistencyLevel consistencyLevel, boolean allowTelemetry, RequestOptions requestOptions, + CosmosKeyCredential cosmosKeyCredential, + ResponseDiagnosticsProcessor responseDiagnosticsProcessor, boolean populateQueryMetrics) { + this.uri = uri; + this.key = key; + this.database = database; + this.connectionPolicy = connectionPolicy; + this.consistencyLevel = consistencyLevel; + this.allowTelemetry = allowTelemetry; + this.requestOptions = requestOptions; + this.cosmosKeyCredential = cosmosKeyCredential; + this.responseDiagnosticsProcessor = responseDiagnosticsProcessor; + this.populateQueryMetrics = populateQueryMetrics; + } + + /** + * Gets uri + * @return uri + */ + public String getUri() { + return uri; + } + + /** + * Gets key + * @return key + */ + public String getKey() { + return key; + } + + /** + * Gets database + * @return database + */ + public String getDatabase() { + return database; + } + + /** + * Gets connection policy + * @return connectionPolicy + */ + public ConnectionPolicy getConnectionPolicy() { + return connectionPolicy; + } + + /** + * Gets consistency level + * @return ConsistencyLevel + */ + public ConsistencyLevel getConsistencyLevel() { + return consistencyLevel; + } + + /** + * Checks if telemetry is allowed + * @return boolean + */ + public boolean isAllowTelemetry() { + return allowTelemetry; + } + + /** + * Gets request options + * @return RequestOptions + */ + public RequestOptions getRequestOptions() { + return requestOptions; + } + + /** + * Gets Cosmos key credential + * @return CosmosKeyCredential + */ + public CosmosKeyCredential getCosmosKeyCredential() { + return cosmosKeyCredential; + } + + /** + * Gets response diagnostics processor + * @return ResponseDiagnosticsProcessor + */ + public ResponseDiagnosticsProcessor getResponseDiagnosticsProcessor() { + return responseDiagnosticsProcessor; + } + + /** + * Checks if is populate query metrics + * @return boolean + */ + public boolean isPopulateQueryMetrics() { + return populateQueryMetrics; + } + + /** + * Sets response diagnostics processor + * @param responseDiagnosticsProcessor must not be {@literal null} + */ + public void setResponseDiagnosticsProcessor(ResponseDiagnosticsProcessor responseDiagnosticsProcessor) { + this.responseDiagnosticsProcessor = responseDiagnosticsProcessor; + } + + /** + * Sets populate query metrics + * @param populateQueryMetrics must not be {@literal null} + */ + public void setPopulateQueryMetrics(boolean populateQueryMetrics) { + this.populateQueryMetrics = populateQueryMetrics; + } + + /** + * create a CosmosDBConfigBuilder with cosmos uri, cosmosKeyCredential and database name + * @param uri must not be {@literal null} + * @param cosmosKeyCredential must not be {@literal null} + * @param database must not be {@literal null} + * @return CosmosDBConfigBuilder + */ + public static CosmosDBConfigBuilder builder(String uri, CosmosKeyCredential cosmosKeyCredential, + String database) { + return defaultBuilder() + .uri(uri) + .cosmosKeyCredential(cosmosKeyCredential) + .database(database) + .connectionPolicy(ConnectionPolicy.defaultPolicy()) + .consistencyLevel(ConsistencyLevel.SESSION) + .requestOptions(new RequestOptions()); + } + + /** + * create a CosmosDBConfigBuilder with cosmos uri, key and database name + * @param uri must not be {@literal null} + * @param key must not be {@literal null} + * @param database must not be {@literal null} + * @return CosmosDBConfigBuilder + */ + public static CosmosDBConfigBuilder builder(String uri, String key, String database) { + return defaultBuilder() + .uri(uri) + .key(key) + .database(database) + .connectionPolicy(ConnectionPolicy.defaultPolicy()) + .consistencyLevel(ConsistencyLevel.SESSION) + .requestOptions(new RequestOptions()); + } + + /** + * create a CosmosDBConfigBuilder with connection string and database name + * @param connectionString must not be {@literal null} + * @param database must not be {@literal null} + * @return CosmosDBConfigBuilder + * @throws CosmosDBAccessException for invalid connection string + */ + public static CosmosDBConfigBuilder builder(String connectionString, String database) { + Assert.hasText(connectionString, "connection string should have text!"); + try { + final String uri = connectionString.split(";")[0].split("=")[1]; + final String key = connectionString.split(";")[1].split("=")[1]; + return builder(uri, key, database); + } catch (ArrayIndexOutOfBoundsException e) { + throw new CosmosDBAccessException("could not parse connection string"); + } + } + + /** + * create a CosmosDBConfigBuilder instance + * @return CosmosDBConfigBuilder + */ + public static CosmosDBConfigBuilder defaultBuilder() { + return new CosmosDBConfigBuilder(); + } + + /** + * Builder class for cosmos db config + */ + public static class CosmosDBConfigBuilder { + private String uri; + private String key; + private String database; + private ConnectionPolicy connectionPolicy; + private ConsistencyLevel consistencyLevel; + private boolean allowTelemetry; + private RequestOptions requestOptions; + private CosmosKeyCredential cosmosKeyCredential; + private ResponseDiagnosticsProcessor responseDiagnosticsProcessor; + private boolean populateQueryMetrics; + + CosmosDBConfigBuilder() { + } + + /** + * Set uri + * + * @param uri value to initialize + * @return CosmosDBConfigBuilder + */ + public CosmosDBConfigBuilder uri(String uri) { + this.uri = uri; + return this; + } + + /** + * Set key + * + * @param key value to initialize + * @return CosmosDBConfigBuilder + */ + public CosmosDBConfigBuilder key(String key) { + this.key = key; + return this; + } + + /** + * Set database + * + * @param database value to initialize + * @return CosmosDBConfigBuilder + */ + public CosmosDBConfigBuilder database(String database) { + this.database = database; + return this; + } + + /** + * Set connectionPolicy + * + * @param connectionPolicy value to initialize + * @return CosmosDBConfigBuilder + */ + public CosmosDBConfigBuilder connectionPolicy(ConnectionPolicy connectionPolicy) { + this.connectionPolicy = connectionPolicy; + return this; + } + + /** + * Set consistencyLevel + * + * @param consistencyLevel value to initialize + * @return CosmosDBConfigBuilder + */ + public CosmosDBConfigBuilder consistencyLevel(ConsistencyLevel consistencyLevel) { + this.consistencyLevel = consistencyLevel; + return this; + } + + /** + * Set allowTelemetry + * + * @param allowTelemetry value to initialize + * @return CosmosDBConfigBuilder + */ + public CosmosDBConfigBuilder allowTelemetry(boolean allowTelemetry) { + this.allowTelemetry = allowTelemetry; + return this; + } + + /** + * Set requestOptions + * + * @param requestOptions value to initialize + * @return CosmosDBConfigBuilder + */ + public CosmosDBConfigBuilder requestOptions(RequestOptions requestOptions) { + this.requestOptions = requestOptions; + return this; + } + + /** + * Set cosmosKeyCredential + * + * @param cosmosKeyCredential value to initialize + * @return CosmosDBConfigBuilder + */ + public CosmosDBConfigBuilder cosmosKeyCredential(CosmosKeyCredential cosmosKeyCredential) { + this.cosmosKeyCredential = cosmosKeyCredential; + return this; + } + + /** + * Set responseDiagnosticsProcessor + * + * @param responseDiagnosticsProcessor value to initialize + * @return CosmosDBConfigBuilder + */ + public CosmosDBConfigBuilder responseDiagnosticsProcessor(ResponseDiagnosticsProcessor + responseDiagnosticsProcessor) { + this.responseDiagnosticsProcessor = responseDiagnosticsProcessor; + return this; + } + + /** + * Set populateQueryMetrics + * + * @param populateQueryMetrics value to initialize + * @return CosmosDBConfigBuilder + */ + public CosmosDBConfigBuilder populateQueryMetrics(boolean populateQueryMetrics) { + this.populateQueryMetrics = populateQueryMetrics; + return this; + } + + /** + * Build a CosmosDBConfig instance + * + * @return CosmosDBConfig + */ + public CosmosDBConfig build() { + return new CosmosDBConfig(this.uri, this.key, this.database, this.connectionPolicy, this.consistencyLevel, + this.allowTelemetry, this.requestOptions, this.cosmosKeyCredential, this.responseDiagnosticsProcessor, + this.populateQueryMetrics); + } + + /** + * Generate string info of instance + * + * @return String + */ + public String toString() { + return "CosmosDBConfig.CosmosDBConfigBuilder(uri=" + + this.uri + + ", key=" + + this.key + + ", database=" + + this.database + + ", connectionPolicy=" + + this.connectionPolicy + + ", consistencyLevel=" + + this.consistencyLevel + + ", allowTelemetry=" + + this.allowTelemetry + + ", requestOptions=" + + this.requestOptions + + ", cosmosKeyCredential=" + + this.cosmosKeyCredential + + ", responseDiagnosticsProcessor=" + + this.responseDiagnosticsProcessor + + ", populateQueryMetrics=" + + this.populateQueryMetrics + + ")"; + } + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/config/package-info.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/config/package-info.java new file mode 100644 index 000000000000..f2fcbfbf6754 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/config/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * This package contains the classes to configure properties of cosmos db + */ +package com.microsoft.azure.spring.data.cosmosdb.config; diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/CosmosOperations.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/CosmosOperations.java new file mode 100644 index 000000000000..458e29377112 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/CosmosOperations.java @@ -0,0 +1,281 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.core; + +import com.azure.data.cosmos.CosmosContainerProperties; +import com.azure.data.cosmos.PartitionKey; +import com.microsoft.azure.spring.data.cosmosdb.core.convert.MappingCosmosConverter; +import com.microsoft.azure.spring.data.cosmosdb.core.query.DocumentQuery; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +/** + * Interface for cosmosdb operations + */ +public interface CosmosOperations { + + /** + * Use getContainerName() instead + * @param domainType class type + * @return container name + * @deprecated Use {@link #getContainerName(Class)} instead + */ + @Deprecated + String getCollectionName(Class domainType); + + /** + * To get container name by domaintype + * @param domainType class type + * @return String + */ + String getContainerName(Class domainType); + + /** + * Use createContainerIfNotExists() instead + * @param information cosmos entity information + * @return created container properties + * @deprecated Use {@link #createContainerIfNotExists(CosmosEntityInformation)} instead + */ + @Deprecated + CosmosContainerProperties createCollectionIfNotExists(CosmosEntityInformation information); + + /** + * Creates container if not exists + * @param information CosmosEntityInformation + * @return CosmosContainerProperties + */ + CosmosContainerProperties createContainerIfNotExists(CosmosEntityInformation information); + + /** + * Find the DocumentQuery, find all the items specified by domain type. + * + * @param domainType the domain type + * @param class type of domain + * @return found results in a List + */ + List findAll(Class domainType); + + /** + * Find the DocumentQuery, find all the items specified by domain type in the given container. + * + * @param containerName the container name + * @param domainType the domain type + * @param class type of domain + * @return found results in a List + */ + List findAll(String containerName, Class domainType); + + /** + * Find the DocumentQuery, find all the items specified by domain type in the given container. + * + * @param partitionKey the partition key + * @param domainType the domain type + * @param class type of domain + * @return found results in a List + */ + List findAll(PartitionKey partitionKey, Class domainType); + + /** + * Finds item by id + * @param id must not be {@literal null} + * @param domainType must not be {@literal null} + * @param type class of domain type + * @return found item + */ + T findById(Object id, Class domainType); + + /** + * Finds item by id + * @param containerName must not be {@literal null} + * @param id must not be {@literal null} + * @param domainType must not be {@literal null} + * @param type class of domain type + * @return found item + */ + T findById(String containerName, Object id, Class domainType); + + /** + * Finds item by id + * @param id must not be {@literal null} + * @param domainType must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param type class of domain type + * @return found item + */ + T findById(Object id, Class domainType, PartitionKey partitionKey); + + /** + * Inserts item + * + * @param objectToSave must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param type class of domain type + * @return the inserted item + */ + T insert(T objectToSave, PartitionKey partitionKey); + + /** + * Inserts item + * + * @param containerName must not be {@literal null} + * @param objectToSave must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param type class of domain type + * @return the inserted item + */ + T insert(String containerName, T objectToSave, PartitionKey partitionKey); + + /** + * Upserts an item with partition key + * @param object upsert object + * @param partitionKey the partition key + * @param type of upsert object + */ + void upsert(T object, PartitionKey partitionKey); + + /** + * Upserts an item into container with partition key + * @param containerName the container name + * @param object upsert object + * @param partitionKey the partition key + * @param type of upsert object + */ + void upsert(String containerName, T object, PartitionKey partitionKey); + + /** + * Upserts an item and return item properties + * @param containerName the container name + * @param object upsert object + * @param partitionKey the partition key + * @param type of upsert object + * @return upsert object entity + */ + T upsertAndReturnEntity(String containerName, T object, PartitionKey partitionKey); + + /** + * Delete an item by id + * + * @param containerName the container name + * @param id the id + * @param partitionKey the partition key + */ + void deleteById(String containerName, Object id, PartitionKey partitionKey); + + /** + * Delete all items in a container + * + * @param containerName the container name + * @param domainType the partition key path + */ + void deleteAll(String containerName, Class domainType); + + /** + * Use deleteContainer() instead + * @param containerName container name + * @deprecated Use {@link #deleteContainer(String)} instead. + */ + @Deprecated + void deleteCollection(String containerName); + + /** + * Delete container + * + * @param containerName the container name + */ + void deleteContainer(String containerName); + + /** + * Delete items matching query + * + * @param query the document query + * @param domainType type class + * @param containerName the container name + * @param type class of domaintype + * @return deleted items in a List + */ + List delete(DocumentQuery query, Class domainType, String containerName); + + /** + * Find query + * + * @param query the document query + * @param domainType type class + * @param containerName the container name + * @param type class of domaintype + * @return found results in a List + */ + List find(DocumentQuery query, Class domainType, String containerName); + + /** + * Find by ids + * + * @param ids iterable of ids + * @param domainType type class + * @param containerName the container name + * @param type of domainType + * @param type of ID + * @return Mono + */ + List findByIds(Iterable ids, Class domainType, String containerName); + + /** + * Exists + * + * @param query the document query + * @param domainType type class + * @param containerName the container name + * @param type of domainType + * @return Boolean + */ + Boolean exists(DocumentQuery query, Class domainType, String containerName); + + /** + * Find all items in a given container with partition key + * + * @param pageable Pageable object + * @param domainType the domainType + * @param containerName the container name + * @param type of domainType + * @return Page + */ + Page findAll(Pageable pageable, Class domainType, String containerName); + + /** + * Pagination query + * @param query the document query + * @param domainType type class + * @param containerName the container name + * @param type class of domaintype + * @return Page + */ + Page paginationQuery(DocumentQuery query, Class domainType, String containerName); + + /** + * Count + * + * @param containerName the container name + * @return count result + */ + long count(String containerName); + + /** + * Count + * + * @param query the document query + * @param domainType the domain type + * @param containerName the container name + * @param type class of domaintype + * @return count result + */ + long count(DocumentQuery query, Class domainType, String containerName); + + /** + * To get converter + * @return MappingCosmosConverter + */ + MappingCosmosConverter getConverter(); +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/CosmosTemplate.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/CosmosTemplate.java new file mode 100644 index 000000000000..2e3a5c116cb7 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/CosmosTemplate.java @@ -0,0 +1,746 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.core; + +import com.azure.data.cosmos.CosmosItemResponse; +import com.azure.data.cosmos.AccessCondition; +import com.azure.data.cosmos.AccessConditionType; +import com.azure.data.cosmos.CosmosItemProperties; +import com.azure.data.cosmos.SqlQuerySpec; +import com.azure.data.cosmos.CosmosItemRequestOptions; +import com.azure.data.cosmos.FeedResponse; +import com.azure.data.cosmos.FeedOptions; +import com.azure.data.cosmos.CosmosClient; +import com.azure.data.cosmos.CosmosContainerProperties; +import com.azure.data.cosmos.CosmosContainerResponse; +import com.azure.data.cosmos.PartitionKey; +import com.microsoft.azure.spring.data.cosmosdb.CosmosDbFactory; +import com.microsoft.azure.spring.data.cosmosdb.common.Memoizer; +import com.microsoft.azure.spring.data.cosmosdb.core.convert.MappingCosmosConverter; +import com.microsoft.azure.spring.data.cosmosdb.core.generator.CountQueryGenerator; +import com.microsoft.azure.spring.data.cosmosdb.core.generator.FindQuerySpecGenerator; +import com.microsoft.azure.spring.data.cosmosdb.core.query.Criteria; +import com.microsoft.azure.spring.data.cosmosdb.core.query.CosmosPageImpl; +import com.microsoft.azure.spring.data.cosmosdb.core.query.CriteriaType; +import com.microsoft.azure.spring.data.cosmosdb.core.query.DocumentQuery; +import com.microsoft.azure.spring.data.cosmosdb.core.query.CosmosPageRequest; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static com.microsoft.azure.spring.data.cosmosdb.common.CosmosdbUtils.fillAndProcessResponseDiagnostics; +import static com.microsoft.azure.spring.data.cosmosdb.exception.CosmosDBExceptionUtils.exceptionHandler; +import static com.microsoft.azure.spring.data.cosmosdb.exception.CosmosDBExceptionUtils.findAPIExceptionHandler; + +/** + * Template class for cosmos db + */ +public class CosmosTemplate implements CosmosOperations, ApplicationContextAware { + + private static final Logger LOGGER = LoggerFactory.getLogger(CosmosTemplate.class); + + private static final String COUNT_VALUE_KEY = "_aggregate"; + + private final MappingCosmosConverter mappingCosmosConverter; + private final String databaseName; + private final ResponseDiagnosticsProcessor responseDiagnosticsProcessor; + private final boolean isPopulateQueryMetrics; + + private final CosmosClient cosmosClient; + private final Function, CosmosEntityInformation> entityInfoCreator = + Memoizer.memoize(this::getCosmosEntityInformation); + + /** + * Initialization + * @param cosmosDbFactory must not be {@literal null} + * @param mappingCosmosConverter must not be {@literal null} + * @param dbName must not be {@literal null} + */ + public CosmosTemplate(CosmosDbFactory cosmosDbFactory, + MappingCosmosConverter mappingCosmosConverter, + String dbName) { + Assert.notNull(cosmosDbFactory, "CosmosDbFactory must not be null!"); + Assert.notNull(mappingCosmosConverter, "MappingCosmosConverter must not be null!"); + + this.mappingCosmosConverter = mappingCosmosConverter; + + this.databaseName = dbName; + this.cosmosClient = cosmosDbFactory.getCosmosClient(); + this.responseDiagnosticsProcessor = cosmosDbFactory.getConfig().getResponseDiagnosticsProcessor(); + this.isPopulateQueryMetrics = cosmosDbFactory.getConfig().isPopulateQueryMetrics(); + } + + /** + * Sets the application context + * @param applicationContext must not be {@literal null} + * @throws BeansException the bean exception + */ + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + } + + /** + * + * Inserts item + * @param objectToSave must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param type class of domain type + * @return the inserted item + */ + public T insert(T objectToSave, PartitionKey partitionKey) { + Assert.notNull(objectToSave, "domainType should not be null"); + + return insert(getContainerName(objectToSave.getClass()), objectToSave, partitionKey); + } + + /** + * Inserts item into the given container + * @param containerName must not be {@literal null} + * @param objectToSave must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param type class of domain type + * @return the inserted item + */ + public T insert(String containerName, T objectToSave, PartitionKey partitionKey) { + Assert.hasText(containerName, "containerName should not be null, empty or only whitespaces"); + Assert.notNull(objectToSave, "objectToSave should not be null"); + + final CosmosItemProperties originalItem = mappingCosmosConverter.writeCosmosItemProperties(objectToSave); + + LOGGER.debug("execute createItem in database {} container {}", this.databaseName, containerName); + + final CosmosItemRequestOptions options = new CosmosItemRequestOptions(); + options.partitionKey(partitionKey); + + @SuppressWarnings("unchecked") + final Class domainType = (Class) objectToSave.getClass(); + + final CosmosItemResponse response = cosmosClient + .getDatabase(this.databaseName) + .getContainer(containerName) + .createItem(originalItem, options) + .doOnNext(cosmosItemResponse -> fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + cosmosItemResponse, null)) + .onErrorResume(throwable -> + exceptionHandler("Failed to insert item", throwable)) + .block(); + + assert response != null; + return mappingCosmosConverter.read(domainType, response.properties()); + } + + /** + * Finds item by id + * @param id must not be {@literal null} + * @param domainType must not be {@literal null} + * @param type class of domain type + * @return found item + */ + public T findById(Object id, Class domainType) { + Assert.notNull(domainType, "domainType should not be null"); + + return findById(getContainerName(domainType), id, domainType); + } + + @Override + public T findById(Object id, Class domainType, PartitionKey partitionKey) { + Assert.notNull(domainType, "domainType should not be null"); + Assert.notNull(partitionKey, "partitionKey should not be null"); + assertValidId(id); + + final String containerName = getContainerName(domainType); + return cosmosClient + .getDatabase(databaseName) + .getContainer(containerName) + .getItem(id.toString(), partitionKey) + .read() + .flatMap(cosmosItemResponse -> { + fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + cosmosItemResponse, null); + return Mono.justOrEmpty(toDomainObject(domainType, + cosmosItemResponse.properties())); + }) + .onErrorResume(throwable -> + findAPIExceptionHandler("Failed to find item", throwable)) + .block(); + } + + /** + * Finds item by id + * @param containerName must not be {@literal null} + * @param id must not be {@literal null} + * @param domainType must not be {@literal null} + * @param type class of domain type + * @return found item + */ + public T findById(String containerName, Object id, Class domainType) { + Assert.hasText(containerName, "containerName should not be null, empty or only whitespaces"); + Assert.notNull(domainType, "domainType should not be null"); + assertValidId(id); + + final String query = String.format("select * from root where root.id = '%s'", id.toString()); + final FeedOptions options = new FeedOptions(); + options.enableCrossPartitionQuery(true); + options.populateQueryMetrics(isPopulateQueryMetrics); + return cosmosClient + .getDatabase(databaseName) + .getContainer(containerName) + .queryItems(query, options) + .flatMap(cosmosItemFeedResponse -> { + fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + null, cosmosItemFeedResponse); + return Mono.justOrEmpty(cosmosItemFeedResponse + .results() + .stream() + .map(cosmosItem -> mappingCosmosConverter.read(domainType, cosmosItem)) + .findFirst()); + }) + .onErrorResume(throwable -> + findAPIExceptionHandler("Failed to find item", throwable)) + .blockFirst(); + } + + /** + * Upserts an item with partition key + * @param object upsert object + * @param partitionKey the partition key + * @param type of upsert object + */ + public void upsert(T object, PartitionKey partitionKey) { + Assert.notNull(object, "Upsert object should not be null"); + + upsert(getContainerName(object.getClass()), object, partitionKey); + } + + /** + * Upserts an item into container with partition key + * @param containerName the container name + * @param object upsert object + * @param partitionKey the partition key + * @param type of upsert object + */ + public void upsert(String containerName, T object, PartitionKey partitionKey) { + upsertAndReturnEntity(containerName, object, partitionKey); + } + + /** + * Upserts an item and return item properties + * @param containerName the container name + * @param object upsert object + * @param partitionKey the partition key + * @param type of upsert object + * @return upsert object entity + */ + public T upsertAndReturnEntity(String containerName, T object, PartitionKey partitionKey) { + Assert.hasText(containerName, "containerName should not be null, empty or only whitespaces"); + Assert.notNull(object, "Upsert object should not be null"); + + final CosmosItemProperties originalItem = mappingCosmosConverter.writeCosmosItemProperties(object); + + LOGGER.debug("execute upsert item in database {} container {}", this.databaseName, containerName); + + @SuppressWarnings("unchecked") + final Class domainType = (Class) object.getClass(); + + final CosmosItemRequestOptions options = new CosmosItemRequestOptions(); + options.partitionKey(partitionKey); + applyVersioning(domainType, originalItem, options); + + final CosmosItemResponse cosmosItemResponse = cosmosClient + .getDatabase(this.databaseName) + .getContainer(containerName) + .upsertItem(originalItem, options) + .doOnNext(response -> fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + response, null)) + .onErrorResume(throwable -> exceptionHandler("Failed to upsert item", throwable)) + .block(); + + assert cosmosItemResponse != null; + return mappingCosmosConverter.read(domainType, cosmosItemResponse.properties()); + } + + /** + * Find the DocumentQuery, find all the items specified by domain type. + * + * @param domainType the domain type + * @param class type of domain + * @return found results in a List + */ + public List findAll(Class domainType) { + Assert.notNull(domainType, "domainType should not be null"); + + return findAll(getContainerName(domainType), domainType); + } + + /** + * Find the DocumentQuery, find all the items specified by domain type in the given container. + * + * @param containerName the container name + * @param domainType the domain type + * @param class type of domain + * @return found results in a List + */ + public List findAll(String containerName, final Class domainType) { + Assert.hasText(containerName, "containerName should not be null, empty or only whitespaces"); + Assert.notNull(domainType, "domainType should not be null"); + + final DocumentQuery query = new DocumentQuery(Criteria.getInstance(CriteriaType.ALL)); + + final List items = findItems(query, domainType, containerName); + return items.stream() + .map(d -> getConverter().read(domainType, d)) + .collect(Collectors.toList()); + } + + @Override + public List findAll(PartitionKey partitionKey, final Class domainType) { + Assert.notNull(partitionKey, "partitionKey should not be null"); + Assert.notNull(domainType, "domainType should not be null"); + + final String containerName = getContainerName(domainType); + + final FeedOptions feedOptions = new FeedOptions(); + feedOptions.partitionKey(partitionKey); + feedOptions.populateQueryMetrics(isPopulateQueryMetrics); + + return cosmosClient + .getDatabase(this.databaseName) + .getContainer(containerName) + .readAllItems(feedOptions) + .flatMap(cosmosItemFeedResponse -> { + fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + null, cosmosItemFeedResponse); + return Flux.fromIterable(cosmosItemFeedResponse.results()); + }) + .map(cosmosItemProperties -> toDomainObject(domainType, cosmosItemProperties)) + .onErrorResume(throwable -> + exceptionHandler("Failed to find items", throwable)) + .collectList() + .block(); + } + + /** + * Delete the DocumentQuery, delete all the items in the given container. + * + * @param containerName Container name of database + * @param domainType the domain type + */ + public void deleteAll(@NonNull String containerName, @NonNull Class domainType) { + Assert.hasText(containerName, "containerName should not be null, empty or only whitespaces"); + + final DocumentQuery query = new DocumentQuery(Criteria.getInstance(CriteriaType.ALL)); + + this.delete(query, domainType, containerName); + } + + @Override + public void deleteCollection(@NonNull String containerName) { + deleteContainer(containerName); + } + + @Override + public void deleteContainer(@NonNull String containerName) { + Assert.hasText(containerName, "containerName should have text."); + cosmosClient.getDatabase(this.databaseName) + .getContainer(containerName) + .delete() + .doOnNext(response -> fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + response, null)) + .onErrorResume(throwable -> + exceptionHandler("Failed to delete container", throwable)) + .block(); + } + + /** + * To get collection name by domaintype + * @param domainType class type + * @return String + */ + public String getCollectionName(Class domainType) { + return getContainerName(domainType); + } + + @Override + public String getContainerName(Class domainType) { + Assert.notNull(domainType, "domainType should not be null"); + + return entityInfoCreator.apply(domainType).getContainerName(); + } + + @Override + public CosmosContainerProperties createCollectionIfNotExists(@NonNull CosmosEntityInformation information) { + return createContainerIfNotExists(information); + } + + @Override + public CosmosContainerProperties createContainerIfNotExists(CosmosEntityInformation information) { + final CosmosContainerResponse response = cosmosClient + .createDatabaseIfNotExists(this.databaseName) + .onErrorResume(throwable -> + exceptionHandler("Failed to create database", throwable)) + .flatMap(cosmosDatabaseResponse -> { + fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + cosmosDatabaseResponse, null); + final CosmosContainerProperties cosmosContainerProperties = new CosmosContainerProperties( + information.getContainerName(), "/" + + information.getPartitionKeyFieldName()); + cosmosContainerProperties.defaultTimeToLive(information.getTimeToLive()); + cosmosContainerProperties.indexingPolicy(information.getIndexingPolicy()); + return cosmosDatabaseResponse + .database() + .createContainerIfNotExists(cosmosContainerProperties, information.getRequestUnit()) + .onErrorResume(throwable -> + exceptionHandler("Failed to create container", throwable)) + .doOnNext(cosmosContainerResponse -> + fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + cosmosContainerResponse, null)); + }) + .block(); + assert response != null; + return response.properties(); + } + + /** + * Delete the DocumentQuery, need to query by id at first, then delete the item + * from the result. + * + * @param containerName Container name of database + * @param id item id + * @param partitionKey the paritition key + */ + public void deleteById(String containerName, Object id, PartitionKey partitionKey) { + Assert.hasText(containerName, "containerName should not be null, empty or only whitespaces"); + assertValidId(id); + + LOGGER.debug("execute deleteById in database {} container {}", this.databaseName, containerName); + + if (partitionKey == null) { + partitionKey = PartitionKey.None; + } + final CosmosItemRequestOptions options = new CosmosItemRequestOptions(); + options.partitionKey(partitionKey); + cosmosClient.getDatabase(this.databaseName) + .getContainer(containerName) + .getItem(id.toString(), partitionKey) + .delete(options) + .doOnNext(response -> fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + response, null)) + .onErrorResume(throwable -> + exceptionHandler("Failed to delete item", throwable)) + .block(); + } + + @Override + public List findByIds(Iterable ids, Class domainType, String containerName) { + Assert.notNull(ids, "Id list should not be null"); + Assert.notNull(domainType, "domainType should not be null."); + Assert.hasText(containerName, "container should not be null, empty or only whitespaces"); + + final DocumentQuery query = new DocumentQuery(Criteria.getInstance(CriteriaType.IN, "id", + Collections.singletonList(ids))); + return find(query, domainType, containerName); + } + + /** + * Finds the document query items + * @param query The representation for query method. + * @param domainType Class of domain + * @param containerName Container name of database + * @param class of domainType + * @return All the found items as List. + */ + public List find(@NonNull DocumentQuery query, @NonNull Class domainType, String containerName) { + Assert.notNull(query, "DocumentQuery should not be null."); + Assert.notNull(domainType, "domainType should not be null."); + Assert.hasText(containerName, "container should not be null, empty or only whitespaces"); + + return findItems(query, domainType, containerName) + .stream() + .map(cosmosItemProperties -> toDomainObject(domainType, cosmosItemProperties)) + .collect(Collectors.toList()); + } + + /** + * Checks if document query items exist + * @param query The representation for query method. + * @param domainType Class of domain + * @param containerName Container name of database + * @param class of domainType + * @return if items exist + */ + public Boolean exists(@NonNull DocumentQuery query, @NonNull Class domainType, String containerName) { + return this.find(query, domainType, containerName).size() > 0; + } + + /** + * Delete the DocumentQuery, need to query the domains at first, then delete the item + * from the result. + * The cosmosdb Sql API do _NOT_ support DELETE query, we cannot add one DeleteQueryGenerator. + * + * @param query The representation for query method. + * @param domainType Class of domain + * @param containerName Container name of database + * @param class of domainType + * @return All the deleted items as List. + */ + @Override + public List delete(@NonNull DocumentQuery query, @NonNull Class domainType, + @NonNull String containerName) { + Assert.notNull(query, "DocumentQuery should not be null."); + Assert.notNull(domainType, "domainType should not be null."); + Assert.hasText(containerName, "container should not be null, empty or only whitespaces"); + + final List results = findItems(query, domainType, containerName); + final List partitionKeyName = getPartitionKeyNames(domainType); + + return results.stream().map(cosmosItemProperties -> { + final CosmosItemResponse cosmosItemResponse = deleteItem(cosmosItemProperties, + partitionKeyName, containerName, domainType); + return getConverter().read(domainType, cosmosItemResponse.properties()); + }).collect(Collectors.toList()); + } + + @Override + public Page findAll(Pageable pageable, Class domainType, String containerName) { + final DocumentQuery query = new DocumentQuery(Criteria.getInstance(CriteriaType.ALL)).with(pageable); + if (pageable.getSort().isSorted()) { + query.with(pageable.getSort()); + } + + return paginationQuery(query, domainType, containerName); + } + + @Override + public Page paginationQuery(DocumentQuery query, Class domainType, String containerName) { + Assert.isTrue(query.getPageable().getPageSize() > 0, "pageable should have page size larger than 0"); + Assert.hasText(containerName, "container should not be null, empty or only whitespaces"); + + final Pageable pageable = query.getPageable(); + final FeedOptions feedOptions = new FeedOptions(); + if (pageable instanceof CosmosPageRequest) { + feedOptions.requestContinuation(((CosmosPageRequest) pageable).getRequestContinuation()); + } + + feedOptions.maxItemCount(pageable.getPageSize()); + feedOptions.enableCrossPartitionQuery(query.isCrossPartitionQuery(getPartitionKeyNames(domainType))); + feedOptions.populateQueryMetrics(isPopulateQueryMetrics); + + final SqlQuerySpec sqlQuerySpec = new FindQuerySpecGenerator().generateCosmos(query); + final FeedResponse feedResponse = cosmosClient + .getDatabase(this.databaseName) + .getContainer(containerName) + .queryItems(sqlQuerySpec, feedOptions) + .doOnNext(propertiesFeedResponse -> fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + null, propertiesFeedResponse)) + .onErrorResume(throwable -> + exceptionHandler("Failed to query items", throwable)) + .next() + .block(); + + assert feedResponse != null; + final Iterator it = feedResponse.results().iterator(); + + final List result = new ArrayList<>(); + for (int index = 0; it.hasNext() + && index < pageable.getPageSize(); index++) { + + final CosmosItemProperties cosmosItemProperties = it.next(); + if (cosmosItemProperties == null) { + continue; + } + + final T entity = mappingCosmosConverter.read(domainType, cosmosItemProperties); + result.add(entity); + } + + final long total = count(query, domainType, containerName); + final int contentSize = result.size(); + + int pageSize; + + if (contentSize < pageable.getPageSize() + && contentSize > 0) { + // If the content size is less than page size, + // this means, cosmosDB is returning less items than page size, + // because of either RU limit, or payload limit + + // Set the page size to content size. + pageSize = contentSize; + } else { + pageSize = pageable.getPageSize(); + } + + final CosmosPageRequest pageRequest = CosmosPageRequest.of(pageable.getOffset(), + pageable.getPageNumber(), + pageSize, + feedResponse.continuationToken(), + query.getSort()); + + return new CosmosPageImpl<>(result, pageRequest, total); + } + + @Override + public long count(String containerName) { + Assert.hasText(containerName, "container name should not be empty"); + + final DocumentQuery query = new DocumentQuery(Criteria.getInstance(CriteriaType.ALL)); + final Long count = getCountValue(query, true, containerName); + assert count != null; + return count; + } + + @Override + public long count(DocumentQuery query, Class domainType, String containerName) { + Assert.notNull(domainType, "domainType should not be null"); + Assert.hasText(containerName, "container name should not be empty"); + + final boolean isCrossPartitionQuery = + query.isCrossPartitionQuery(getPartitionKeyNames(domainType)); + final Long count = getCountValue(query, isCrossPartitionQuery, containerName); + assert count != null; + return count; + } + + @Override + public MappingCosmosConverter getConverter() { + return this.mappingCosmosConverter; + } + + private Long getCountValue(DocumentQuery query, boolean isCrossPartitionQuery, String containerName) { + final SqlQuerySpec querySpec = new CountQueryGenerator().generateCosmos(query); + final FeedOptions options = new FeedOptions(); + + options.enableCrossPartitionQuery(isCrossPartitionQuery); + options.populateQueryMetrics(isPopulateQueryMetrics); + + return executeQuery(querySpec, containerName, options) + .onErrorResume(throwable -> + exceptionHandler("Failed to get count value", throwable)) + .doOnNext(response -> fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + null, response)) + .next() + .map(r -> r.results().get(0).getLong(COUNT_VALUE_KEY)) + .block(); + } + + private Flux> executeQuery(SqlQuerySpec sqlQuerySpec, String containerName, + FeedOptions options) { + return cosmosClient.getDatabase(this.databaseName) + .getContainer(containerName) + .queryItems(sqlQuerySpec, options) + .onErrorResume(throwable -> + exceptionHandler("Failed to execute query", throwable)); + } + + private List getPartitionKeyNames(Class domainType) { + final CosmosEntityInformation entityInfo = entityInfoCreator.apply(domainType); + + if (entityInfo.getPartitionKeyFieldName() == null) { + return new ArrayList<>(); + } + + return Collections.singletonList(entityInfo.getPartitionKeyFieldName()); + } + + private void assertValidId(Object id) { + Assert.notNull(id, "id should not be null"); + if (id instanceof String) { + Assert.hasText(id.toString(), "id should not be empty or only whitespaces."); + } + } + + private List findItems(@NonNull DocumentQuery query, + @NonNull Class domainType, + @NonNull String containerName) { + final SqlQuerySpec sqlQuerySpec = new FindQuerySpecGenerator().generateCosmos(query); + final boolean isCrossPartitionQuery = + query.isCrossPartitionQuery(getPartitionKeyNames(domainType)); + final FeedOptions feedOptions = new FeedOptions(); + feedOptions.enableCrossPartitionQuery(isCrossPartitionQuery); + feedOptions.populateQueryMetrics(isPopulateQueryMetrics); + + return cosmosClient + .getDatabase(this.databaseName) + .getContainer(containerName) + .queryItems(sqlQuerySpec, feedOptions) + .flatMap(cosmosItemFeedResponse -> { + fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + null, cosmosItemFeedResponse); + return Flux.fromIterable(cosmosItemFeedResponse.results()); + }) + .onErrorResume(throwable -> + exceptionHandler("Failed to find items", throwable)) + .collectList() + .block(); + } + + private CosmosItemResponse deleteItem(@NonNull CosmosItemProperties cosmosItemProperties, + @NonNull List partitionKeyNames, + String containerName, + @NonNull Class domainType) { + Assert.isTrue(partitionKeyNames.size() <= 1, "Only one Partition is supported."); + + PartitionKey partitionKey = null; + + if (!partitionKeyNames.isEmpty() + && StringUtils.hasText(partitionKeyNames.get(0))) { + partitionKey = new PartitionKey(cosmosItemProperties.get(partitionKeyNames.get(0))); + } + + if (partitionKey == null) { + partitionKey = PartitionKey.None; + } + + final CosmosItemRequestOptions options = new CosmosItemRequestOptions(partitionKey); + applyVersioning(domainType, cosmosItemProperties, options); + + return cosmosClient + .getDatabase(this.databaseName) + .getContainer(containerName) + .getItem(cosmosItemProperties.id(), partitionKey) + .delete(options) + .doOnNext(response -> fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + response, null)) + .onErrorResume(throwable -> + exceptionHandler("Failed to delete item", throwable)) + .block(); + } + + private T toDomainObject(@NonNull Class domainType, CosmosItemProperties cosmosItemProperties) { + return mappingCosmosConverter.read(domainType, cosmosItemProperties); + } + + private void applyVersioning(Class domainType, + CosmosItemProperties cosmosItemProperties, + CosmosItemRequestOptions options) { + + if (entityInfoCreator.apply(domainType).isVersioned()) { + final AccessCondition accessCondition = new AccessCondition(); + accessCondition.type(AccessConditionType.IF_MATCH); + accessCondition.condition(cosmosItemProperties.etag()); + options.accessCondition(accessCondition); + } + } + + private CosmosEntityInformation getCosmosEntityInformation(Class domainType) { + return new CosmosEntityInformation<>(domainType); + } + +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/ReactiveCosmosOperations.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/ReactiveCosmosOperations.java new file mode 100644 index 000000000000..64bf11e91877 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/ReactiveCosmosOperations.java @@ -0,0 +1,236 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.core; + +import com.azure.data.cosmos.CosmosContainerResponse; +import com.azure.data.cosmos.PartitionKey; +import com.microsoft.azure.spring.data.cosmosdb.core.convert.MappingCosmosConverter; +import com.microsoft.azure.spring.data.cosmosdb.core.query.DocumentQuery; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * Operation class of reactive cosmos + */ +public interface ReactiveCosmosOperations { + + /** + * Get container name + * + * @param domainType the domainType + * @return container name + */ + String getContainerName(Class domainType); + + /** + * Use createContainerIfNotExists() instead + * @param information cosmos entity information + * @return Mono of cosmos container response + * @deprecated use createContainerIfNotExists(CosmosEntityInformation) instead. + */ + @Deprecated + Mono createCollectionIfNotExists(CosmosEntityInformation information); + + /** + * Creates a container if it doesn't already exist + * + * @param information the CosmosEntityInformation + * @return Mono + */ + Mono createContainerIfNotExists(CosmosEntityInformation information); + + /** + * Find all items in a given container + * + * @param containerName the containerName + * @param domainType the domainType + * @param type of domainType + * @return Flux + */ + Flux findAll(String containerName, Class domainType); + + /** + * Find all items in a given container + * + * @param domainType the domainType + * @param type of domainType + * @return Flux + */ + Flux findAll(Class domainType); + + /** + * Find all items in a given container with partition key + * + * @param partitionKey partition Key + * @param domainType the domainType + * @param type of domainType + * @return Flux + */ + Flux findAll(PartitionKey partitionKey, Class domainType); + + /** + * Find by id + * + * @param id the id + * @param domainType the domainType + * @param type of domainType + * @return Mono + */ + Mono findById(Object id, Class domainType); + + /** + * Find by id + * + * @param containerName the containername + * @param id the id + * @param domainType type class + * @param type of domainType + * @return Mono + */ + Mono findById(String containerName, Object id, Class domainType); + + /** + * Find by id + * + * @param id the id + * @param domainType type class + * @param partitionKey partition Key + * @param type of domainType + * @return Mono + */ + Mono findById(Object id, Class domainType, PartitionKey partitionKey); + + /** + * Insert + * + * @param objectToSave the object to save + * @param partitionKey the partition key + * @param type of inserted objectToSave + * @return Mono + */ + Mono insert(T objectToSave, PartitionKey partitionKey); + + /** + * Insert + * + * @param type of inserted objectToSave + * @param containerName the container name + * @param objectToSave the object to save + * @param partitionKey the partition key + * @return Mono + */ + Mono insert(String containerName, Object objectToSave, PartitionKey partitionKey); + + /** + * Upsert + * + * @param object the object to upsert + * @param partitionKey the partition key + * @param type class of object + * @return Mono + */ + Mono upsert(T object, PartitionKey partitionKey); + + /** + * Upsert + * + * @param containerName the container name + * @param object the object to save + * @param partitionKey the partition key + * @param type class of object + * @return Mono + */ + Mono upsert(String containerName, T object, PartitionKey partitionKey); + + /** + * Delete an item by id + * + * @param containerName the container name + * @param id the id + * @param partitionKey the partition key + * @return void Mono + */ + Mono deleteById(String containerName, Object id, PartitionKey partitionKey); + + /** + * Delete all items in a container + * + * @param containerName the container name + * @param partitionKey the partition key path + * @return void Mono + */ + Mono deleteAll(String containerName, String partitionKey); + + /** + * Delete container + * + * @param containerName the container name + */ + void deleteContainer(String containerName); + + /** + * Delete items matching query + * + * @param query the document query + * @param domainType type class + * @param containerName the container name + * @param type class of domaintype + * @return Flux + */ + Flux delete(DocumentQuery query, Class domainType, String containerName); + + /** + * Find items + * + * @param query the document query + * @param domainType type class + * @param containerName the container name + * @param type class of domaintype + * @return Flux + */ + Flux find(DocumentQuery query, Class domainType, String containerName); + + /** + * Exists + * + * @param query the document query + * @param domainType type class + * @param containerName the container name + * @return Mono + */ + Mono exists(DocumentQuery query, Class domainType, String containerName); + + /** + * Exists + * @param id the id + * @param domainType type class + * @param containerName the containercontainer nam,e + * @return Mono + */ + Mono existsById(Object id, Class domainType, String containerName); + + /** + * Count + * + * @param containerName the container name + * @return Mono + */ + Mono count(String containerName); + + /** + * Count + * + * @param query the document query + * @param containerName the container name + * @return Mono + */ + Mono count(DocumentQuery query, String containerName); + + /** + * To get converter + * @return MappingCosmosConverter + */ + MappingCosmosConverter getConverter(); +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/ReactiveCosmosTemplate.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/ReactiveCosmosTemplate.java new file mode 100644 index 000000000000..38200f453b31 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/ReactiveCosmosTemplate.java @@ -0,0 +1,695 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.core; + +import com.azure.data.cosmos.AccessCondition; +import com.azure.data.cosmos.AccessConditionType; +import com.azure.data.cosmos.CosmosItemProperties; +import com.azure.data.cosmos.SqlQuerySpec; +import com.azure.data.cosmos.CosmosItemRequestOptions; +import com.azure.data.cosmos.FeedResponse; +import com.azure.data.cosmos.FeedOptions; +import com.azure.data.cosmos.CosmosClient; +import com.azure.data.cosmos.CosmosContainerProperties; +import com.azure.data.cosmos.CosmosContainerResponse; +import com.azure.data.cosmos.PartitionKey; +import com.microsoft.azure.spring.data.cosmosdb.CosmosDbFactory; +import com.microsoft.azure.spring.data.cosmosdb.common.Memoizer; +import com.microsoft.azure.spring.data.cosmosdb.core.convert.MappingCosmosConverter; +import com.microsoft.azure.spring.data.cosmosdb.core.generator.CountQueryGenerator; +import com.microsoft.azure.spring.data.cosmosdb.core.generator.FindQuerySpecGenerator; +import com.microsoft.azure.spring.data.cosmosdb.core.query.Criteria; +import com.microsoft.azure.spring.data.cosmosdb.core.query.CriteriaType; +import com.microsoft.azure.spring.data.cosmosdb.core.query.DocumentQuery; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + +import static com.microsoft.azure.spring.data.cosmosdb.common.CosmosdbUtils.fillAndProcessResponseDiagnostics; +import static com.microsoft.azure.spring.data.cosmosdb.exception.CosmosDBExceptionUtils.exceptionHandler; +import static com.microsoft.azure.spring.data.cosmosdb.exception.CosmosDBExceptionUtils.findAPIExceptionHandler; + +/** + * Template class of reactive cosmos + */ +@SuppressWarnings("unchecked") +public class ReactiveCosmosTemplate implements ReactiveCosmosOperations, ApplicationContextAware { + private static final String COUNT_VALUE_KEY = "_aggregate"; + + private final MappingCosmosConverter mappingCosmosConverter; + private final String databaseName; + + private final CosmosClient cosmosClient; + private final ResponseDiagnosticsProcessor responseDiagnosticsProcessor; + private final boolean isPopulateQueryMetrics; + + private final Function, CosmosEntityInformation> entityInfoCreator = + Memoizer.memoize(this::getCosmosEntityInformation); + + private final List containerNameCache; + + /** + * Constructor + * + * @param cosmosDbFactory the cosmosdbfactory + * @param mappingCosmosConverter the mappingCosmosConverter + * @param dbName database name + */ + public ReactiveCosmosTemplate(CosmosDbFactory cosmosDbFactory, + MappingCosmosConverter mappingCosmosConverter, + String dbName) { + Assert.notNull(cosmosDbFactory, "CosmosDbFactory must not be null!"); + Assert.notNull(mappingCosmosConverter, "MappingCosmosConverter must not be null!"); + + this.mappingCosmosConverter = mappingCosmosConverter; + this.databaseName = dbName; + this.containerNameCache = new ArrayList<>(); + + this.cosmosClient = cosmosDbFactory.getCosmosClient(); + this.responseDiagnosticsProcessor = cosmosDbFactory.getConfig().getResponseDiagnosticsProcessor(); + this.isPopulateQueryMetrics = cosmosDbFactory.getConfig().isPopulateQueryMetrics(); + } + + /** + * @param applicationContext the application context + * @throws BeansException the bean exception + */ + public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException { + // NOTE: When application context instance variable gets introduced, assign it here. + } + + /** + * Creates a container if it doesn't already exist + * + * @param information the CosmosEntityInformation + * @return Mono containing CosmosContainerResponse + */ + @Override + public Mono createCollectionIfNotExists(CosmosEntityInformation information) { + return createContainerIfNotExists(information); + } + + /** + * Creates a container if it doesn't already exist + * + * @param information the CosmosEntityInformation + * @return Mono containing CosmosContainerResponse + */ + @Override + public Mono createContainerIfNotExists(CosmosEntityInformation information) { + + return cosmosClient + .createDatabaseIfNotExists(this.databaseName) + .onErrorResume(throwable -> + exceptionHandler("Failed to create database", throwable)) + .flatMap(cosmosDatabaseResponse -> { + fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + cosmosDatabaseResponse, null); + final CosmosContainerProperties cosmosContainerProperties = new CosmosContainerProperties( + information.getContainerName(), + "/" + information.getPartitionKeyFieldName()); + cosmosContainerProperties.defaultTimeToLive(information.getTimeToLive()); + cosmosContainerProperties.indexingPolicy(information.getIndexingPolicy()); + return cosmosDatabaseResponse + .database() + .createContainerIfNotExists(cosmosContainerProperties, information.getRequestUnit()) + .map(cosmosContainerResponse -> { + fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + cosmosContainerResponse, null); + this.containerNameCache.add(information.getContainerName()); + return cosmosContainerResponse; + }) + .onErrorResume(throwable -> + exceptionHandler("Failed to create container", throwable)); + }); + + } + + /** + * Find all items in a given container + * + * @param containerName the containerName + * @param domainType the domainType + * @return Flux with all the found items or error + */ + @Override + public Flux findAll(String containerName, Class domainType) { + final DocumentQuery query = new DocumentQuery(Criteria.getInstance(CriteriaType.ALL)); + + return find(query, domainType, containerName); + } + + /** + * Find all items in a given container + * + * @param domainType the domainType + * @return Flux with all the found items or error + */ + @Override + public Flux findAll(Class domainType) { + return findAll(domainType.getSimpleName(), domainType); + } + + @Override + public Flux findAll(PartitionKey partitionKey, Class domainType) { + Assert.notNull(partitionKey, "partitionKey should not be null"); + Assert.notNull(domainType, "domainType should not be null"); + + final String containerName = getContainerName(domainType); + + final FeedOptions feedOptions = new FeedOptions(); + feedOptions.partitionKey(partitionKey); + feedOptions.populateQueryMetrics(isPopulateQueryMetrics); + + return cosmosClient + .getDatabase(this.databaseName) + .getContainer(containerName) + .readAllItems(feedOptions) + .flatMap(cosmosItemFeedResponse -> { + fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + null, cosmosItemFeedResponse); + return Flux.fromIterable(cosmosItemFeedResponse.results()); + }) + .map(cosmosItemProperties -> toDomainObject(domainType, cosmosItemProperties)) + .onErrorResume(throwable -> + exceptionHandler("Failed to find items", throwable)); + } + + /** + * Find by id + * + * @param id the id + * @param domainType the domainType + * @return Mono with the item or error + */ + @Override + public Mono findById(Object id, Class domainType) { + Assert.notNull(domainType, "domainType should not be null"); + return findById(getContainerName(domainType), id, domainType); + } + + /** + * Find by id + * + * @param containerName the containername + * @param id the id + * @param domainType the entity class + * @return Mono with the item or error + */ + @Override + public Mono findById(String containerName, Object id, Class domainType) { + Assert.hasText(containerName, "containerName should not be null, empty or only whitespaces"); + Assert.notNull(domainType, "domainType should not be null"); + assertValidId(id); + + final String query = String.format("select * from root where root.id = '%s'", id.toString()); + final FeedOptions options = new FeedOptions(); + options.enableCrossPartitionQuery(true); + options.populateQueryMetrics(isPopulateQueryMetrics); + + return cosmosClient.getDatabase(databaseName) + .getContainer(containerName) + .queryItems(query, options) + .flatMap(cosmosItemFeedResponse -> { + fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + null, cosmosItemFeedResponse); + return Mono.justOrEmpty(cosmosItemFeedResponse + .results() + .stream() + .map(cosmosItem -> toDomainObject(domainType, cosmosItem)) + .findFirst()); + }) + .onErrorResume(throwable -> + findAPIExceptionHandler("Failed to find item", throwable)) + .next(); + } + + /** + * Find by id + * + * @param id the id + * @param domainType the entity class + * @param partitionKey partition Key + * @return Mono with the item or error + */ + @Override + public Mono findById(Object id, Class domainType, PartitionKey partitionKey) { + Assert.notNull(domainType, "domainType should not be null"); + assertValidId(id); + + final String containerName = getContainerName(domainType); + return cosmosClient.getDatabase(databaseName) + .getContainer(containerName) + .getItem(id.toString(), partitionKey) + .read() + .flatMap(cosmosItemResponse -> { + fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + cosmosItemResponse, null); + return Mono.justOrEmpty(toDomainObject(domainType, + cosmosItemResponse.properties())); + }) + .onErrorResume(throwable -> + findAPIExceptionHandler("Failed to find item", throwable)); + } + + /** + * Insert + * + * @param type of inserted objectToSave + * @param objectToSave the object to save + * @param partitionKey the partition key + * @return Mono with the item or error + */ + public Mono insert(T objectToSave, PartitionKey partitionKey) { + Assert.notNull(objectToSave, "domainType should not be null"); + + return insert(getContainerName(objectToSave.getClass()), objectToSave, partitionKey); + } + + /** + * Insert + * + * @param objectToSave the object to save + * @param type of inserted objectToSave + * @return Mono with the item or error + */ + public Mono insert(T objectToSave) { + Assert.notNull(objectToSave, "objectToSave should not be null"); + + final Class domainType = (Class) objectToSave.getClass(); + final CosmosItemProperties originalItem = mappingCosmosConverter.writeCosmosItemProperties(objectToSave); + return cosmosClient.getDatabase(this.databaseName) + .getContainer(getContainerName(objectToSave.getClass())) + .createItem(originalItem, new CosmosItemRequestOptions()) + .onErrorResume(throwable -> + exceptionHandler("Failed to insert item", throwable)) + .flatMap(cosmosItemResponse -> { + fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + cosmosItemResponse, null); + return Mono.just(toDomainObject(domainType, cosmosItemResponse.properties())); + }); + } + + /** + * Insert + * + * @param type of inserted objectToSave + * @param containerName the container name + * @param objectToSave the object to save + * @param partitionKey the partition key + * @return Mono with the item or error + */ + public Mono insert(String containerName, Object objectToSave, PartitionKey partitionKey) { + Assert.hasText(containerName, "containerName should not be null, empty or only whitespaces"); + Assert.notNull(objectToSave, "objectToSave should not be null"); + + final Class domainType = (Class) objectToSave.getClass(); + final CosmosItemProperties originalItem = mappingCosmosConverter.writeCosmosItemProperties(objectToSave); + final CosmosItemRequestOptions options = new CosmosItemRequestOptions(); + if (partitionKey != null) { + options.partitionKey(partitionKey); + } + return cosmosClient.getDatabase(this.databaseName) + .getContainer(containerName) + .createItem(originalItem, options) + .onErrorResume(throwable -> + exceptionHandler("Failed to insert item", throwable)) + .flatMap(cosmosItemResponse -> { + fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + cosmosItemResponse, null); + return Mono.just(toDomainObject(domainType, cosmosItemResponse.properties())); + }); + } + + /** + * Upsert + * + * @param object the object to upsert + * @param partitionKey the partition key + * @return Mono with the item or error + */ + @Override + public Mono upsert(T object, PartitionKey partitionKey) { + return upsert(getContainerName(object.getClass()), object, partitionKey); + } + + /** + * Upsert + * + * @param containerName the container name + * @param object the object to save + * @param partitionKey the partition key + * @return Mono with the item or error + */ + @Override + public Mono upsert(String containerName, T object, PartitionKey partitionKey) { + final Class domainType = (Class) object.getClass(); + final CosmosItemProperties originalItem = mappingCosmosConverter.writeCosmosItemProperties(object); + final CosmosItemRequestOptions options = new CosmosItemRequestOptions(); + if (partitionKey != null) { + options.partitionKey(partitionKey); + } + + applyVersioning(object.getClass(), originalItem, options); + + return cosmosClient.getDatabase(this.databaseName) + .getContainer(containerName) + .upsertItem(originalItem, options) + .flatMap(cosmosItemResponse -> { + fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + cosmosItemResponse, null); + return Mono.just(toDomainObject(domainType, cosmosItemResponse.properties())); + }) + .onErrorResume(throwable -> + exceptionHandler("Failed to upsert item", throwable)); + } + + /** + * Delete an item by id + * + * @param containerName the container name + * @param id the id + * @param partitionKey the partition key + * @return void Mono + */ + @Override + public Mono deleteById(String containerName, Object id, PartitionKey partitionKey) { + Assert.hasText(containerName, "container name should not be null, empty or only whitespaces"); + assertValidId(id); + + if (partitionKey == null) { + partitionKey = PartitionKey.None; + } + + final CosmosItemRequestOptions options = new CosmosItemRequestOptions(); + options.partitionKey(partitionKey); + return cosmosClient.getDatabase(this.databaseName) + .getContainer(containerName) + .getItem(id.toString(), partitionKey) + .delete(options) + .doOnNext(cosmosItemResponse -> + fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + cosmosItemResponse, null)) + .onErrorResume(throwable -> + exceptionHandler("Failed to delete item", throwable)) + .then(); + } + + /** + * Delete all items in a container + * + * @param containerName the container name + * @param partitionKeyName the partition key path + * @return void Mono + */ + @Override + public Mono deleteAll(String containerName, String partitionKeyName) { + Assert.hasText(containerName, "container name should not be null, empty or only whitespaces"); + Assert.notNull(partitionKeyName, "partitionKeyName should not be null"); + + final Criteria criteria = Criteria.getInstance(CriteriaType.ALL); + final DocumentQuery query = new DocumentQuery(criteria); + final SqlQuerySpec sqlQuerySpec = new FindQuerySpecGenerator().generateCosmos(query); + final FeedOptions options = new FeedOptions(); + final boolean isCrossPartitionQuery = query.isCrossPartitionQuery(Collections.singletonList(partitionKeyName)); + options.enableCrossPartitionQuery(isCrossPartitionQuery); + options.populateQueryMetrics(isPopulateQueryMetrics); + return cosmosClient.getDatabase(this.databaseName) + .getContainer(containerName) + .queryItems(sqlQuerySpec, options) + .onErrorResume(throwable -> + exceptionHandler("Failed to query items", throwable)) + .flatMap(cosmosItemFeedResponse -> { + fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + null, cosmosItemFeedResponse); + return Flux.fromIterable(cosmosItemFeedResponse.results()); + }) + .flatMap(cosmosItemProperties -> cosmosClient + .getDatabase(this.databaseName) + .getContainer(containerName) + .getItem(cosmosItemProperties.id(), cosmosItemProperties.get(partitionKeyName)) + .delete() + .doOnNext(cosmosItemResponse -> + fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + cosmosItemResponse, null)) + .onErrorResume(throwable -> + exceptionHandler("Failed to delete items", throwable))) + .then(); + } + + /** + * Delete items matching query + * + * @param query the document query + * @param domainType the entity class + * @param containerName the container name + * @return Mono + */ + @Override + public Flux delete(DocumentQuery query, Class domainType, String containerName) { + Assert.notNull(query, "DocumentQuery should not be null."); + Assert.notNull(domainType, "domainType should not be null."); + Assert.hasText(containerName, "container name should not be null, empty or only whitespaces"); + + final Flux results = findItems(query, domainType, containerName); + final List partitionKeyName = getPartitionKeyNames(domainType); + + return results.flatMap(d -> deleteItem(d, partitionKeyName, containerName, domainType)) + .flatMap(cosmosItemProperties -> Mono.just(toDomainObject(domainType, cosmosItemProperties))); + } + + /** + * Find items + * + * @param query the document query + * @param domainType the entity class + * @param containerName the container name + * @return Flux with found items or error + */ + @Override + public Flux find(DocumentQuery query, Class domainType, String containerName) { + return findItems(query, domainType, containerName) + .map(cosmosItemProperties -> toDomainObject(domainType, cosmosItemProperties)); + } + + /** + * Exists + * + * @param query the document query + * @param domainType the entity class + * @param containerName the container name + * @return Mono with a boolean or error + */ + @Override + public Mono exists(DocumentQuery query, Class domainType, String containerName) { + return count(query, true, containerName).flatMap(count -> Mono.just(count > 0)); + } + + /** + * Exists + * @param id the id + * @param domainType the entity class + * @param containerName the containercontainer nam,e + * @return Mono with a boolean or error + */ + public Mono existsById(Object id, Class domainType, String containerName) { + return findById(containerName, id, domainType) + .flatMap(o -> Mono.just(o != null)); + } + + /** + * Count + * + * @param containerName the container name + * @return Mono with the count or error + */ + @Override + public Mono count(String containerName) { + final DocumentQuery query = new DocumentQuery(Criteria.getInstance(CriteriaType.ALL)); + return count(query, true, containerName); + } + + /** + * Count + * + * @param query the document query + * @param containerName the container name + * @return Mono with count or error + */ + @Override + public Mono count(DocumentQuery query, String containerName) { + return count(query, true, containerName); + } + + @Override + public MappingCosmosConverter getConverter() { + return mappingCosmosConverter; + } + + /** + *Count + * + * @param query the document query + * @param isCrossPartitionQuery flag of cross partition + * @param containerName the container name + * @return Mono + */ + public Mono count(DocumentQuery query, boolean isCrossPartitionQuery, String containerName) { + return getCountValue(query, isCrossPartitionQuery, containerName); + } + + private Mono getCountValue(DocumentQuery query, boolean isCrossPartitionQuery, String containerName) { + final SqlQuerySpec querySpec = new CountQueryGenerator().generateCosmos(query); + final FeedOptions options = new FeedOptions(); + + options.enableCrossPartitionQuery(isCrossPartitionQuery); + options.populateQueryMetrics(isPopulateQueryMetrics); + + return executeQuery(querySpec, containerName, options) + .doOnNext(feedResponse -> fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + null, feedResponse)) + .onErrorResume(throwable -> + exceptionHandler("Failed to get count value", throwable)) + .next() + .map(r -> r.results().get(0).getLong(COUNT_VALUE_KEY)); + } + + private Flux> executeQuery(SqlQuerySpec sqlQuerySpec, String containerName, + FeedOptions options) { + + return cosmosClient.getDatabase(this.databaseName) + .getContainer(containerName) + .queryItems(sqlQuerySpec, options) + .onErrorResume(throwable -> + exceptionHandler("Failed to execute query", throwable)); + } + + /** + * Delete container with container name + * + * @param containerName the container name + */ + @Override + public void deleteContainer(@NonNull String containerName) { + Assert.hasText(containerName, "containerName should have text."); + cosmosClient.getDatabase(this.databaseName) + .getContainer(containerName) + .delete() + .doOnNext(cosmosContainerResponse -> + fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + cosmosContainerResponse, null)) + .onErrorResume(throwable -> + exceptionHandler("Failed to delete container", throwable)) + .block(); + this.containerNameCache.remove(containerName); + } + + /** + * @param domainType the domain class + * @return the container name + */ + public String getContainerName(Class domainType) { + Assert.notNull(domainType, "domainType should not be null"); + + return entityInfoCreator.apply(domainType).getContainerName(); + } + + private Flux findItems(@NonNull DocumentQuery query, @NonNull Class domainType, + @NonNull String containerName) { + final SqlQuerySpec sqlQuerySpec = new FindQuerySpecGenerator().generateCosmos(query); + final boolean isCrossPartitionQuery = query.isCrossPartitionQuery(getPartitionKeyNames(domainType)); + final FeedOptions feedOptions = new FeedOptions(); + feedOptions.enableCrossPartitionQuery(isCrossPartitionQuery); + feedOptions.populateQueryMetrics(isPopulateQueryMetrics); + + return cosmosClient + .getDatabase(this.databaseName) + .getContainer(containerName) + .queryItems(sqlQuerySpec, feedOptions) + .flatMap(cosmosItemFeedResponse -> { + fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + null, cosmosItemFeedResponse); + return Flux.fromIterable(cosmosItemFeedResponse.results()); + }) + .onErrorResume(throwable -> + exceptionHandler("Failed to query items", throwable)); + } + + private void assertValidId(Object id) { + Assert.notNull(id, "id should not be null"); + if (id instanceof String) { + Assert.hasText(id.toString(), "id should not be empty or only whitespaces."); + } + } + + private List getPartitionKeyNames(Class domainType) { + final CosmosEntityInformation entityInfo = entityInfoCreator.apply(domainType); + + if (entityInfo.getPartitionKeyFieldName() == null) { + return new ArrayList<>(); + } + + return Collections.singletonList(entityInfo.getPartitionKeyFieldName()); + } + + private Mono deleteItem(@NonNull CosmosItemProperties cosmosItemProperties, + @NonNull List partitionKeyNames, + String containerName, + @NonNull Class domainType) { + Assert.isTrue(partitionKeyNames.size() <= 1, "Only one Partition is supported."); + + PartitionKey partitionKey = null; + + if (!partitionKeyNames.isEmpty() + && StringUtils.hasText(partitionKeyNames.get(0))) { + partitionKey = new PartitionKey(cosmosItemProperties.get(partitionKeyNames.get(0))); + } + + final CosmosItemRequestOptions options = new CosmosItemRequestOptions(partitionKey); + applyVersioning(domainType, cosmosItemProperties, options); + + return cosmosClient.getDatabase(this.databaseName) + .getContainer(containerName) + .getItem(cosmosItemProperties.id(), partitionKey) + .delete(options) + .map(cosmosItemResponse -> { + fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, + cosmosItemResponse, null); + return cosmosItemProperties; + }) + .onErrorResume(throwable -> + exceptionHandler("Failed to delete item", throwable)); + } + + private T toDomainObject(@NonNull Class domainType, CosmosItemProperties cosmosItemProperties) { + return mappingCosmosConverter.read(domainType, cosmosItemProperties); + } + + private void applyVersioning(Class domainType, + CosmosItemProperties cosmosItemProperties, + CosmosItemRequestOptions options) { + + if (entityInfoCreator.apply(domainType).isVersioned()) { + final AccessCondition accessCondition = new AccessCondition(); + accessCondition.type(AccessConditionType.IF_MATCH); + accessCondition.condition(cosmosItemProperties.etag()); + options.accessCondition(accessCondition); + } + } + + private CosmosEntityInformation getCosmosEntityInformation(Class domainType) { + return new CosmosEntityInformation<>(domainType); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/ResponseDiagnostics.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/ResponseDiagnostics.java new file mode 100644 index 000000000000..316eee94c4f1 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/ResponseDiagnostics.java @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.core; + +import com.azure.data.cosmos.CosmosResponseDiagnostics; +import com.azure.data.cosmos.FeedResponse; +import com.azure.data.cosmos.FeedResponseDiagnostics; +import com.azure.data.cosmos.Resource; + +/** + * Diagnostics class of cosmos and feed response + */ +public class ResponseDiagnostics { + + private CosmosResponseDiagnostics cosmosResponseDiagnostics; + private FeedResponseDiagnostics feedResponseDiagnostics; + private CosmosResponseStatistics cosmosResponseStatistics; + + /** + * Initialization + * + * @param cosmosResponseDiagnostics cannot be null + * @param feedResponseDiagnostics cannot be null + */ + public ResponseDiagnostics(CosmosResponseDiagnostics cosmosResponseDiagnostics, + FeedResponseDiagnostics feedResponseDiagnostics) { + this.cosmosResponseDiagnostics = cosmosResponseDiagnostics; + this.feedResponseDiagnostics = feedResponseDiagnostics; + } + + /** + * Initialization + * + * @param cosmosResponseDiagnostics cannot be null + * @param feedResponseDiagnostics cannot be null + * @param cosmosResponseStatistics cannot be null + */ + public ResponseDiagnostics(CosmosResponseDiagnostics cosmosResponseDiagnostics, + FeedResponseDiagnostics feedResponseDiagnostics, + CosmosResponseStatistics cosmosResponseStatistics) { + this.cosmosResponseDiagnostics = cosmosResponseDiagnostics; + this.feedResponseDiagnostics = feedResponseDiagnostics; + this.cosmosResponseStatistics = cosmosResponseStatistics; + } + + /** + * To get diagnostics of cosmos response + * @return CosmosResponseDiagnostics + */ + public CosmosResponseDiagnostics getCosmosResponseDiagnostics() { + return cosmosResponseDiagnostics; + } + + /** + * To set diagnostics of cosmos response + * @param cosmosResponseDiagnostics cannot be null + */ + public void setCosmosResponseDiagnostics(CosmosResponseDiagnostics cosmosResponseDiagnostics) { + this.cosmosResponseDiagnostics = cosmosResponseDiagnostics; + } + + /** + * To get diagnostics of feed response + * @return FeedResponseDiagnostics + */ + public FeedResponseDiagnostics getFeedResponseDiagnostics() { + return feedResponseDiagnostics; + } + + /** + * To set diagnostics of feed response + * @param feedResponseDiagnostics cannot be null + */ + public void setFeedResponseDiagnostics(FeedResponseDiagnostics feedResponseDiagnostics) { + this.feedResponseDiagnostics = feedResponseDiagnostics; + } + + /** + * To get the statistics value of cosmos response + * @return CosmosResponseStatistics + */ + public CosmosResponseStatistics getCosmosResponseStatistics() { + return cosmosResponseStatistics; + } + + /** + * To set statistics of cosmos response + * @param cosmosResponseStatistics cannot be null + */ + public void setCosmosResponseStatistics(CosmosResponseStatistics cosmosResponseStatistics) { + this.cosmosResponseStatistics = cosmosResponseStatistics; + } + + @Override + public String toString() { + final StringBuilder diagnostics = new StringBuilder(); + if (cosmosResponseDiagnostics != null) { + diagnostics.append("cosmosResponseDiagnostics={") + .append(cosmosResponseDiagnostics) + .append("}"); + } + if (feedResponseDiagnostics != null) { + if (diagnostics.length() != 0) { + diagnostics.append(", "); + } + diagnostics.append("feedResponseDiagnostics={") + .append(feedResponseDiagnostics) + .append("}"); + } + if (cosmosResponseStatistics != null) { + if (diagnostics.length() != 0) { + diagnostics.append(", "); + } + diagnostics.append("cosmosResponseStatistics={") + .append(cosmosResponseStatistics) + .append("}"); + } + return diagnostics.toString(); + } + + /** + * Generates statistics from cosmos response + */ + public static class CosmosResponseStatistics { + + private final double requestCharge; + private final String activityId; + + /** + * Initialization + * + * @param feedResponse response from feed + * @param type of cosmosResponse + */ + public CosmosResponseStatistics(FeedResponse feedResponse) { + this.requestCharge = feedResponse.requestCharge(); + this.activityId = feedResponse.activityId(); + } + + /** + * To get the charge value of request + * @return double + */ + public double getRequestCharge() { + return requestCharge; + } + + /** + * To get the activity id + * @return String + */ + public String getActivityId() { + return activityId; + } + + @Override + public String toString() { + return "CosmosResponseStatistics{" + + "requestCharge=" + + requestCharge + + ", activityId='" + + activityId + + '\'' + + '}'; + } + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/ResponseDiagnosticsProcessor.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/ResponseDiagnosticsProcessor.java new file mode 100644 index 000000000000..b8407cc36c03 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/ResponseDiagnosticsProcessor.java @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.core; + +import javax.annotation.Nullable; + +/** + * Interface for processing cosmosdb response + */ +public interface ResponseDiagnosticsProcessor { + + /** + * Gets called after receiving response from CosmosDb. + * Response Diagnostics are collected from API responses and + * then set in {@link ResponseDiagnostics} object. + *

+ * In case of missing diagnostics from CosmosDb, responseDiagnostics will be null. + * + * @param responseDiagnostics responseDiagnostics object containing CosmosDb response + * diagnostics information + */ + void processResponseDiagnostics(@Nullable ResponseDiagnostics responseDiagnostics); +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/convert/MappingCosmosConverter.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/convert/MappingCosmosConverter.java new file mode 100644 index 000000000000..a09e3f8e4de5 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/convert/MappingCosmosConverter.java @@ -0,0 +1,202 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.core.convert; + +import com.azure.data.cosmos.CosmosItemProperties; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.azure.spring.data.cosmosdb.Constants; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.CosmosPersistentEntity; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.CosmosPersistentProperty; +import com.microsoft.azure.spring.data.cosmosdb.exception.CosmosDBAccessException; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.data.convert.EntityConverter; +import org.springframework.data.mapping.MappingException; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.ConvertingPropertyAccessor; +import org.springframework.util.Assert; + +import java.io.IOException; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Date; + +import static com.microsoft.azure.spring.data.cosmosdb.Constants.ISO_8601_COMPATIBLE_DATE_PATTERN; + +/** + * A converter class between common types and cosmosItemProperties + */ +@SuppressWarnings("unchecked") +public class MappingCosmosConverter + implements EntityConverter, CosmosPersistentProperty, + Object, CosmosItemProperties>, + ApplicationContextAware { + + protected final MappingContext, + CosmosPersistentProperty> mappingContext; + protected GenericConversionService conversionService; + private ApplicationContext applicationContext; + private final ObjectMapper objectMapper; + + /** + * Initialization + * @param mappingContext must not be {@literal null} + * @param objectMapper must not be {@literal null} + */ + public MappingCosmosConverter( + MappingContext, CosmosPersistentProperty> mappingContext, + @Qualifier(Constants.OBJECTMAPPER_BEAN_NAME) ObjectMapper objectMapper) { + this.mappingContext = mappingContext; + this.conversionService = new GenericConversionService(); + this.objectMapper = objectMapper == null ? ObjectMapperFactory.getObjectMapper() + : objectMapper; + } + + @Override + public R read(Class type, CosmosItemProperties cosmosItemProperties) { + if (cosmosItemProperties == null) { + return null; + } + + final CosmosPersistentEntity entity = mappingContext.getPersistentEntity(type); + Assert.notNull(entity, "Entity is null."); + + return readInternal(entity, type, cosmosItemProperties); + } + + private R readInternal(final CosmosPersistentEntity entity, Class type, + final CosmosItemProperties cosmosItemProperties) { + + try { + final CosmosPersistentProperty idProperty = entity.getIdProperty(); + final Object idValue = cosmosItemProperties.id(); + final JSONObject jsonObject = new JSONObject(cosmosItemProperties.toJson()); + + if (idProperty != null) { + // Replace the key id to the actual id field name in domain + jsonObject.remove(Constants.ID_PROPERTY_NAME); + jsonObject.put(idProperty.getName(), idValue); + } + + return objectMapper.readValue(jsonObject.toString(), type); + } catch (IOException e) { + throw new IllegalStateException("Failed to read the source document " + + cosmosItemProperties.toJson() + + " to target type " + + type, e); + } + } + + @Override + @Deprecated + public void write(Object sourceEntity, CosmosItemProperties document) { + throw new UnsupportedOperationException("The feature is not implemented yet"); + } + + /** + * To write source entity as a cosmos item + * @param sourceEntity must not be {@literal null} + * @return CosmosItemProperties + * @throws MappingException no mapping metadata for entity type + * @throws CosmosDBAccessException fail to map document value + */ + public CosmosItemProperties writeCosmosItemProperties(Object sourceEntity) { + if (sourceEntity == null) { + return null; + } + + final CosmosPersistentEntity persistentEntity = + mappingContext.getPersistentEntity(sourceEntity.getClass()); + + if (persistentEntity == null) { + throw new MappingException("no mapping metadata for entity type: " + + sourceEntity.getClass().getName()); + } + + final ConvertingPropertyAccessor accessor = getPropertyAccessor(sourceEntity); + final CosmosPersistentProperty idProperty = persistentEntity.getIdProperty(); + final CosmosItemProperties cosmosItemProperties; + + try { + cosmosItemProperties = + new CosmosItemProperties(objectMapper.writeValueAsString(sourceEntity)); + } catch (JsonProcessingException e) { + throw new CosmosDBAccessException("Failed to map document value.", e); + } + + if (idProperty != null) { + final Object value = accessor.getProperty(idProperty); + final String id = value == null ? null : value.toString(); + cosmosItemProperties.id(id); + } + + return cosmosItemProperties; + } + + /** + * To get application context + * @return ApplicationContext + */ + public ApplicationContext getApplicationContext() { + return this.applicationContext; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + public ConversionService getConversionService() { + return conversionService; + } + + /** + * To get mapping context + * @return MappingContext + */ + public MappingContext, CosmosPersistentProperty> getMappingContext() { + return mappingContext; + } + + + private ConvertingPropertyAccessor getPropertyAccessor(Object entity) { + final CosmosPersistentEntity entityInformation = + mappingContext.getPersistentEntity(entity.getClass()); + + Assert.notNull(entityInformation, "EntityInformation should not be null."); + final PersistentPropertyAccessor accessor = entityInformation.getPropertyAccessor(entity); + return new ConvertingPropertyAccessor<>(accessor, conversionService); + } + + /** + * Convert a property value to the value stored in CosmosDB + * + * @param fromPropertyValue source property value + * @return fromPropertyValue converted property value stored in CosmosDB + */ + public static Object toCosmosDbValue(Object fromPropertyValue) { + if (fromPropertyValue == null) { + return null; + } + + // com.microsoft.azure.data.cosmos.JsonSerializable#set(String, T) cannot set values for Date and Enum correctly + + if (fromPropertyValue instanceof Date) { + fromPropertyValue = ((Date) fromPropertyValue).getTime(); + } else if (fromPropertyValue instanceof ZonedDateTime) { + fromPropertyValue = ((ZonedDateTime) fromPropertyValue) + .format(DateTimeFormatter.ofPattern(ISO_8601_COMPATIBLE_DATE_PATTERN)); + } else if (fromPropertyValue instanceof Enum) { + fromPropertyValue = fromPropertyValue.toString(); + } + + return fromPropertyValue; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/convert/ObjectMapperFactory.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/convert/ObjectMapperFactory.java new file mode 100644 index 000000000000..7cdef7ced6f3 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/convert/ObjectMapperFactory.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.core.convert; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; + +/** + * Factory class for object mapper + */ +public class ObjectMapperFactory { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + static { + OBJECT_MAPPER.registerModule(new ParameterNamesModule()) + .registerModule(new Jdk8Module()) + .registerModule(new JavaTimeModule()); + OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + /** + * To get object mapper + * @return ObjectMapper + */ + public static ObjectMapper getObjectMapper() { + return OBJECT_MAPPER; + } + +} + diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/convert/package-info.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/convert/package-info.java new file mode 100644 index 000000000000..dc67fc729470 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/convert/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * This package contains the converter classes of cosmos db + */ +package com.microsoft.azure.spring.data.cosmosdb.core.convert; diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/generator/AbstractQueryGenerator.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/generator/AbstractQueryGenerator.java new file mode 100644 index 000000000000..c28b1f6ed1f8 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/generator/AbstractQueryGenerator.java @@ -0,0 +1,227 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.core.generator; + +import com.azure.data.cosmos.SqlParameterList; +import com.azure.data.cosmos.SqlQuerySpec; +import com.microsoft.azure.spring.data.cosmosdb.core.query.Criteria; +import com.microsoft.azure.spring.data.cosmosdb.core.query.CriteriaType; +import com.microsoft.azure.spring.data.cosmosdb.core.query.DocumentQuery; +import com.microsoft.azure.spring.data.cosmosdb.exception.IllegalQueryException; +import org.javatuples.Pair; +import org.springframework.data.domain.Sort; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import static com.microsoft.azure.spring.data.cosmosdb.core.convert.MappingCosmosConverter.toCosmosDbValue; + +/** + * Base class for generating sql query + */ +public abstract class AbstractQueryGenerator { + + protected AbstractQueryGenerator() { + } + + private String generateQueryParameter(@NonNull String subject) { + return subject.replaceAll("\\.", "_"); // user.name is not valid sql parameter identifier. + } + + private String generateUnaryQuery(@NonNull Criteria criteria) { + Assert.isTrue(criteria.getSubjectValues().isEmpty(), "Unary criteria should have no one subject value"); + Assert.isTrue(CriteriaType.isUnary(criteria.getType()), "Criteria type should be unary operation"); + final String subject = criteria.getSubject(); + + if (CriteriaType.isFunction(criteria.getType())) { + return String.format("%s(r.%s)", criteria.getType().getSqlKeyword(), subject); + } else { + return String.format("r.%s %s", subject, criteria.getType().getSqlKeyword()); + } + } + + private String generateBinaryQuery(@NonNull Criteria criteria, @NonNull List> parameters) { + Assert.isTrue(criteria.getSubjectValues().size() == 1, + "Binary criteria should have only one subject value"); + Assert.isTrue(CriteriaType.isBinary(criteria.getType()), "Criteria type should be binary operation"); + + final String subject = criteria.getSubject(); + final Object subjectValue = toCosmosDbValue(criteria.getSubjectValues().get(0)); + final String parameter = generateQueryParameter(subject); + + parameters.add(Pair.with(parameter, subjectValue)); + + if (CriteriaType.isFunction(criteria.getType())) { + return String.format("%s(r.%s, @%s)", criteria.getType().getSqlKeyword(), subject, parameter); + } else { + return String.format("r.%s %s @%s", subject, criteria.getType().getSqlKeyword(), parameter); + } + } + + private String generateBetween(@NonNull Criteria criteria, @NonNull List> parameters) { + final String subject = criteria.getSubject(); + final Object value1 = toCosmosDbValue(criteria.getSubjectValues().get(0)); + final Object value2 = toCosmosDbValue(criteria.getSubjectValues().get(1)); + final String subject1 = subject + "start"; + final String subject2 = subject + "end"; + final String parameter1 = generateQueryParameter(subject1); + final String parameter2 = generateQueryParameter(subject2); + final String keyword = criteria.getType().getSqlKeyword(); + + parameters.add(Pair.with(parameter1, value1)); + parameters.add(Pair.with(parameter2, value2)); + + return String.format("(r.%s %s @%s AND @%s)", subject, keyword, parameter1, parameter2); + } + + private String generateClosedQuery(@NonNull String left, @NonNull String right, CriteriaType type) { + Assert.isTrue(CriteriaType.isClosed(type) + && CriteriaType.isBinary(type), + "Criteria type should be binary and closure operation"); + + return String.join(" ", left, type.getSqlKeyword(), right); + } + + @SuppressWarnings("unchecked") + private String generateInQuery(Criteria criteria) { + Assert.isTrue(criteria.getSubjectValues().size() == 1, + "Criteria should have only one subject value"); + if (!(criteria.getSubjectValues().get(0) instanceof Collection)) { + throw new IllegalQueryException("IN keyword requires Collection type in parameters"); + } + final List inRangeValues = new ArrayList<>(); + final Collection values = (Collection) criteria.getSubjectValues().get(0); + + values.forEach(o -> { + if (o instanceof Integer || o instanceof Long) { + inRangeValues.add(String.format("%d", o)); + } else if (o instanceof String) { + inRangeValues.add(String.format("'%s'", (String) o)); + } else if (o instanceof Boolean) { + inRangeValues.add(String.format("%b", (Boolean) o)); + } else { + throw new IllegalQueryException("IN keyword Range only support Number and String type."); + } + }); + + final String inRange = String.join(",", inRangeValues); + return String.format("r.%s %s (%s)", criteria.getSubject(), criteria.getType().getSqlKeyword(), inRange); + } + + private String generateQueryBody(@NonNull Criteria criteria, @NonNull List> parameters) { + final CriteriaType type = criteria.getType(); + + switch (type) { + case ALL: + return ""; + case IN: + case NOT_IN: + return generateInQuery(criteria); + case BETWEEN: + return generateBetween(criteria, parameters); + case IS_NULL: + case IS_NOT_NULL: + case FALSE: + case TRUE: + return generateUnaryQuery(criteria); + case IS_EQUAL: + case NOT: + case BEFORE: + case AFTER: + case LESS_THAN: + case LESS_THAN_EQUAL: + case GREATER_THAN: + case GREATER_THAN_EQUAL: + case CONTAINING: + case ENDS_WITH: + case STARTS_WITH: + return generateBinaryQuery(criteria, parameters); + case AND: + case OR: + Assert.isTrue(criteria.getSubCriteria().size() == 2, + "criteria should have two SubCriteria"); + + final String left = generateQueryBody(criteria.getSubCriteria().get(0), parameters); + final String right = generateQueryBody(criteria.getSubCriteria().get(1), parameters); + + return generateClosedQuery(left, right, type); + default: + throw new UnsupportedOperationException("unsupported Criteria type: " + + type); + } + } + + /** + * Generate a query body for interface QuerySpecGenerator. + * The query body compose of Sql query String and its' parameters. + * The parameters organized as a list of Pair, for each pair compose parameter name and value. + * + * @param query the representation for query method. + * @return A pair tuple compose of Sql query. + */ + @NonNull + private Pair>> generateQueryBody(@NonNull DocumentQuery query) { + final List> parameters = new ArrayList<>(); + String queryString = this.generateQueryBody(query.getCriteria(), parameters); + + if (StringUtils.hasText(queryString)) { + queryString = String.join(" ", "WHERE", queryString); + } + + return Pair.with(queryString, parameters); + } + + private String getParameter(@NonNull Sort.Order order) { + Assert.isTrue(!order.isIgnoreCase(), "Ignore case is not supported"); + + final String direction = order.isDescending() ? "DESC" : "ASC"; + + return String.format("r.%s %s", order.getProperty(), direction); + } + + private String generateQuerySort(@NonNull Sort sort) { + if (sort.isUnsorted()) { + return ""; + } + + final String queryTail = "ORDER BY"; + final List subjects = sort.stream().map(this::getParameter).collect(Collectors.toList()); + + return queryTail + + " " + + String.join(",", subjects); + } + + @NonNull + private String generateQueryTail(@NonNull DocumentQuery query) { + final List queryTails = new ArrayList<>(); + + queryTails.add(generateQuerySort(query.getSort())); + + return String.join(" ", queryTails.stream().filter(StringUtils::hasText).collect(Collectors.toList())); + } + + + protected SqlQuerySpec generateCosmosQuery(@NonNull DocumentQuery query, + @NonNull String queryHead) { + final Pair>> queryBody = generateQueryBody(query); + final String queryString = String.join(" ", queryHead, queryBody.getValue0(), generateQueryTail(query)); + final List> parameters = queryBody.getValue1(); + final SqlParameterList sqlParameters = + new SqlParameterList(); + + sqlParameters.addAll( + parameters.stream() + .map(p -> new com.azure.data.cosmos.SqlParameter("@" + + p.getValue0(), toCosmosDbValue(p.getValue1()))) + .collect(Collectors.toList()) + ); + + return new SqlQuerySpec(queryString, sqlParameters); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/generator/CountQueryGenerator.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/generator/CountQueryGenerator.java new file mode 100644 index 000000000000..bddecf136e4c --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/generator/CountQueryGenerator.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.core.generator; + +import com.azure.data.cosmos.SqlQuerySpec; +import com.microsoft.azure.spring.data.cosmosdb.core.query.DocumentQuery; + +/** + * Generate count query + */ +public class CountQueryGenerator extends AbstractQueryGenerator implements QuerySpecGenerator { + + @Override + public SqlQuerySpec generateCosmos(DocumentQuery query) { + return super.generateCosmosQuery(query, "SELECT VALUE COUNT(1) FROM r"); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/generator/FindQuerySpecGenerator.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/generator/FindQuerySpecGenerator.java new file mode 100644 index 000000000000..3296e23e89b2 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/generator/FindQuerySpecGenerator.java @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.core.generator; + +import com.azure.data.cosmos.SqlQuerySpec; +import com.microsoft.azure.spring.data.cosmosdb.core.query.DocumentQuery; + +/** + * Generate sql find query + */ +public class FindQuerySpecGenerator extends AbstractQueryGenerator implements QuerySpecGenerator { + /** + * Initialization + */ + public FindQuerySpecGenerator() { + } + + @Override + public SqlQuerySpec generateCosmos(DocumentQuery query) { + return super.generateCosmosQuery(query, "SELECT * FROM ROOT r"); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/generator/QuerySpecGenerator.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/generator/QuerySpecGenerator.java new file mode 100644 index 000000000000..a1cf9e8e0e2f --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/generator/QuerySpecGenerator.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.core.generator; + +import com.azure.data.cosmos.SqlQuerySpec; +import com.microsoft.azure.spring.data.cosmosdb.core.query.DocumentQuery; + +/** + * Interface of generating SqlQuerySpec + */ +public interface QuerySpecGenerator { + + /** + * Generate the SqlQuerySpec for cosmosDB client. + * @param query tree structured query condition. + * @return SqlQuerySpec executed by cosmos client. + */ + SqlQuerySpec generateCosmos(DocumentQuery query); +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/generator/package-info.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/generator/package-info.java new file mode 100644 index 000000000000..b67e5a92e7e3 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/generator/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * This package contains the generator classes of cosmos db + */ +package com.microsoft.azure.spring.data.cosmosdb.core.generator; diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/BasicCosmosPersistentEntity.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/BasicCosmosPersistentEntity.java new file mode 100644 index 000000000000..89ccc99ada71 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/BasicCosmosPersistentEntity.java @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.core.mapping; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.expression.BeanFactoryAccessor; +import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.data.mapping.model.BasicPersistentEntity; +import org.springframework.data.util.TypeInformation; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +/** + * Simple value object to capture information of {@link CosmosPersistentProperty}s. + */ +public class BasicCosmosPersistentEntity extends BasicPersistentEntity + implements CosmosPersistentEntity, ApplicationContextAware { + + private final StandardEvaluationContext context; + + /** + * Creates a new {@link BasicCosmosPersistentEntity} from the given {@link TypeInformation}. + * + * @param typeInformation must not be {@literal null}. + */ + public BasicCosmosPersistentEntity(TypeInformation typeInformation) { + super(typeInformation); + this.context = new StandardEvaluationContext(); + } + + /** + * To set application context + * @param applicationContext must not be {@literal null}. + * @throws BeansException the bean exception + */ + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + context.addPropertyAccessor(new BeanFactoryAccessor()); + context.setBeanResolver(new BeanFactoryResolver(applicationContext)); + context.setRootObject(applicationContext); + } + + /** + * To get collection of entity + * @return String + */ + public String getCollection() { + return ""; + } + + @Override + public String getContainer() { + return ""; + } + + @Override + public String getLanguage() { + return ""; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/BasicCosmosPersistentProperty.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/BasicCosmosPersistentProperty.java new file mode 100644 index 000000000000..7b5fd02c173e --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/BasicCosmosPersistentProperty.java @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.core.mapping; + +import com.microsoft.azure.spring.data.cosmosdb.Constants; +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.model.AnnotationBasedPersistentProperty; +import org.springframework.data.mapping.model.Property; +import org.springframework.data.mapping.model.SimpleTypeHolder; + +/** + * Special {@link CosmosPersistentProperty} that takes annotations at a property into account. + */ +public class BasicCosmosPersistentProperty extends AnnotationBasedPersistentProperty + implements CosmosPersistentProperty { + + /** + * Creates a new {@link BasicCosmosPersistentProperty}. + * + * @param property must not be {@literal null}. + * @param owner must not be {@literal null}. + * @param simpleTypeHolder must not be {@literal null}. + */ + public BasicCosmosPersistentProperty(Property property, CosmosPersistentEntity owner, + SimpleTypeHolder simpleTypeHolder) { + super(property, owner, simpleTypeHolder); + } + + @Override + protected Association createAssociation() { + return new Association<>(this, null); + } + + @Override + public boolean isIdProperty() { + + if (super.isIdProperty()) { + return true; + } + + return getName().equals(Constants.ID_PROPERTY_NAME); + } + +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/CosmosMappingContext.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/CosmosMappingContext.java new file mode 100644 index 000000000000..f2c1c7bf2bb4 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/CosmosMappingContext.java @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.core.mapping; + +import org.springframework.context.ApplicationContext; +import org.springframework.data.mapping.context.AbstractMappingContext; +import org.springframework.data.mapping.model.Property; +import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.util.TypeInformation; + +/** + * Class to build mapping metadata and thus create instances of {@link BasicCosmosPersistentEntity} and + * {@link CosmosPersistentProperty}. + */ +public class CosmosMappingContext + extends AbstractMappingContext, CosmosPersistentProperty> { + + private ApplicationContext context; + + @Override + protected BasicCosmosPersistentEntity createPersistentEntity(TypeInformation typeInformation) { + final BasicCosmosPersistentEntity entity = new BasicCosmosPersistentEntity<>(typeInformation); + + if (context != null) { + entity.setApplicationContext(context); + } + return entity; + } + + @Override + public CosmosPersistentProperty createPersistentProperty(Property property, + BasicCosmosPersistentEntity owner, + SimpleTypeHolder simpleTypeHolder) { + return new BasicCosmosPersistentProperty(property, owner, simpleTypeHolder); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.context = applicationContext; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/CosmosPersistentEntity.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/CosmosPersistentEntity.java new file mode 100644 index 000000000000..3418648de325 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/CosmosPersistentEntity.java @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.core.mapping; + +import org.springframework.data.mapping.PersistentEntity; + +/** + * Represents a cosmos persistent entity. + */ +public interface CosmosPersistentEntity extends PersistentEntity { + /** + * To get collection + * @return String + * @deprecated use {@link #getContainer()} instead + */ + @Deprecated + String getCollection(); + + /** + * To get container of entity + * @return String + */ + String getContainer(); + + /** + * To get language + * @return String + */ + String getLanguage(); +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/CosmosPersistentProperty.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/CosmosPersistentProperty.java new file mode 100644 index 000000000000..31ce39f31037 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/CosmosPersistentProperty.java @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.core.mapping; + +import org.springframework.data.mapping.PersistentProperty; + +/** + * Interface for cosmos persistent property + */ +public interface CosmosPersistentProperty extends PersistentProperty { +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/Document.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/Document.java new file mode 100644 index 000000000000..79cd9534e794 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/Document.java @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.core.mapping; + +import com.microsoft.azure.spring.data.cosmosdb.Constants; +import org.springframework.data.annotation.Persistent; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.annotation.ElementType; + +/** + * Annotation of cosmos document + */ +@Persistent +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface Document { + /** + * To set collection name + * @return String + */ + String collection() default Constants.DEFAULT_COLLECTION_NAME; + + /** + * To set request unit + * @return default as 4000 + */ + String ru() default Constants.DEFAULT_REQUEST_UNIT; + + /** + * To set the ttl of container level + * @return default as no ttl + */ + int timeToLive() default Constants.DEFAULT_TIME_TO_LIVE; + + /** + * To set if create collection automatically + * @return default as true + */ + boolean autoCreateCollection() default Constants.DEFAULT_AUTO_CREATE_CONTAINER; +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/DocumentIndexingPolicy.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/DocumentIndexingPolicy.java new file mode 100644 index 000000000000..e578cdb40933 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/DocumentIndexingPolicy.java @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.core.mapping; + +import com.azure.data.cosmos.IndexingMode; +import com.microsoft.azure.spring.data.cosmosdb.Constants; +import org.springframework.data.annotation.Persistent; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.annotation.ElementType; + +/** + * Annotation for document indexing policy + */ +@Persistent +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DocumentIndexingPolicy { + /** + * To set automatic indexing + * @return default as true + */ + boolean automatic() default Constants.DEFAULT_INDEXINGPOLICY_AUTOMATIC; + + /** + * To set indexing mode + * + * @return IndexingMode + */ + IndexingMode mode() default IndexingMode.CONSISTENT; // Enum is not really compile time constant + + /** + * To include paths + * @return String[] + */ + String[] includePaths() default {}; + + /** + * To exclude paths + * @return String[] + */ + String[] excludePaths() default {}; +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/PartitionKey.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/PartitionKey.java new file mode 100644 index 000000000000..6ae69dd61509 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/PartitionKey.java @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.core.mapping; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.annotation.ElementType; + +/** + * Interface for type partition key + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD}) +public @interface PartitionKey { + + /** + * The name of the partition key if the serialized attribute name differs from the field name + * + * @return partition key name + */ + String value() default ""; +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/package-info.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/package-info.java new file mode 100644 index 000000000000..e7a0f48b4da8 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * This package contains the mapping classes of cosmos persistent entities + */ +package com.microsoft.azure.spring.data.cosmosdb.core.mapping; diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/package-info.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/package-info.java new file mode 100644 index 000000000000..0a5a2dee562e --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * This package contains the core classes of cosmos db, includes converters, + * query generators and mapping to cosmos entities + */ +package com.microsoft.azure.spring.data.cosmosdb.core; diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/query/CosmosPageImpl.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/query/CosmosPageImpl.java new file mode 100644 index 000000000000..771e53910e51 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/query/CosmosPageImpl.java @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.core.query; + +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import java.util.List; +import java.util.Objects; + +/** + * {@code CosmosPageImpl} implementation. + * + * @param the type of which the CosmosPageImpl consists. + */ +public class CosmosPageImpl extends PageImpl { + + private static final long serialVersionUID = 5294396337522314504L; + + // For any query, CosmosDB returns documents less than or equal to page size + // Depending on the number of RUs, the number of returned documents can change + // Storing the offset of current page, helps to check hasNext and next values + private final long offset; + + /** + * Constructor of {@code CosmosPageImpl}. + * + * @param content the content of this page, must not be {@literal null}. + * @param pageable the paging information, must not be {@literal null}. + * @param total amount of items available. The total might be adapted considering the length of the content + * given, if it is going to be the content of the last page. This is in place to mitigate inconsistencies. + */ + public CosmosPageImpl(List content, Pageable pageable, long total) { + super(content, pageable, total); + this.offset = pageable.getOffset(); + } + + @Override + public int getTotalPages() { + return super.getTotalPages(); + } + + @Override + public long getTotalElements() { + return super.getTotalElements(); + } + + @Override + public boolean hasNext() { + return this.offset + + getContent().size() < getTotalElements(); + } + + @Override + public boolean isLast() { + return super.isLast(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + final CosmosPageImpl that = (CosmosPageImpl) o; + return offset == that.offset; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), offset); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/query/CosmosPageRequest.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/query/CosmosPageRequest.java new file mode 100644 index 000000000000..68d98dea0c30 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/query/CosmosPageRequest.java @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.core.query; + +import com.azure.data.cosmos.FeedResponse; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +/** + * CosmosPageRequest representing page request during pagination query, field + * {@link FeedResponse#continuationToken()} response continuation token} is saved + * to help query next page. + *

+ * The requestContinuation token should be saved after each request and reused in later queries. + */ +public class CosmosPageRequest extends PageRequest { + private static final long serialVersionUID = 6093304300037688375L; + + private long offset; + + // Request continuation token used to resume query + private final String requestContinuation; + + /** + * Creates a new {@link PageRequest} with unsorted parameters applied. + * + * @param page zero-based page index, must not be negative. + * @param size the size of the page to be returned, must be greater than 0. + * @param requestContinuation must not be {@literal null}. + */ + public CosmosPageRequest(int page, int size, String requestContinuation) { + super(page, size, Sort.unsorted()); + this.requestContinuation = requestContinuation; + } + + /** + * Creates a new {@link CosmosPageRequest} with sort parameters applied. + * + * @param page zero-based page index, must not be negative. + * @param size the size of the page to be returned, must be greater than 0. + * @param sort must not be {@literal null}, use {@link Sort#unsorted()} instead. + * @param requestContinuation must not be {@literal null}. + */ + public CosmosPageRequest(int page, int size, String requestContinuation, Sort sort) { + super(page, size, sort); + this.requestContinuation = requestContinuation; + } + + private CosmosPageRequest(long offset, int page, int size, String requestContinuation) { + super(page, size, Sort.unsorted()); + this.offset = offset; + this.requestContinuation = requestContinuation; + } + + private CosmosPageRequest(long offset, int page, int size, String requestContinuation, + Sort sort) { + super(page, size, sort); + this.offset = offset; + this.requestContinuation = requestContinuation; + } + + /** + * Creates a new {@link CosmosPageRequest} + * + * @param page zero-based page index, must not be negative. + * @param size the size of the page to be returned, must be greater than 0. + * @param requestContinuation cannot be null + * @param sort cannot be null + * @return CosmosPageRequest + */ + public static CosmosPageRequest of(int page, int size, String requestContinuation, Sort sort) { + return new CosmosPageRequest(0, page, size, requestContinuation, sort); + } + + /** + * Creates a new {@link CosmosPageRequest} + * + * @param offset cannot be null + * @param page zero-based page index, must not be negative. + * @param size the size of the page to be returned, must be greater than 0. + * @param requestContinuation cannot be null + * @param sort cannot be null + * @return CosmosPageRequest + */ + public static CosmosPageRequest of(long offset, int page, int size, String requestContinuation, Sort sort) { + return new CosmosPageRequest(offset, page, size, requestContinuation, sort); + } + + @Override + public Pageable next() { + return new CosmosPageRequest(this.offset + (long) this.getPageSize(), + this.getPageNumber() + 1, getPageSize(), this.requestContinuation, getSort()); + } + + @Override + public long getOffset() { + return offset; + } + + /** + * To get request continuation + * @return String + */ + public String getRequestContinuation() { + return this.requestContinuation; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + + result = 31 * result + + (requestContinuation != null ? requestContinuation.hashCode() : 0); + + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof CosmosPageRequest)) { + return false; + } + + final CosmosPageRequest that = (CosmosPageRequest) obj; + + final boolean continuationTokenEquals = requestContinuation != null + ? requestContinuation.equals(that.requestContinuation) : that.requestContinuation == null; + + return continuationTokenEquals + && super.equals(that); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/query/Criteria.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/query/Criteria.java new file mode 100644 index 000000000000..5a0d7b8c5a31 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/query/Criteria.java @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.core.query; + +import org.springframework.lang.NonNull; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class of criteria + */ +public final class Criteria { + + private String subject; + private List subjectValues; + private final CriteriaType type; + private final List subCriteria; + + /** + * To get subject + * @return subject value + */ + public String getSubject() { + return subject; + } + + /** + * To get CriteriaType + * @return CriteriaType + */ + public List getSubjectValues() { + return subjectValues; + } + + /** + * To get CriteriaType + * @return CriteriaType + */ + public CriteriaType getType() { + return type; + } + + /** + * To get sub criteria + * @return List of sub criteria + */ + public List getSubCriteria() { + return subCriteria; + } + + private Criteria(CriteriaType type) { + this.type = type; + this.subCriteria = new ArrayList<>(); + } + + /** + * To get a criteria instance with subject + * @param type CriteriaType + * @param subject subject + * @param values subject value + * @return Criteria instance + */ + public static Criteria getInstance(CriteriaType type, @NonNull String subject, @NonNull List values) { + final Criteria criteria = new Criteria(type); + + criteria.subject = subject; + criteria.subjectValues = values; + + return criteria; + } + + /** + * To get a criteria instance with sub criteria + * @param type CriteriaType + * @param left Criteria + * @param right Criteria + * @return Criteria instance + */ + public static Criteria getInstance(CriteriaType type, @NonNull Criteria left, @NonNull Criteria right) { + final Criteria criteria = new Criteria(type); + + criteria.subCriteria.add(left); + criteria.subCriteria.add(right); + + return criteria; + } + + /** + * To get a new criteria instance + * @param type CriteriaType + * @return Criteria instance + */ + public static Criteria getInstance(CriteriaType type) { + return new Criteria(type); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/query/CriteriaType.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/query/CriteriaType.java new file mode 100644 index 000000000000..caf6c927875e --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/query/CriteriaType.java @@ -0,0 +1,204 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.core.query; + +import org.springframework.lang.NonNull; +import org.springframework.data.repository.query.parser.Part; + +import java.beans.ConstructorProperties; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Enum of criteria type + */ +public enum CriteriaType { + + ALL(""), + IS_EQUAL("="), + OR("OR"), + AND("AND"), + NOT("<>"), + BEFORE("<"), + AFTER(">"), + IN("IN"), + NOT_IN("NOT IN"), + IS_NULL("IS_NULL"), + IS_NOT_NULL("NOT IS_NULL"), + LESS_THAN("<"), + LESS_THAN_EQUAL("<="), + GREATER_THAN(">"), + GREATER_THAN_EQUAL(">="), + CONTAINING("CONTAINS"), + ENDS_WITH("ENDSWITH"), + STARTS_WITH("STARTSWITH"), + TRUE("= true"), + FALSE("= false"), + BETWEEN("BETWEEN"); + + private String sqlKeyword; + + private static final Map PART_TREE_TYPE_TO_CRITERIA; + + static { + final Map map = new HashMap<>(); + + map.put(Part.Type.NEGATING_SIMPLE_PROPERTY, CriteriaType.NOT); + map.put(Part.Type.IS_NULL, CriteriaType.IS_NULL); + map.put(Part.Type.IS_NOT_NULL, CriteriaType.IS_NOT_NULL); + map.put(Part.Type.SIMPLE_PROPERTY, CriteriaType.IS_EQUAL); + map.put(Part.Type.BEFORE, CriteriaType.BEFORE); + map.put(Part.Type.AFTER, CriteriaType.AFTER); + map.put(Part.Type.IN, CriteriaType.IN); + map.put(Part.Type.NOT_IN, CriteriaType.NOT_IN); + map.put(Part.Type.GREATER_THAN, CriteriaType.GREATER_THAN); + map.put(Part.Type.CONTAINING, CriteriaType.CONTAINING); + map.put(Part.Type.ENDING_WITH, CriteriaType.ENDS_WITH); + map.put(Part.Type.STARTING_WITH, CriteriaType.STARTS_WITH); + map.put(Part.Type.GREATER_THAN_EQUAL, CriteriaType.GREATER_THAN_EQUAL); + map.put(Part.Type.LESS_THAN, CriteriaType.LESS_THAN); + map.put(Part.Type.LESS_THAN_EQUAL, CriteriaType.LESS_THAN_EQUAL); + map.put(Part.Type.TRUE, CriteriaType.TRUE); + map.put(Part.Type.FALSE, CriteriaType.FALSE); + map.put(Part.Type.BETWEEN, CriteriaType.BETWEEN); + + PART_TREE_TYPE_TO_CRITERIA = Collections.unmodifiableMap(map); + } + + @ConstructorProperties({"sqlKeyword"}) + CriteriaType(String sqlKeyword) { + this.sqlKeyword = sqlKeyword; + } + + /** + * To get sql keyword + * @return String + */ + public String getSqlKeyword() { + return sqlKeyword; + } + + /** + * Check if PartType is NOT supported. + * + * @param partType PartType to be checked. + * @return True if unsupported, or false. + */ + public static boolean isPartTypeUnSupported(@NonNull Part.Type partType) { + return !isPartTypeSupported(partType); + } + + /** + * Check if PartType is supported. + * + * @param partType PartType to be checked. + * @return True if supported, or false. + */ + public static boolean isPartTypeSupported(@NonNull Part.Type partType) { + return PART_TREE_TYPE_TO_CRITERIA.containsKey(partType); + } + + /** + * Convert to criteria type. + * + * @param partType PartType to be converted. + * @return CriteriaType + * @throws UnsupportedOperationException for unsupported part type + */ + @SuppressWarnings("") + public static CriteriaType toCriteriaType(@NonNull Part.Type partType) { + final CriteriaType criteriaType = PART_TREE_TYPE_TO_CRITERIA.get(partType); + + if (criteriaType == null) { + throw new UnsupportedOperationException("Unsupported part type: " + + partType); + } + + return criteriaType; + } + + /** + * Check if CriteriaType operation is closure, with format of (A ops A -> A). + * Example: AND, OR. + * + * @param type CriteriaType operation + * @return True if match, or false. + */ + public static boolean isClosed(CriteriaType type) { + switch (type) { + case AND: + case OR: + return true; + default: + return false; + } + } + + /** + * Check if CriteriaType operation is binary, with format of (A ops A -> B). + * Example: IS_EQUAL, AFTER. + * + * @param type CriteriaType operation + * @return True if match, or false. + */ + public static boolean isBinary(CriteriaType type) { + switch (type) { + case IN: + case NOT_IN: + case AND: + case OR: + case NOT: + case IS_EQUAL: + case BEFORE: + case AFTER: + case LESS_THAN: + case LESS_THAN_EQUAL: + case GREATER_THAN: + case GREATER_THAN_EQUAL: + case CONTAINING: + case ENDS_WITH: + case STARTS_WITH: + return true; + default: + return false; + } + } + + /** + * Check if CriteriaType operation is a function. + * + * @param type CriteriaType + * @return True if match, or false. + */ + public static boolean isFunction(CriteriaType type) { + switch (type) { + case CONTAINING: + case ENDS_WITH: + case STARTS_WITH: + case IS_NULL: + case IS_NOT_NULL: + return true; + default: + return false; + } + } + + /** + * Check if CriteriaType operation is unary, with format of (ops A -> B). + * + * @param type CriteriaType + * @return True if match, or false. + */ + public static boolean isUnary(CriteriaType type) { + switch (type) { + case IS_NULL: + case IS_NOT_NULL: + case TRUE: + case FALSE: + return true; + default: + return false; + } + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/query/DocumentQuery.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/query/DocumentQuery.java new file mode 100644 index 000000000000..8adfb92d93b7 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/query/DocumentQuery.java @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.core.query; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; + +import java.util.List; +import java.util.Optional; + +/** + * Class for document query + */ +public class DocumentQuery { + + private final Criteria criteria; + + private Sort sort = Sort.unsorted(); + + private Pageable pageable = Pageable.unpaged(); + + /** + * Initialization + * + * @param criteria object + */ + public DocumentQuery(@NonNull Criteria criteria) { + this.criteria = criteria; + } + + /** + * To get Criteria object + * + * @return Criteria + */ + public Criteria getCriteria() { + return criteria; + } + + /** + * To get Sort object + * + * @return Sort + */ + public Sort getSort() { + return sort; + } + + /** + * To get Pageable object + * + * @return Pageable + */ + public Pageable getPageable() { + return pageable; + } + + /** + * With Sort + * + * @param sort Sort + * @return DocumentQuery object + */ + public DocumentQuery with(@NonNull Sort sort) { + if (sort.isSorted()) { + this.sort = sort.and(this.sort); + } + + return this; + } + + /** + * With Sort + * + * @param pageable Sort + * @return DocumentQuery object + */ + public DocumentQuery with(@NonNull Pageable pageable) { + Assert.notNull(pageable, "pageable should not be null"); + + this.pageable = pageable; + return this; + } + + private boolean isCrossPartitionQuery(@NonNull String keyName) { + Assert.hasText(keyName, "PartitionKey should have text."); + + final Optional criteria = this.getSubjectCriteria(this.criteria, keyName); + + return criteria.map(criteria1 -> criteria1.getType() != CriteriaType.IS_EQUAL).orElse(true); + } + + private boolean hasKeywordOr() { + // If there is OR keyword in DocumentQuery, the top node of Criteria must be OR type. + return this.criteria.getType() == CriteriaType.OR; + } + + /** + * Indicate if DocumentQuery should enable cross partition query. + * + * @param partitionKeys The list of partitionKey names. + * @return If DocumentQuery should enable cross partition query + */ + public boolean isCrossPartitionQuery(@NonNull List partitionKeys) { + if (partitionKeys.isEmpty()) { + return true; + } + + return partitionKeys.stream().filter(this::isCrossPartitionQuery) + .findFirst() + .map(p -> true) + .orElse(hasKeywordOr()); + } + + /** + * To get criteria by type + * @param criteriaType the criteria type + * @return Optional + */ + public Optional getCriteriaByType(@NonNull CriteriaType criteriaType) { + return getCriteriaByType(criteriaType, this.criteria); + } + + private Optional getCriteriaByType(@NonNull CriteriaType criteriaType, @NonNull Criteria criteria) { + if (criteria.getType().equals(criteriaType)) { + return Optional.of(criteria); + } + + for (final Criteria subCriteria: criteria.getSubCriteria()) { + if (getCriteriaByType(criteriaType, subCriteria).isPresent()) { + return Optional.of(subCriteria); + } + } + + return Optional.empty(); + } + + private Optional getSubjectCriteria(@NonNull Criteria criteria, @NonNull String keyName) { + if (keyName.equals(criteria.getSubject())) { + return Optional.of(criteria); + } + + final List subCriteriaList = criteria.getSubCriteria(); + + for (final Criteria c : subCriteriaList) { + final Optional subjectCriteria = getSubjectCriteria(c, keyName); + + if (subjectCriteria.isPresent()) { + return subjectCriteria; + } + } + + return Optional.empty(); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/query/package-info.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/query/package-info.java new file mode 100644 index 000000000000..bbd03f8b278f --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/core/query/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * This package contains the query classes of cosmos db document + */ +package com.microsoft.azure.spring.data.cosmosdb.core.query; diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/exception/ConfigurationException.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/exception/ConfigurationException.java new file mode 100644 index 000000000000..3aea8e557a7f --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/exception/ConfigurationException.java @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.exception; + +import org.springframework.dao.DataAccessException; + +/** + * General exception for illegal configuration of cosmosdb + */ +public class ConfigurationException extends DataAccessException { + + /** + * Construct a {@code IllegalQueryException} with the specified detail message. + * @param msg the detail message + */ + public ConfigurationException(String msg) { + super(msg); + } + + /** + * Construct a {@code IllegalQueryException} with the specified detail message + * and nested exception. + * + * @param msg the detail message + * @param cause the nested exception + */ + public ConfigurationException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/exception/CosmosDBAccessException.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/exception/CosmosDBAccessException.java new file mode 100644 index 000000000000..f20791008ab8 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/exception/CosmosDBAccessException.java @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.exception; + +import com.azure.data.cosmos.CosmosClientException; +import org.springframework.dao.DataAccessException; +import org.springframework.lang.Nullable; + +/** + * Public class extending DataAccessException, exposes innerException. + * Every API in {@link com.microsoft.azure.spring.data.cosmosdb.repository.CosmosRepository} + * and {@link com.microsoft.azure.spring.data.cosmosdb.repository.ReactiveCosmosRepository} + * should throw {@link CosmosDBAccessException}. + * innerException refers to the exception thrown by CosmosDB SDK. Callers of repository APIs can + * rely on innerException for any retriable logic, or for more details on the failure of + * the operation. + */ +public class CosmosDBAccessException extends DataAccessException { + + protected final CosmosClientException cosmosClientException; + + /** + * Construct a {@code CosmosDBAccessException} with the specified detail message. + * @param msg the detail message + */ + public CosmosDBAccessException(String msg) { + super(msg); + this.cosmosClientException = null; + } + + /** + * Construct a {@code CosmosDBAccessException} with the specified detail message. + * and nested exception. + * @param msg the detail message + * @param cause the nested Throwable + */ + public CosmosDBAccessException(@Nullable String msg, @Nullable Throwable cause) { + super(msg, cause); + if (cause instanceof CosmosClientException) { + this.cosmosClientException = (CosmosClientException) cause; + } else { + this.cosmosClientException = null; + } + } + + /** + * Construct a {@code CosmosDBAccessException} with the specified detail message + * and nested exception. + * + * @param msg the detail message + * @param cause the nested exception + */ + public CosmosDBAccessException(@Nullable String msg, @Nullable Exception cause) { + super(msg, cause); + this.cosmosClientException = cause instanceof CosmosClientException + ? (CosmosClientException) cause + : null; + } + + /** + * To get exception object for cosmos client + * @return CosmosClientException + */ + public CosmosClientException getCosmosClientException() { + return cosmosClientException; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/exception/CosmosDBExceptionUtils.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/exception/CosmosDBExceptionUtils.java new file mode 100644 index 000000000000..4e11eca0c4e6 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/exception/CosmosDBExceptionUtils.java @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.exception; + +import com.azure.data.cosmos.CosmosClientException; +import com.azure.data.cosmos.internal.HttpConstants; +import org.springframework.util.StringUtils; +import reactor.core.Exceptions; +import reactor.core.publisher.Mono; + +/** + * To handle and throw a cosmosdb exception when access the database + */ +public class CosmosDBExceptionUtils { + + /** + * To throw a CosmosDBAccessException + * + * @param message the detail message + * @param throwable exception + * @param type class of Mono + * @return Mono instance + * @throws CosmosDBAccessException for operations on cosmosdb + */ + public static Mono exceptionHandler(String message, Throwable throwable) { + if (StringUtils.isEmpty(message)) { + message = "Failed to access cosmosdb database"; + } + // Unwrap the exception in case if it is a reactive exception + final Throwable unwrappedThrowable = Exceptions.unwrap(throwable); + throw new CosmosDBAccessException(message, unwrappedThrowable); + } + + /** + * To find an exceptionHandler for a excetption and return empty Mono if the exception status code is not found + * + * @param message the detail message + * @param throwable exception + * @param type class of Mono + * @return Mono instance + */ + public static Mono findAPIExceptionHandler(String message, Throwable throwable) { + // Unwrap the exception in case if it is a reactive exception + final Throwable unwrappedThrowable = Exceptions.unwrap(throwable); + if (unwrappedThrowable instanceof CosmosClientException) { + final CosmosClientException cosmosClientException = (CosmosClientException) unwrappedThrowable; + if (cosmosClientException.statusCode() == HttpConstants.StatusCodes.NOTFOUND) { + return Mono.empty(); + } + } + return exceptionHandler(message, unwrappedThrowable); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/exception/DatabaseCreationException.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/exception/DatabaseCreationException.java new file mode 100644 index 000000000000..d0230725cf97 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/exception/DatabaseCreationException.java @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.exception; + +import org.springframework.dao.DataAccessException; +import org.springframework.lang.Nullable; + +/** + * General exception for illegal creation of cosmosdb + */ +public class DatabaseCreationException extends DataAccessException { + + /** + * Construct a {@code IllegalQueryException} with the specified detail message. + * @param msg the detail message + */ + public DatabaseCreationException(String msg) { + super(msg); + } + + /** + * Construct a {@code IllegalQueryException} with the specified detail message + * and nested exception. + * + * @param msg the detail message + * @param cause the nested exception + */ + public DatabaseCreationException(@Nullable String msg, @Nullable Throwable cause) { + super(msg, cause); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/exception/IllegalCollectionException.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/exception/IllegalCollectionException.java new file mode 100644 index 000000000000..c447f2d5d291 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/exception/IllegalCollectionException.java @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.exception; + +import org.springframework.dao.DataAccessException; +import org.springframework.lang.Nullable; + +/** + * General exception for illegal collection of cosmosdb + */ +public class IllegalCollectionException extends DataAccessException { + + /** + * Construct a {@code IllegalQueryException} with the specified detail message. + * @param msg the detail message + */ + public IllegalCollectionException(String msg) { + super(msg); + } + + /** + * Construct a {@code IllegalQueryException} with the specified detail message + * and nested exception. + * + * @param msg the detail message + * @param cause the nested exception + */ + public IllegalCollectionException(@Nullable String msg, @Nullable Throwable cause) { + super(msg, cause); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/exception/IllegalQueryException.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/exception/IllegalQueryException.java new file mode 100644 index 000000000000..75c33a0bb5e9 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/exception/IllegalQueryException.java @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.exception; + +import org.springframework.dao.DataAccessException; +import org.springframework.lang.Nullable; + +/** + * General exception for illegal query of cosmosdb + */ +public class IllegalQueryException extends DataAccessException { + + /** + * Construct a {@code IllegalQueryException} with the specified detail message. + * @param msg the detail message + */ + public IllegalQueryException(String msg) { + super(msg); + } + + /** + * Construct a {@code IllegalQueryException} with the specified detail message + * and nested exception. + * + * @param msg the detail message + * @param cause the nested exception + */ + public IllegalQueryException(@Nullable String msg, @Nullable Throwable cause) { + super(msg, cause); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/exception/package-info.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/exception/package-info.java new file mode 100644 index 000000000000..a420e54064b7 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/exception/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * This package contains the exception classes of cosmos db + */ +package com.microsoft.azure.spring.data.cosmosdb.exception; diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/package-info.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/package-info.java new file mode 100644 index 000000000000..12dc2276157d --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Cosmosdb class for spring + */ +package com.microsoft.azure.spring.data.cosmosdb; diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/CosmosRepository.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/CosmosRepository.java new file mode 100644 index 000000000000..85274905a3c1 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/CosmosRepository.java @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.repository; + +import com.azure.data.cosmos.PartitionKey; +import org.springframework.data.repository.NoRepositoryBean; +import org.springframework.data.repository.PagingAndSortingRepository; + +import java.io.Serializable; +import java.util.List; +import java.util.Optional; + +/** + * Extension of {@link PagingAndSortingRepository} to provide additional methods to retrieve entities + * using the pagination and sorting abstraction. + */ +@NoRepositoryBean +public interface CosmosRepository extends PagingAndSortingRepository { + + /** + * Retrieves an entity by its id. + * + * @param id must not be {@literal null}. + * @param partitionKey partition key value of entity, must not be null. + * @return the entity with the given id or {@literal Optional#empty()} if none found + * @throws IllegalArgumentException if {@code id} is {@literal null}. + */ + Optional findById(ID id, PartitionKey partitionKey); + + /** + * Deletes an entity by its id and partition key. + * + * @param id must not be {@literal null}. + * @param partitionKey partition key value of the entity, must not be null. + * @throws IllegalArgumentException in case the given {@code id} is {@literal null}. + */ + void deleteById(ID id, PartitionKey partitionKey); + + /** + * Returns list of items in a specific partition + * + * @param partitionKey partition key value + * @return list of items with partition key value + */ + List findAll(PartitionKey partitionKey); + +} + diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/ReactiveCosmosRepository.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/ReactiveCosmosRepository.java new file mode 100644 index 000000000000..b963adc03fcd --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/ReactiveCosmosRepository.java @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository; + +import com.azure.data.cosmos.PartitionKey; +import org.springframework.data.repository.NoRepositoryBean; +import org.springframework.data.repository.reactive.ReactiveSortingRepository; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +/** + * Repository interface with search and delete operation + */ +@NoRepositoryBean +public interface ReactiveCosmosRepository extends ReactiveSortingRepository { + + /** + * Retrieves an entity by its id and partition key. + * @param id must not be {@literal null}. + * @param partitionKey partition key value of the entity, must not be null. + * @return {@link Mono} emitting the entity with the given id or {@link Mono#empty()} if none found. + * @throws IllegalArgumentException in case the given {@code id} is {@literal null}. + */ + Mono findById(K id, PartitionKey partitionKey); + + /** + * Deletes an entity by its id and partition key. + * @param id must not be {@literal null}. + * @param partitionKey partition key value of the entity, must not be null. + * @return {@link Mono} emitting the void Mono. + * @throws IllegalArgumentException in case the given {@code id} is {@literal null}. + */ + Mono deleteById(K id, PartitionKey partitionKey); + + /** + * Returns Flux of items in a specific partition + * @param partitionKey partition key value + * @return {@link Flux} of items with partition key value + */ + Flux findAll(PartitionKey partitionKey); +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/CosmosRepositoriesRegistrar.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/CosmosRepositoriesRegistrar.java new file mode 100644 index 000000000000..ba20dd279785 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/CosmosRepositoriesRegistrar.java @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.repository.config; + +import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport; +import org.springframework.data.repository.config.RepositoryConfigurationExtension; + +import java.lang.annotation.Annotation; + +/** + * Inherit {@link RepositoryBeanDefinitionRegistrarSupport} class to enable repository and get extension + */ +public class CosmosRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport { + + @Override + protected Class getAnnotation() { + return EnableCosmosRepositories.class; + } + + @Override + protected RepositoryConfigurationExtension getExtension() { + return new CosmosRepositoryConfigurationExtension(); + } + + +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/CosmosRepositoryConfigurationExtension.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/CosmosRepositoryConfigurationExtension.java new file mode 100644 index 000000000000..2c479f88887b --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/CosmosRepositoryConfigurationExtension.java @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.repository.config; + +import com.microsoft.azure.spring.data.cosmosdb.Constants; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.CosmosMappingContext; +import com.microsoft.azure.spring.data.cosmosdb.repository.CosmosRepository; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosRepositoryFactoryBean; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; +import org.springframework.data.repository.config.RepositoryConfigurationSource; +import org.springframework.data.repository.core.RepositoryMetadata; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Collections; + +/** + * Configuration extension class based on {@link RepositoryConfigurationExtensionSupport} provide options to set + * repository support. + */ +public class CosmosRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport { + + @Override + public String getModuleName() { + return Constants.COSMOSDB_MODULE_NAME; + } + + @Override + public String getModulePrefix() { + return Constants.COSMOSDB_MODULE_PREFIX; + } + + /** + * Return the name of the repository factory bean class. + * @return String value of bean name + */ + public String getRepositoryFactoryBeanClassName() { + return CosmosRepositoryFactoryBean.class.getName(); + } + + @Override + protected Collection> getIdentifyingTypes() { + return Collections.>singleton(CosmosRepository.class); + } + + @Override + protected Collection> getIdentifyingAnnotations() { + return Collections.emptyList(); + } + + + @Override + public void registerBeansForRoot(BeanDefinitionRegistry registry, RepositoryConfigurationSource config) { + super.registerBeansForRoot(registry, config); + + if (!registry.containsBeanDefinition(Constants.COSMOS_MAPPING_CONTEXT)) { + final RootBeanDefinition definition = new RootBeanDefinition(CosmosMappingContext.class); + definition.setRole(AbstractBeanDefinition.ROLE_INFRASTRUCTURE); + definition.setSource(config.getSource()); + + registry.registerBeanDefinition(Constants.COSMOS_MAPPING_CONTEXT, definition); + } + } + + @Override + public void postProcess(BeanDefinitionBuilder builder, RepositoryConfigurationSource source) { + super.postProcess(builder, source); + } + + // Overriding this to provide reactive repository support. + @Override + protected boolean useRepositoryConfiguration(RepositoryMetadata metadata) { + // CosmosRepository is the sync repository, and hence returning !isReactiveRepository. + // ReactiveCosmosRepository is reactive repository. + return !metadata.isReactiveRepository(); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/EnableCosmosRepositories.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/EnableCosmosRepositories.java new file mode 100644 index 000000000000..14eca6637790 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/EnableCosmosRepositories.java @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.repository.config; + +import com.microsoft.azure.spring.data.cosmosdb.Constants; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosRepositoryFactoryBean; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Import; +import org.springframework.data.repository.config.DefaultRepositoryBaseClass; +import org.springframework.data.repository.query.QueryLookupStrategy; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Interface to enable cosmos repository + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Import(CosmosRepositoriesRegistrar.class) +public @interface EnableCosmosRepositories { + + /** + * Toset repo value + * @return default as {} + */ + String[] value() default {}; + + /** + * To set base packages + * @return default as {} + */ + String[] basePackages() default {}; + + /** + * To set base package class + * @return default as {} + */ + Class[] basePackageClasses() default {}; + + /** + * To include filters + * @return default as {} + */ + Filter[] includeFilters() default {}; + + /** + * To exclude filters + * @return default as {} + */ + Filter[] excludeFilters() default {}; + + /** + * To set repo Implement postfix + * @return default as "Impl" + */ + String repositoryImplementationPostfix() default Constants.DEFAULT_REPOSITORY_IMPLEMENT_POSTFIX; + + /** + * To set the named query location + * @return default as "" + */ + String namedQueriesLocation() default ""; + + /** + * To set query look up strategy + * @return QueryLookupStrategy.Key + */ + QueryLookupStrategy.Key queryLookupStrategy() default QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND; + + /** + * To set factory bean class of repo + * @return default value is ReactiveCosmosRepositoryFactoryBean.class + */ + Class repositoryFactoryBeanClass() default CosmosRepositoryFactoryBean.class; + + /** + * To set base class of repo + * @return default value is DefaultRepositoryBaseClass.class + */ + Class repositoryBaseClass() default DefaultRepositoryBaseClass.class; + + /** + * To set if consider nested repositories + * @return default value is false + */ + boolean considerNestedRepositories() default false; +} + diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/EnableReactiveCosmosRepositories.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/EnableReactiveCosmosRepositories.java new file mode 100644 index 000000000000..7eb05a311832 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/EnableReactiveCosmosRepositories.java @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.repository.config; + +import com.microsoft.azure.spring.data.cosmosdb.Constants; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.ReactiveCosmosRepositoryFactoryBean; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Import; +import org.springframework.data.repository.config.DefaultRepositoryBaseClass; +import org.springframework.data.repository.query.QueryLookupStrategy; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Interface to enable reactive cosmos repository + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Import(ReactiveCosmosRepositoriesRegistrar.class) +public @interface EnableReactiveCosmosRepositories { + + /** + * Toset repo value + * @return default as {} + */ + String[] value() default {}; + + /** + * To set base packages + * @return default as {} + */ + String[] basePackages() default {}; + + /** + * To set base package class + * @return default as {} + */ + Class[] basePackageClasses() default {}; + + /** + * To include filters + * @return default as {} + */ + Filter[] includeFilters() default {}; + + /** + * To exclude filters + * @return default as {} + */ + Filter[] excludeFilters() default {}; + + /** + * To set repo Implement postfix + * @return default as "Impl" + */ + String repositoryImplementationPostfix() default Constants.DEFAULT_REPOSITORY_IMPLEMENT_POSTFIX; + + /** + * To set the named query location + * @return default as "" + */ + String namedQueriesLocation() default ""; + + /** + * To set query look up strategy + * @return QueryLookupStrategy.Key + */ + QueryLookupStrategy.Key queryLookupStrategy() default QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND; + + /** + * To set factory bean class of repo + * @return default value is ReactiveCosmosRepositoryFactoryBean.class + */ + Class repositoryFactoryBeanClass() default ReactiveCosmosRepositoryFactoryBean.class; + + /** + * To set base class of repo + * @return default value is DefaultRepositoryBaseClass.class + */ + Class repositoryBaseClass() default DefaultRepositoryBaseClass.class; + + /** + * To set if consider nested repositories + * @return default value is false + */ + boolean considerNestedRepositories() default false; +} + diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/ReactiveCosmosRepositoriesRegistrar.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/ReactiveCosmosRepositoriesRegistrar.java new file mode 100644 index 000000000000..bbcb6b2b84f4 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/ReactiveCosmosRepositoriesRegistrar.java @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.repository.config; + +import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport; +import org.springframework.data.repository.config.RepositoryConfigurationExtension; + +import java.lang.annotation.Annotation; + +/** + * Inherit {@link RepositoryBeanDefinitionRegistrarSupport} class to enable repository and get extension + */ +public class ReactiveCosmosRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport { + + @Override + protected Class getAnnotation() { + return EnableReactiveCosmosRepositories.class; + } + + @Override + protected RepositoryConfigurationExtension getExtension() { + return new ReactiveCosmosRepositoryConfigurationExtension(); + } + + +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/ReactiveCosmosRepositoryConfigurationExtension.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/ReactiveCosmosRepositoryConfigurationExtension.java new file mode 100644 index 000000000000..e9a5b87faa59 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/ReactiveCosmosRepositoryConfigurationExtension.java @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.repository.config; + +import com.microsoft.azure.spring.data.cosmosdb.Constants; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.CosmosMappingContext; +import com.microsoft.azure.spring.data.cosmosdb.repository.ReactiveCosmosRepository; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.ReactiveCosmosRepositoryFactoryBean; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; +import org.springframework.data.repository.config.RepositoryConfigurationSource; +import org.springframework.data.repository.core.RepositoryMetadata; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Collections; + +/** + * Configuration extension class based on {@link RepositoryConfigurationExtensionSupport} provide options to set + * reactive repository support. + */ +public class ReactiveCosmosRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport { + + @Override + public String getModuleName() { + return Constants.COSMOSDB_MODULE_NAME; + } + + @Override + public String getModulePrefix() { + return Constants.COSMOSDB_MODULE_PREFIX; + } + + /** + * Return the name of the repository factory bean class. + * @return String value of bean name + */ + public String getRepositoryFactoryBeanClassName() { + return ReactiveCosmosRepositoryFactoryBean.class.getName(); + } + + @Override + protected Collection> getIdentifyingTypes() { + return Collections.>singleton(ReactiveCosmosRepository.class); + } + + @Override + protected Collection> getIdentifyingAnnotations() { + return Collections.emptyList(); + } + + + @Override + public void registerBeansForRoot(BeanDefinitionRegistry registry, RepositoryConfigurationSource config) { + super.registerBeansForRoot(registry, config); + + if (!registry.containsBeanDefinition(Constants.COSMOS_MAPPING_CONTEXT)) { + final RootBeanDefinition definition = new RootBeanDefinition(CosmosMappingContext.class); + definition.setRole(AbstractBeanDefinition.ROLE_INFRASTRUCTURE); + definition.setSource(config.getSource()); + + registry.registerBeanDefinition(Constants.COSMOS_MAPPING_CONTEXT, definition); + } + } + + @Override + public void postProcess(BeanDefinitionBuilder builder, RepositoryConfigurationSource source) { + super.postProcess(builder, source); + } + + // Overriding this to provide reactive repository support. + @Override + protected boolean useRepositoryConfiguration(RepositoryMetadata metadata) { + return metadata.isReactiveRepository(); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/package-info.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/package-info.java new file mode 100644 index 000000000000..7891cc7d30ad --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * This package contains the config classes of setting up cosmosdb repositories + */ +package com.microsoft.azure.spring.data.cosmosdb.repository.config; diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/package-info.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/package-info.java new file mode 100644 index 000000000000..fa885616b308 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * This package contains the support, query and config classes of setting up cosmosdb repositories + */ +package com.microsoft.azure.spring.data.cosmosdb.repository; diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/AbstractCosmosQuery.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/AbstractCosmosQuery.java new file mode 100644 index 000000000000..55e012d185e6 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/AbstractCosmosQuery.java @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.query; + +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosOperations; +import com.microsoft.azure.spring.data.cosmosdb.core.query.DocumentQuery; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ResultProcessor; + +/** + * Abstract class for cosmos query. + */ +public abstract class AbstractCosmosQuery implements RepositoryQuery { + + private final CosmosQueryMethod method; + private final CosmosOperations operations; + + /** + * Initialization + * + * @param method CosmosQueryMethod + * @param operations CosmosOperations + */ + public AbstractCosmosQuery(CosmosQueryMethod method, CosmosOperations operations) { + this.method = method; + this.operations = operations; + } + + /** + * Executes the {@link AbstractCosmosQuery} with the given parameters. + * + * @param parameters must not be {@literal null}. + * @return execution result. Can be {@literal null}. + */ + public Object execute(Object[] parameters) { + final CosmosParameterAccessor accessor = new CosmosParameterParameterAccessor(method, parameters); + final DocumentQuery query = createQuery(accessor); + + final ResultProcessor processor = method.getResultProcessor().withDynamicProjection(accessor); + final String container = ((CosmosEntityMetadata) method.getEntityInformation()).getContainerName(); + + final CosmosQueryExecution execution = getExecution(accessor); + return execution.execute(query, processor.getReturnedType().getDomainType(), container); + } + + + private CosmosQueryExecution getExecution(CosmosParameterAccessor accessor) { + if (isDeleteQuery()) { + return new CosmosQueryExecution.DeleteExecution(operations); + } else if (method.isPageQuery()) { + return new CosmosQueryExecution.PagedExecution(operations, accessor.getPageable()); + } else if (isExistsQuery()) { + return new CosmosQueryExecution.ExistsExecution(operations); + } else { + return new CosmosQueryExecution.MultiEntityExecution(operations); + } + } + + /** + * Get method of query + * + * @return CosmosQueryMethod + */ + public CosmosQueryMethod getQueryMethod() { + return method; + } + + protected abstract DocumentQuery createQuery(CosmosParameterAccessor accessor); + + protected abstract boolean isDeleteQuery(); + + protected abstract boolean isExistsQuery(); + +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/AbstractReactiveCosmosQuery.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/AbstractReactiveCosmosQuery.java new file mode 100644 index 000000000000..8b0d92e6f18a --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/AbstractReactiveCosmosQuery.java @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.query; + +import com.microsoft.azure.spring.data.cosmosdb.core.ReactiveCosmosOperations; +import com.microsoft.azure.spring.data.cosmosdb.core.query.DocumentQuery; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ResultProcessor; + +/** + * Abstract class for reactive cosmos query. + */ +public abstract class AbstractReactiveCosmosQuery implements RepositoryQuery { + + private final ReactiveCosmosQueryMethod method; + private final ReactiveCosmosOperations operations; + + /** + * Initialization + * + * @param method ReactiveCosmosQueryMethod + * @param operations ReactiveCosmosOperations + */ + public AbstractReactiveCosmosQuery(ReactiveCosmosQueryMethod method, + ReactiveCosmosOperations operations) { + this.method = method; + this.operations = operations; + } + + /** + * Executes the {@link AbstractReactiveCosmosQuery} with the given parameters. + * + * @param parameters must not be {@literal null}. + * @return execution result. Can be {@literal null}. + */ + public Object execute(Object[] parameters) { + final ReactiveCosmosParameterAccessor accessor = + new ReactiveCosmosParameterParameterAccessor(method, parameters); + final DocumentQuery query = createQuery(accessor); + + final ResultProcessor processor = + method.getResultProcessor().withDynamicProjection(accessor); + final String containerName = + ((ReactiveCosmosEntityMetadata) method.getEntityInformation()).getContainerName(); + + final ReactiveCosmosQueryExecution execution = getExecution(accessor); + return execution.execute(query, processor.getReturnedType().getDomainType(), containerName); + } + + + private ReactiveCosmosQueryExecution getExecution(ReactiveCosmosParameterAccessor accessor) { + if (isDeleteQuery()) { + return new ReactiveCosmosQueryExecution.DeleteExecution(operations); + } else if (method.isPageQuery()) { + throw new IllegalArgumentException("Paged Query is not supported by reactive cosmos " + + "db"); + } else if (isExistsQuery()) { + return new ReactiveCosmosQueryExecution.ExistsExecution(operations); + } else { + return new ReactiveCosmosQueryExecution.MultiEntityExecution(operations); + } + } + + /** + * Get method of query + * + * @return ReactiveCosmosQueryMethod + */ + public ReactiveCosmosQueryMethod getQueryMethod() { + return method; + } + + protected abstract DocumentQuery createQuery(ReactiveCosmosParameterAccessor accessor); + + protected abstract boolean isDeleteQuery(); + + protected abstract boolean isExistsQuery(); + +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/CosmosEntityMetadata.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/CosmosEntityMetadata.java new file mode 100644 index 000000000000..af36f066e50f --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/CosmosEntityMetadata.java @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.query; + +import org.springframework.data.repository.core.EntityMetadata; + +/** + * Metadata class to describe cosmos entity includes domain type and container information + */ +public interface CosmosEntityMetadata extends EntityMetadata { + + /** + * Get collection name from the given entity + * @return String + * @deprecated use {@link #getContainerName()} instead + */ + @Deprecated + String getCollectionName(); + + /** + * Get container name from the given entity + * @return String + */ + String getContainerName(); +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/CosmosParameter.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/CosmosParameter.java new file mode 100644 index 000000000000..d667df89a993 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/CosmosParameter.java @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.query; + +import org.springframework.core.MethodParameter; +import org.springframework.data.repository.query.Parameter; + +/** + * A single cosmos parameter of a query method. + */ +public class CosmosParameter extends Parameter { + + /** + * Creates a new {@link CosmosParameter} for the given {@link MethodParameter}. + * + * @param parameter must not be {@literal null}. + */ + public CosmosParameter(MethodParameter parameter) { + super(parameter); + } + + @Override + public boolean isSpecialParameter() { + return super.isSpecialParameter(); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/CosmosParameterAccessor.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/CosmosParameterAccessor.java new file mode 100644 index 000000000000..0cb40ad1db77 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/CosmosParameterAccessor.java @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.query; + +import org.springframework.data.repository.query.ParameterAccessor; + +/** + * Interface to access method parameters. Allows dedicated access to parameters of special types and expose api to read + * values. + */ +public interface CosmosParameterAccessor extends ParameterAccessor { + + /** + * Get values of method parameters + * @return Object[] + */ + Object[] getValues(); +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/CosmosParameterParameterAccessor.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/CosmosParameterParameterAccessor.java new file mode 100644 index 000000000000..2ce2d081ec3e --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/CosmosParameterParameterAccessor.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.query; + +import org.springframework.data.repository.query.ParametersParameterAccessor; + +import java.util.Arrays; +import java.util.List; + +/** + * {@link ParametersParameterAccessor} implementation and store all special parameters in a List. + */ +public class CosmosParameterParameterAccessor extends ParametersParameterAccessor + implements CosmosParameterAccessor { + + private final List values; + + /** + * Creates a new {@link CosmosParameterParameterAccessor}. + * + * @param method must not be {@literal null}. + * @param values must not be {@literal null}. + */ + public CosmosParameterParameterAccessor(CosmosQueryMethod method, Object[] values) { + super(method.getParameters(), values); + + this.values = Arrays.asList(values); + } + + @Override + public Object[] getValues() { + return values.toArray(); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/CosmosParameters.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/CosmosParameters.java new file mode 100644 index 000000000000..55386969f85d --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/CosmosParameters.java @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.query; + +import org.springframework.core.MethodParameter; +import org.springframework.data.repository.query.Parameters; + +import java.lang.reflect.Method; +import java.util.List; + +/** + * Method parameters that have to be bound to query parameters or applied to the query independently. + */ +public class CosmosParameters extends Parameters { + + /** + * Creates a new instance of {@link CosmosParameters}. + * + * @param method must not be {@literal null}. + */ + public CosmosParameters(Method method) { + super(method); + } + + private CosmosParameters(List parameters) { + super(parameters); + } + + @Override + protected CosmosParameters createFrom(List parameters) { + return new CosmosParameters(parameters); + } + + @Override + protected CosmosParameter createParameter(MethodParameter parameter) { + return new CosmosParameter(parameter); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/CosmosQueryCreator.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/CosmosQueryCreator.java new file mode 100644 index 000000000000..828917f1b2d1 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/CosmosQueryCreator.java @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.query; + +import com.microsoft.azure.spring.data.cosmosdb.Constants; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.CosmosPersistentProperty; +import com.microsoft.azure.spring.data.cosmosdb.core.query.Criteria; +import com.microsoft.azure.spring.data.cosmosdb.core.query.CriteriaType; +import com.microsoft.azure.spring.data.cosmosdb.core.query.DocumentQuery; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.springframework.data.domain.Sort; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.repository.query.parser.AbstractQueryCreator; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +/** + * TODO: String based query, based on how cosmosdb provides. + * StringCosmosQuery class, + * How to bind values to the query. if CosmosDb already has binding capability, if not we would have to do it here in + * some creative way.query creator are associated with part tree queries, + */ +public class CosmosQueryCreator extends AbstractQueryCreator { + + private final MappingContext mappingContext; + + /** + * Creates a new {@link CosmosQueryCreator}. {@link CosmosParameterAccessor} is used to hand actual + * parameter values into the callback methods as well as to apply dynamic sorting via a {@link Sort} parameter. + * + * @param tree must not be {@literal null}. + * @param accessor must not be {@literal null}. + * @param mappingContext must not be {@literal null}. + */ + public CosmosQueryCreator(PartTree tree, CosmosParameterAccessor accessor, + MappingContext mappingContext) { + super(tree, accessor); + + this.mappingContext = mappingContext; + } + + private String getSubject(@NonNull Part part) { + String subject = mappingContext.getPersistentPropertyPath(part.getProperty()).toDotPath(); + final Class domainType = part.getProperty().getOwningType().getType(); + + @SuppressWarnings("unchecked") final CosmosEntityInformation information = + new CosmosEntityInformation<>(domainType); + + if (information.getIdField().getName().equals(subject)) { + subject = Constants.ID_PROPERTY_NAME; + } + + return subject; + } + + @Override // Note (panli): side effect here, this method will change the iterator status of parameters. + protected Criteria create(Part part, Iterator parameters) { + final Part.Type type = part.getType(); + final String subject = getSubject(part); + final List values = new ArrayList<>(); + + if (CriteriaType.isPartTypeUnSupported(type)) { + throw new UnsupportedOperationException("Unsupported keyword: " + + type); + } + + for (int i = 0; i < part.getNumberOfArguments(); i++) { + Assert.isTrue(parameters.hasNext(), "should not reach the end of iterator"); + values.add(parameters.next()); + } + + return Criteria.getInstance(CriteriaType.toCriteriaType(type), subject, values); + } + + @Override + protected Criteria and(@NonNull Part part, @NonNull Criteria base, @NonNull Iterator parameters) { + final Criteria right = this.create(part, parameters); + + return Criteria.getInstance(CriteriaType.AND, base, right); + } + + @Override + protected Criteria or(@NonNull Criteria base, @NonNull Criteria criteria) { + return Criteria.getInstance(CriteriaType.OR, base, criteria); + } + + @Override + protected DocumentQuery complete(@NonNull Criteria criteria, @NonNull Sort sort) { + return new DocumentQuery(criteria).with(sort); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/CosmosQueryExecution.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/CosmosQueryExecution.java new file mode 100644 index 000000000000..6d3ae6f3ed6f --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/CosmosQueryExecution.java @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.query; + +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosOperations; +import com.microsoft.azure.spring.data.cosmosdb.core.query.CosmosPageRequest; +import com.microsoft.azure.spring.data.cosmosdb.core.query.DocumentQuery; +import org.springframework.data.domain.Pageable; + +/** + * Interface to execute cosmos query operations + */ +public interface CosmosQueryExecution { + + /** + * Declare an execute function for different operations to call + * + * @param query document query operation + * @param type domain type + * @param container container to conduct query + * @return Object according to execution result + */ + Object execute(DocumentQuery query, Class type, String container); + + /** + * Container operation implementation to execute a container name query + */ + final class ContainerExecution implements CosmosQueryExecution { + + private final CosmosOperations operations; + + public ContainerExecution(CosmosOperations operations) { + this.operations = operations; + } + + @Override + public Object execute(DocumentQuery query, Class type, String container) { + return operations.getContainerName(type); + } + } + + /** + * Find operation implementation to execute a find query + */ + final class MultiEntityExecution implements CosmosQueryExecution { + + private final CosmosOperations operations; + + public MultiEntityExecution(CosmosOperations operations) { + this.operations = operations; + } + + @Override + public Object execute(DocumentQuery query, Class type, String container) { + return operations.find(query, type, container); + } + } + + /** + * exist operation implementation to execute a exists query + */ + final class ExistsExecution implements CosmosQueryExecution { + + private final CosmosOperations operations; + + public ExistsExecution(CosmosOperations operations) { + this.operations = operations; + } + + @Override + public Object execute(DocumentQuery query, Class type, String container) { + return operations.exists(query, type, container); + } + } + + /** + * delete operation implementation to execute a delete query + */ + final class DeleteExecution implements CosmosQueryExecution { + + private final CosmosOperations operations; + + public DeleteExecution(CosmosOperations operations) { + this.operations = operations; + } + + @Override + public Object execute(DocumentQuery query, Class type, String container) { + return operations.delete(query, type, container); + } + } + + /** + * paginationQuery operation implementation to execute a paginationQuery query + */ + final class PagedExecution implements CosmosQueryExecution { + private final CosmosOperations operations; + private final Pageable pageable; + + public PagedExecution(CosmosOperations operations, Pageable pageable) { + this.operations = operations; + this.pageable = pageable; + } + + @Override + public Object execute(DocumentQuery query, Class type, String container) { + if (pageable.getPageNumber() != 0 + && !(pageable instanceof CosmosPageRequest)) { + throw new IllegalStateException("Not the first page but Pageable is not a valid " + + "CosmosPageRequest, requestContinuation is required for non first page request"); + } + + query.with(pageable); + + return operations.paginationQuery(query, type, container); + } + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/CosmosQueryMethod.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/CosmosQueryMethod.java new file mode 100644 index 000000000000..033b38282c9e --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/CosmosQueryMethod.java @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.query; + +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.repository.core.EntityMetadata; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.query.QueryMethod; + +import java.lang.reflect.Method; + +/** + * Inherit QueryMethod class to generate a method that is designated to execute a finder query. + */ +public class CosmosQueryMethod extends QueryMethod { + + private CosmosEntityMetadata metadata; + + /** + * Creates a new {@link CosmosQueryMethod} from the given parameters. Looks up the correct query to use + * for following invocations of the method given. + * + * @param method must not be {@literal null}. + * @param metadata must not be {@literal null}. + * @param factory must not be {@literal null}. + */ + public CosmosQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory) { + super(method, metadata, factory); + } + + @Override + @SuppressWarnings("unchecked") + public EntityMetadata getEntityInformation() { + final Class domainType = (Class) getDomainClass(); + final CosmosEntityInformation entityInformation = + new CosmosEntityInformation(domainType); + + this.metadata = new SimpleCosmosEntityMetadata(domainType, entityInformation); + return this.metadata; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/PartTreeCosmosQuery.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/PartTreeCosmosQuery.java new file mode 100644 index 000000000000..6618f5528ef3 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/PartTreeCosmosQuery.java @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.query; + +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosOperations; +import com.microsoft.azure.spring.data.cosmosdb.core.query.DocumentQuery; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.CosmosPersistentProperty; +import org.apache.commons.lang3.NotImplementedException; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.data.repository.query.parser.PartTree; + +/** + * Cosmos query class with {@link PartTree} to parse a {@link String} into a tree or {@link PartTree.OrPart}s consisting + * of simple {@link Part} instances in turn. + */ +public class PartTreeCosmosQuery extends AbstractCosmosQuery { + + private final PartTree tree; + private final MappingContext mappingContext; + private final ResultProcessor processor; + + /** + * Initialization + * @param method CosmosQueryMethod + * @param operations CosmosOperations + */ + public PartTreeCosmosQuery(CosmosQueryMethod method, CosmosOperations operations) { + super(method, operations); + + this.processor = method.getResultProcessor(); + this.tree = new PartTree(method.getName(), processor.getReturnedType().getDomainType()); + this.mappingContext = operations.getConverter().getMappingContext(); + } + + @Override + protected DocumentQuery createQuery(CosmosParameterAccessor accessor) { + final CosmosQueryCreator creator = new CosmosQueryCreator(tree, accessor, mappingContext); + + final DocumentQuery query = creator.createQuery(); + + if (tree.isLimiting()) { + throw new NotImplementedException("Limiting is not supported."); + } + + return query; + } + + @Override + protected boolean isDeleteQuery() { + return tree.isDelete(); + } + + @Override + protected boolean isExistsQuery() { + return tree.isExistsProjection(); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/PartTreeReactiveCosmosQuery.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/PartTreeReactiveCosmosQuery.java new file mode 100644 index 000000000000..7ee24913e199 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/PartTreeReactiveCosmosQuery.java @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.query; + +import com.microsoft.azure.spring.data.cosmosdb.core.ReactiveCosmosOperations; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.CosmosPersistentProperty; +import com.microsoft.azure.spring.data.cosmosdb.core.query.DocumentQuery; +import org.apache.commons.lang3.NotImplementedException; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.data.repository.query.parser.PartTree; + +/** + * Reactive cosmos query class with {@link PartTree} to parse a {@link String} into a tree or {@link PartTree.OrPart}s + * consisting of simple {@link Part} instances in turn. + */ +public class PartTreeReactiveCosmosQuery extends AbstractReactiveCosmosQuery { + + private final PartTree tree; + private final MappingContext mappingContext; + private final ResultProcessor processor; + + /** + * Initialization + * @param method ReactiveCosmosQueryMethod + * @param operations ReactiveCosmosOperations + */ + public PartTreeReactiveCosmosQuery(ReactiveCosmosQueryMethod method, ReactiveCosmosOperations operations) { + super(method, operations); + + this.processor = method.getResultProcessor(); + this.tree = new PartTree(method.getName(), processor.getReturnedType().getDomainType()); + this.mappingContext = operations.getConverter().getMappingContext(); + } + + @Override + protected DocumentQuery createQuery(ReactiveCosmosParameterAccessor accessor) { + final ReactiveCosmosQueryCreator creator = new ReactiveCosmosQueryCreator(tree, accessor, mappingContext); + + final DocumentQuery query = creator.createQuery(); + + if (tree.isLimiting()) { + throw new NotImplementedException("Limiting is not supported."); + } + + return query; + } + + @Override + protected boolean isDeleteQuery() { + return tree.isDelete(); + } + + @Override + protected boolean isExistsQuery() { + return tree.isExistsProjection(); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/ReactiveCosmosEntityMetadata.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/ReactiveCosmosEntityMetadata.java new file mode 100644 index 000000000000..62c6b844e12a --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/ReactiveCosmosEntityMetadata.java @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.query; + +import org.springframework.data.repository.core.EntityMetadata; + +/** + * Metadata class to describe reactive cosmos entity includes domain type and container information + */ +public interface ReactiveCosmosEntityMetadata extends EntityMetadata { + + /** + * Get collection name from the given entity + * @return String + * @deprecated use {@link #getContainerName()} instead + */ + @Deprecated + String getCollectionName(); + + /** + * Get container name from the given entity + * @return String + */ + String getContainerName(); +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/ReactiveCosmosParameterAccessor.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/ReactiveCosmosParameterAccessor.java new file mode 100644 index 000000000000..fc90b7d55de1 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/ReactiveCosmosParameterAccessor.java @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.query; + +import org.springframework.data.repository.query.ParameterAccessor; + +/** + * Interface to access method parameters. Allows dedicated access to parameters of special types and expose api to read + * values. + */ +public interface ReactiveCosmosParameterAccessor extends ParameterAccessor { + + /** + * Get values of method parameters + * @return Object[] + */ + Object[] getValues(); +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/ReactiveCosmosParameterParameterAccessor.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/ReactiveCosmosParameterParameterAccessor.java new file mode 100644 index 000000000000..9d31c6381d32 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/ReactiveCosmosParameterParameterAccessor.java @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.query; + +import org.springframework.data.repository.query.ParametersParameterAccessor; + +import java.util.Arrays; +import java.util.List; + +/** + * {@link ReactiveCosmosParameterParameterAccessor} implementation using a {@link ParametersParameterAccessor} instance + * to find special parameters. + */ +public class ReactiveCosmosParameterParameterAccessor extends ParametersParameterAccessor + implements ReactiveCosmosParameterAccessor { + + private final List values; + + /** + * Creates a new {@link ReactiveCosmosParameterParameterAccessor}. + * + * @param method must not be {@literal null}. + * @param values must not be {@literal null}. + */ + public ReactiveCosmosParameterParameterAccessor(ReactiveCosmosQueryMethod method, Object[] values) { + super(method.getParameters(), values); + + this.values = Arrays.asList(values); + } + + @Override + public Object[] getValues() { + return values.toArray(); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/ReactiveCosmosQueryCreator.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/ReactiveCosmosQueryCreator.java new file mode 100644 index 000000000000..be36e333bcf7 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/ReactiveCosmosQueryCreator.java @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.query; + +import com.microsoft.azure.spring.data.cosmosdb.Constants; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.CosmosPersistentProperty; +import com.microsoft.azure.spring.data.cosmosdb.core.query.Criteria; +import com.microsoft.azure.spring.data.cosmosdb.core.query.CriteriaType; +import com.microsoft.azure.spring.data.cosmosdb.core.query.DocumentQuery; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.springframework.data.domain.Sort; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.repository.query.parser.AbstractQueryCreator; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Class for reactive cosmos query creators that create criteria based queries from a {@link PartTree}. + */ +public class ReactiveCosmosQueryCreator extends AbstractQueryCreator { + + private final MappingContext mappingContext; + + /** + * Creates a new {@link ReactiveCosmosQueryCreator}. {@link ReactiveCosmosParameterAccessor} is used to hand actual + * parameter values into the callback methods as well as to apply dynamic sorting via a {@link Sort} parameter. + * + * @param tree must not be {@literal null}. + * @param accessor must not be {@literal null}. + * @param mappingContext must not be {@literal null}. + */ + public ReactiveCosmosQueryCreator(PartTree tree, ReactiveCosmosParameterAccessor accessor, + MappingContext mappingContext) { + super(tree, accessor); + + this.mappingContext = mappingContext; + } + + private String getSubject(@NonNull Part part) { + String subject = mappingContext.getPersistentPropertyPath(part.getProperty()).toDotPath(); + final Class domainType = part.getProperty().getOwningType().getType(); + + @SuppressWarnings("unchecked") final CosmosEntityInformation information = + new CosmosEntityInformation<>(domainType); + + if (information.getIdField().getName().equals(subject)) { + subject = Constants.ID_PROPERTY_NAME; + } + + return subject; + } + + @Override // Note (panli): side effect here, this method will change the iterator status of parameters. + protected Criteria create(Part part, Iterator parameters) { + final Part.Type type = part.getType(); + final String subject = getSubject(part); + final List values = new ArrayList<>(); + + if (CriteriaType.isPartTypeUnSupported(type)) { + throw new UnsupportedOperationException("Unsupported keyword: " + + type); + } + + for (int i = 0; i < part.getNumberOfArguments(); i++) { + Assert.isTrue(parameters.hasNext(), "should not reach the end of iterator"); + values.add(parameters.next()); + } + + return Criteria.getInstance(CriteriaType.toCriteriaType(type), subject, values); + } + + @Override + protected Criteria and(@NonNull Part part, @NonNull Criteria base, @NonNull Iterator parameters) { + final Criteria right = this.create(part, parameters); + + return Criteria.getInstance(CriteriaType.AND, base, right); + } + + @Override + protected Criteria or(@NonNull Criteria base, @NonNull Criteria criteria) { + return Criteria.getInstance(CriteriaType.OR, base, criteria); + } + + @Override + protected DocumentQuery complete(@NonNull Criteria criteria, @NonNull Sort sort) { + return new DocumentQuery(criteria).with(sort); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/ReactiveCosmosQueryExecution.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/ReactiveCosmosQueryExecution.java new file mode 100644 index 000000000000..bf615fae2dbe --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/ReactiveCosmosQueryExecution.java @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.query; + +import com.microsoft.azure.spring.data.cosmosdb.core.ReactiveCosmosOperations; +import com.microsoft.azure.spring.data.cosmosdb.core.query.DocumentQuery; + +/** + * Interface to execute reactive cosmos query operations + */ +public interface ReactiveCosmosQueryExecution { + + /** + * Declare an execute function for different operations to call + * + * @param query document query operation + * @param type domain type + * @param container container to conduct query + * @return Object according to execution result + */ + Object execute(DocumentQuery query, Class type, String container); + + /** + * Container operation implementation to execute a container name query + */ + final class ContainerExecution implements ReactiveCosmosQueryExecution { + + private final ReactiveCosmosOperations operations; + + public ContainerExecution(ReactiveCosmosOperations operations) { + this.operations = operations; + } + + @Override + public Object execute(DocumentQuery query, Class type, String container) { + return operations.getContainerName(type); + } + } + + /** + * Find operation implementation to execute a find query + */ + final class MultiEntityExecution implements ReactiveCosmosQueryExecution { + + private final ReactiveCosmosOperations operations; + + public MultiEntityExecution(ReactiveCosmosOperations operations) { + this.operations = operations; + } + + @Override + public Object execute(DocumentQuery query, Class type, String container) { + return operations.find(query, type, container); + } + } + + /** + * Exist operation implementation to execute a exist query + */ + final class ExistsExecution implements ReactiveCosmosQueryExecution { + + private final ReactiveCosmosOperations operations; + + public ExistsExecution(ReactiveCosmosOperations operations) { + this.operations = operations; + } + + @Override + public Object execute(DocumentQuery query, Class type, String container) { + return operations.exists(query, type, container); + } + } + + /** + * Delete operation implementation to execute a delete query + */ + final class DeleteExecution implements ReactiveCosmosQueryExecution { + + private final ReactiveCosmosOperations operations; + + public DeleteExecution(ReactiveCosmosOperations operations) { + this.operations = operations; + } + + @Override + public Object execute(DocumentQuery query, Class type, String container) { + return operations.delete(query, type, container); + } + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/ReactiveCosmosQueryMethod.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/ReactiveCosmosQueryMethod.java new file mode 100644 index 000000000000..288fa02d7564 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/ReactiveCosmosQueryMethod.java @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.query; + +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.repository.core.EntityMetadata; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.query.QueryMethod; + +import java.lang.reflect.Method; + +/** + * Inherit from QueryMethod class to execute a finder query. + */ +public class ReactiveCosmosQueryMethod extends QueryMethod { + + private ReactiveCosmosEntityMetadata metadata; + + /** + * Creates a new {@link QueryMethod} from the given parameters. Looks up the correct query to use for following + * invocations of the method given. + * + * @param method must not be {@literal null}. + * @param metadata must not be {@literal null}. + * @param factory must not be {@literal null}. + */ + public ReactiveCosmosQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory) { + super(method, metadata, factory); + } + + @Override + @SuppressWarnings("unchecked") + public EntityMetadata getEntityInformation() { + final Class domainType = (Class) getDomainClass(); + final CosmosEntityInformation entityInformation = + new CosmosEntityInformation(domainType); + + this.metadata = new SimpleReactiveCosmosEntityMetadata(domainType, entityInformation); + return this.metadata; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/SimpleCosmosEntityMetadata.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/SimpleCosmosEntityMetadata.java new file mode 100644 index 000000000000..c027a1bc7a50 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/SimpleCosmosEntityMetadata.java @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.query; + +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.springframework.util.Assert; + +/** + * Metadata class to describe simple cosmos entity includes domain type and cosmos entity information + */ +public class SimpleCosmosEntityMetadata implements CosmosEntityMetadata { + + private final Class type; + private final CosmosEntityInformation entityInformation; + + /** + * Initialization + * + * @param type the actual domain class type + * @param entityInformation cosmos entity + */ + public SimpleCosmosEntityMetadata(Class type, CosmosEntityInformation entityInformation) { + Assert.notNull(type, "type must not be null!"); + Assert.notNull(entityInformation, "entityInformation must not be null!"); + + this.type = type; + this.entityInformation = entityInformation; + } + + /** + * Return the actual domain class type + * + * @return type + */ + public Class getJavaType() { + return type; + } + + /** + * Get collection name of cosmos + * + * @return container name + */ + public String getCollectionName() { + return entityInformation.getContainerName(); + } + + /** + * Get container name of cosmos + * + * @return container name + */ + public String getContainerName() { + return entityInformation.getContainerName(); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/SimpleReactiveCosmosEntityMetadata.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/SimpleReactiveCosmosEntityMetadata.java new file mode 100644 index 000000000000..36294fcdc6ec --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/SimpleReactiveCosmosEntityMetadata.java @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.query; + +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.springframework.util.Assert; + +/** + * Metadata class to describe simple reactive cosmos entity includes domain type and cosmos entity information + */ +public class SimpleReactiveCosmosEntityMetadata implements ReactiveCosmosEntityMetadata { + + private final Class type; + private final CosmosEntityInformation entityInformation; + + /** + * Initialization + * + * @param type the actual domain class type + * @param entityInformation cosmos entity + */ + public SimpleReactiveCosmosEntityMetadata(Class type, CosmosEntityInformation entityInformation) { + Assert.notNull(type, "type must not be null!"); + Assert.notNull(entityInformation, "entityInformation must not be null!"); + + this.type = type; + this.entityInformation = entityInformation; + } + + /** + * Return the actual domain class type + * + * @return type + */ + public Class getJavaType() { + return type; + } + + /** + * Get collection name of cosmos + * + * @return container name + */ + public String getCollectionName() { + return entityInformation.getContainerName(); + } + + @Override + public String getContainerName() { + return entityInformation.getContainerName(); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/package-info.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/package-info.java new file mode 100644 index 000000000000..18fb93d32872 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/query/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * This package contains the process cosmos queries + */ +package com.microsoft.azure.spring.data.cosmosdb.repository.query; diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/CosmosEntityInformation.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/CosmosEntityInformation.java new file mode 100644 index 000000000000..14dd833c76bc --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/CosmosEntityInformation.java @@ -0,0 +1,367 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.repository.support; + +import com.azure.data.cosmos.ExcludedPath; +import com.azure.data.cosmos.IncludedPath; +import com.azure.data.cosmos.IndexingMode; +import com.azure.data.cosmos.IndexingPolicy; +import com.microsoft.azure.spring.data.cosmosdb.Constants; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.DocumentIndexingPolicy; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.PartitionKey; +import org.apache.commons.lang3.reflect.FieldUtils; + +import org.json.JSONObject; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Version; +import org.springframework.data.repository.core.support.AbstractEntityInformation; +import org.springframework.lang.NonNull; +import org.springframework.util.ReflectionUtils; + +import static com.microsoft.azure.spring.data.cosmosdb.common.ExpressionResolver.resolveExpression; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +/** + * Class to describe cosmosdb entity + */ +public class CosmosEntityInformation extends AbstractEntityInformation { + + private static final String ETAG = "_etag"; + private final Field id; + private final Field partitionKeyField; + private final String containerName; + private final Integer requestUnit; + private final Integer timeToLive; + private final IndexingPolicy indexingPolicy; + private final boolean isVersioned; + private boolean autoCreateContainer; + + /** + * Initialization + * + * @param domainType to specify id field + */ + public CosmosEntityInformation(Class domainType) { + super(domainType); + + this.id = getIdField(domainType); + ReflectionUtils.makeAccessible(this.id); + + this.containerName = getContainerName(domainType); + this.partitionKeyField = getPartitionKeyField(domainType); + if (this.partitionKeyField != null) { + ReflectionUtils.makeAccessible(this.partitionKeyField); + } + + this.requestUnit = getRequestUnit(domainType); + this.timeToLive = getTimeToLive(domainType); + this.indexingPolicy = getIndexingPolicy(domainType); + this.isVersioned = getIsVersioned(domainType); + this.autoCreateContainer = getIsAutoCreateContainer(domainType); + } + + /** + * Get the field represented by the supplied id field on the + * specified entity. + * + * @param entity the target object from which to get the field + * @return the id's current value + */ + @SuppressWarnings("unchecked") + public ID getId(T entity) { + return (ID) ReflectionUtils.getField(id, entity); + } + + /** + * Get id field + * + * @return id + */ + public Field getIdField() { + return this.id; + } + + /** + * Get id type + * + * @return class of id type + */ + @SuppressWarnings("unchecked") + public Class getIdType() { + return (Class) id.getType(); + } + + /** + * Get collection name + * + * @return collection name + * @deprecated Use {@link #getContainerName()} instead + */ + @Deprecated + public String getCollectionName() { + return this.containerName; + } + + /** + * Get container name + * + * @return container name + */ + public String getContainerName() { + return this.containerName; + } + + /** + * Get request unit value + * + * @return request unit + */ + public Integer getRequestUnit() { + return this.requestUnit; + } + + /** + * Get timeToLive value + * + * @return timeToLive + */ + public Integer getTimeToLive() { + return this.timeToLive; + } + + /** + * Get indexing policy + * + * @return IndexingPolicy + */ + @NonNull + public IndexingPolicy getIndexingPolicy() { + return this.indexingPolicy; + } + + /** + * Check if is versioned + * + * @return boolean + */ + public boolean isVersioned() { + return isVersioned; + } + + /** + * Get the field name represented by the supplied partitionKeyField object + * + * @return partition key field name + */ + public String getPartitionKeyFieldName() { + if (partitionKeyField == null) { + return null; + } else { + final PartitionKey partitionKey = partitionKeyField.getAnnotation(PartitionKey.class); + return partitionKey.value().equals("") ? partitionKeyField.getName() : partitionKey.value(); + } + } + + /** + * Get the field value represented by the supplied partitionKeyField object on the + * specified entity object. + * + * @param entity the target object from which to get the field + * @return partition key field + */ + public String getPartitionKeyFieldValue(T entity) { + return partitionKeyField == null ? null : (String) ReflectionUtils.getField(partitionKeyField, entity); + } + + /** + * Check if auto creating collection is allowed + * + * @return boolean + * @deprecated Use {@link #isAutoCreateContainer()} instead. + */ + @Deprecated + public boolean isAutoCreateCollection() { + return autoCreateContainer; + } + + /** + * Check if auto creating container is allowed + * + * @return boolean + */ + public boolean isAutoCreateContainer() { + return autoCreateContainer; + } + + private IndexingPolicy getIndexingPolicy(Class domainType) { + final IndexingPolicy policy = new IndexingPolicy(); + + policy.automatic(this.getIndexingPolicyAutomatic(domainType)); + policy.indexingMode(this.getIndexingPolicyMode(domainType)); + policy.setIncludedPaths(this.getIndexingPolicyIncludePaths(domainType)); + policy.excludedPaths(this.getIndexingPolicyExcludePaths(domainType)); + + return policy; + } + + private Field getIdField(Class domainType) { + final Field idField; + final List fields = FieldUtils.getFieldsListWithAnnotation(domainType, Id.class); + + if (fields.isEmpty()) { + idField = ReflectionUtils.findField(getJavaType(), Constants.ID_PROPERTY_NAME); + } else if (fields.size() == 1) { + idField = fields.get(0); + } else { + throw new IllegalArgumentException("only one field with @Id annotation!"); + } + + if (idField == null) { + throw new IllegalArgumentException("domain should contain @Id field or field named id"); + } else if (idField.getType() != String.class + && idField.getType() != Integer.class + && idField.getType() != int.class) { + throw new IllegalArgumentException("type of id field must be String or Integer"); + } + + return idField; + } + + private String getContainerName(Class domainType) { + String customContainerName = domainType.getSimpleName(); + + final Document annotation = domainType.getAnnotation(Document.class); + + if (annotation != null + && annotation.collection() != null + && !annotation.collection().isEmpty()) { + customContainerName = resolveExpression(annotation.collection()); + } + + return customContainerName; + } + + private Field getPartitionKeyField(Class domainType) { + Field partitionKey = null; + + final List fields = FieldUtils.getFieldsListWithAnnotation(domainType, PartitionKey.class); + + if (fields.size() == 1) { + partitionKey = fields.get(0); + } else if (fields.size() > 1) { + throw new IllegalArgumentException("Azure Cosmos DB supports only one partition key, " + + "only one field with @PartitionKey annotation!"); + } + + if (partitionKey != null + && partitionKey.getType() != String.class) { + throw new IllegalArgumentException("type of PartitionKey field must be String"); + } + return partitionKey; + } + + private Integer getRequestUnit(Class domainType) { + Integer ru = Integer.parseInt(Constants.DEFAULT_REQUEST_UNIT); + final Document annotation = domainType.getAnnotation(Document.class); + + if (annotation != null + && annotation.ru() != null + && !annotation.ru().isEmpty()) { + ru = Integer.parseInt(annotation.ru()); + } + return ru; + } + + private Integer getTimeToLive(Class domainType) { + Integer ttl = Constants.DEFAULT_TIME_TO_LIVE; + final Document annotation = domainType.getAnnotation(Document.class); + + if (annotation != null) { + ttl = annotation.timeToLive(); + } + + return ttl; + } + + + private Boolean getIndexingPolicyAutomatic(Class domainType) { + Boolean isAutomatic = Boolean.valueOf(Constants.DEFAULT_INDEXINGPOLICY_AUTOMATIC); + final DocumentIndexingPolicy annotation = domainType.getAnnotation(DocumentIndexingPolicy.class); + + if (annotation != null) { + isAutomatic = Boolean.valueOf(annotation.automatic()); + } + + return isAutomatic; + } + + private IndexingMode getIndexingPolicyMode(Class domainType) { + IndexingMode mode = Constants.DEFAULT_INDEXINGPOLICY_MODE; + final DocumentIndexingPolicy annotation = domainType.getAnnotation(DocumentIndexingPolicy.class); + + if (annotation != null) { + mode = annotation.mode(); + } + + return mode; + } + + private List getIndexingPolicyIncludePaths(Class domainType) { + final List pathArrayList = new ArrayList<>(); + final DocumentIndexingPolicy annotation = domainType.getAnnotation(DocumentIndexingPolicy.class); + + if (annotation == null || annotation.includePaths() == null || annotation.includePaths().length == 0) { + return null; // Align the default value of IndexingPolicy + } + + final String[] rawPaths = annotation.includePaths(); + + for (final String path : rawPaths) { + pathArrayList.add(new IncludedPath(path)); + } + + return pathArrayList; + } + + private List getIndexingPolicyExcludePaths(Class domainType) { + final List pathArrayList = new ArrayList<>(); + final DocumentIndexingPolicy annotation = domainType.getAnnotation(DocumentIndexingPolicy.class); + + if (annotation == null || annotation.excludePaths().length == 0) { + return null; // Align the default value of IndexingPolicy + } + + final String[] rawPaths = annotation.excludePaths(); + for (final String path : rawPaths) { + final JSONObject obj = new JSONObject(path); + pathArrayList.add(new ExcludedPath().path(obj.get("path").toString())); + } + + return pathArrayList; + } + + private boolean getIsVersioned(Class domainType) { + final Field findField = ReflectionUtils.findField(domainType, ETAG); + return findField != null + && findField.getType() == String.class + && findField.isAnnotationPresent(Version.class); + } + + private boolean getIsAutoCreateContainer(Class domainType) { + final Document annotation = domainType.getAnnotation(Document.class); + + boolean autoCreateContainer = Constants.DEFAULT_AUTO_CREATE_CONTAINER; + if (annotation != null) { + autoCreateContainer = annotation.autoCreateCollection(); + } + + return autoCreateContainer; + } + +} + diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/CosmosRepositoryFactory.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/CosmosRepositoryFactory.java new file mode 100644 index 000000000000..2ca611f65cd5 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/CosmosRepositoryFactory.java @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.repository.support; + +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosOperations; +import com.microsoft.azure.spring.data.cosmosdb.repository.query.CosmosQueryMethod; +import com.microsoft.azure.spring.data.cosmosdb.repository.query.PartTreeCosmosQuery; +import org.springframework.context.ApplicationContext; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.repository.core.EntityInformation; +import org.springframework.data.repository.core.NamedQueries; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.util.Assert; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Optional; + +/** + * Factory class for cosmos repository contains application context and operations information + */ +public class CosmosRepositoryFactory extends RepositoryFactorySupport { + + private final ApplicationContext applicationContext; + private final CosmosOperations cosmosOperations; + + /** + * Initialization + * + * @param cosmosOperations for cosmosdb operations + * @param applicationContext for the context + */ + public CosmosRepositoryFactory(CosmosOperations cosmosOperations, ApplicationContext applicationContext) { + this.cosmosOperations = cosmosOperations; + this.applicationContext = applicationContext; + } + + @Override + protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { + return SimpleCosmosRepository.class; + } + + @Override + protected Object getTargetRepository(RepositoryInformation information) { + final EntityInformation entityInformation = getEntityInformation(information.getDomainType()); + return getTargetRepositoryViaReflection(information, entityInformation, this.applicationContext); + } + + @Override + public EntityInformation getEntityInformation(Class domainType) { + return new CosmosEntityInformation<>(domainType); + } + + @Override + protected Optional getQueryLookupStrategy( + QueryLookupStrategy.Key key, QueryMethodEvaluationContextProvider evaluationContextProvider) { + return Optional.of(new CosmosDbQueryLookupStrategy(cosmosOperations, evaluationContextProvider)); + } + + private static class CosmosDbQueryLookupStrategy implements QueryLookupStrategy { + private final CosmosOperations dbOperations; + + CosmosDbQueryLookupStrategy( + CosmosOperations operations, QueryMethodEvaluationContextProvider provider) { + this.dbOperations = operations; + } + + @Override + public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, + ProjectionFactory factory, NamedQueries namedQueries) { + final CosmosQueryMethod queryMethod = new CosmosQueryMethod(method, metadata, factory); + + Assert.notNull(queryMethod, "queryMethod must not be null!"); + Assert.notNull(dbOperations, "dbOperations must not be null!"); + return new PartTreeCosmosQuery(queryMethod, dbOperations); + + } + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/CosmosRepositoryFactoryBean.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/CosmosRepositoryFactoryBean.java new file mode 100644 index 000000000000..717c9c499b04 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/CosmosRepositoryFactoryBean.java @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.support; + +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosOperations; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.CosmosMappingContext; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; + +import java.io.Serializable; + +/** + * Adapter for Springs {@link FactoryBean} interface to allow easy setup of cosmos repository factories via Spring + * configuration. + */ +public class CosmosRepositoryFactoryBean, S, ID extends Serializable> + extends RepositoryFactoryBeanSupport + implements ApplicationContextAware { + + private ApplicationContext applicationContext; + private CosmosOperations operations; + private boolean mappingContextConfigured = false; + + /** + * Creates a new {@link RepositoryFactoryBeanSupport} for the given repository interface. + * + * @param repositoryInterface must not be {@literal null}. + */ + public CosmosRepositoryFactoryBean(Class repositoryInterface) { + super(repositoryInterface); + } + + /** + * Set cosmos operation + * + * @param operations for cosmos operations + */ + @Autowired + public void setCosmosOperations(CosmosOperations operations) { + this.operations = operations; + } + + @Override + protected final RepositoryFactorySupport createRepositoryFactory() { + return getFactoryInstance(applicationContext); + } + + protected RepositoryFactorySupport getFactoryInstance(ApplicationContext applicationContext) { + return new CosmosRepositoryFactory(operations, applicationContext); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + @Override + protected void setMappingContext(MappingContext mappingContext) { + super.setMappingContext(mappingContext); + this.mappingContextConfigured = true; + } + + @Override + public void afterPropertiesSet() { + super.afterPropertiesSet(); + + if (!this.mappingContextConfigured) { + if (operations != null) { + setMappingContext(operations.getConverter().getMappingContext()); + } else { + setMappingContext(new CosmosMappingContext()); + } + } + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/ReactiveCosmosRepositoryFactory.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/ReactiveCosmosRepositoryFactory.java new file mode 100644 index 000000000000..7ba254bf0d80 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/ReactiveCosmosRepositoryFactory.java @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.support; + +import com.microsoft.azure.spring.data.cosmosdb.core.ReactiveCosmosOperations; +import com.microsoft.azure.spring.data.cosmosdb.repository.query.PartTreeReactiveCosmosQuery; +import com.microsoft.azure.spring.data.cosmosdb.repository.query.ReactiveCosmosQueryMethod; +import org.springframework.context.ApplicationContext; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.repository.core.EntityInformation; +import org.springframework.data.repository.core.NamedQueries; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport; +import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.util.Assert; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Optional; + +/** + * Factory class for reactive cosmos repository contains application context and operations information + */ +public class ReactiveCosmosRepositoryFactory extends ReactiveRepositoryFactorySupport { + + private final ApplicationContext applicationContext; + private final ReactiveCosmosOperations cosmosOperations; + + /** + * Initialization + * + * @param cosmosOperations for cosmosdb operations + * @param applicationContext for the context + */ + public ReactiveCosmosRepositoryFactory(ReactiveCosmosOperations cosmosOperations, + ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + this.cosmosOperations = cosmosOperations; + } + + @Override + public EntityInformation getEntityInformation(Class domainType) { + return new CosmosEntityInformation<>(domainType); + } + + @Override + protected Object getTargetRepository(RepositoryInformation information) { + final EntityInformation entityInformation = + getEntityInformation(information.getDomainType()); + return getTargetRepositoryViaReflection(information, entityInformation, + this.applicationContext); + } + + @Override + protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { + return SimpleReactiveCosmosRepository.class; + } + + @Override + protected Optional getQueryLookupStrategy( + QueryLookupStrategy.Key key, + QueryMethodEvaluationContextProvider evaluationContextProvider) { + return Optional.of(new ReactiveCosmosQueryLookupStrategy(cosmosOperations, + evaluationContextProvider)); + } + + private static class ReactiveCosmosQueryLookupStrategy implements QueryLookupStrategy { + private final ReactiveCosmosOperations cosmosOperations; + + ReactiveCosmosQueryLookupStrategy( + ReactiveCosmosOperations operations, QueryMethodEvaluationContextProvider provider) { + this.cosmosOperations = operations; + } + + @Override + public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, + ProjectionFactory factory, NamedQueries namedQueries) { + final ReactiveCosmosQueryMethod queryMethod = new ReactiveCosmosQueryMethod(method, + metadata, factory); + + Assert.notNull(queryMethod, "queryMethod must not be null!"); + Assert.notNull(cosmosOperations, "dbOperations must not be null!"); + return new PartTreeReactiveCosmosQuery(queryMethod, cosmosOperations); + + } + } + +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/ReactiveCosmosRepositoryFactoryBean.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/ReactiveCosmosRepositoryFactoryBean.java new file mode 100644 index 000000000000..2e23b3f81acd --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/ReactiveCosmosRepositoryFactoryBean.java @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.support; + +import com.microsoft.azure.spring.data.cosmosdb.core.ReactiveCosmosOperations; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.CosmosMappingContext; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; + +import java.io.Serializable; + +/** + * Adapter for Springs {@link FactoryBean} interface to allow easy setup of reactive cosmos repository factories + * via Spring configuration. + */ +public class ReactiveCosmosRepositoryFactoryBean, S, + K extends Serializable> + extends RepositoryFactoryBeanSupport + implements ApplicationContextAware { + + private ApplicationContext applicationContext; + private ReactiveCosmosOperations cosmosOperations; + private boolean mappingContextConfigured = false; + + /** + * Creates a new {@link RepositoryFactoryBeanSupport} for the given repository interface. + * + * @param repositoryInterface must not be {@literal null}. + */ + public ReactiveCosmosRepositoryFactoryBean(Class repositoryInterface) { + super(repositoryInterface); + } + + /** + * Set reactive Cosmosdb operations + * + * @param operations contains cosmos operations + */ + @Autowired + public void setReactiveCosmosOperations(ReactiveCosmosOperations operations) { + this.cosmosOperations = operations; + } + + @Override + protected final RepositoryFactorySupport createRepositoryFactory() { + return getFactoryInstance(applicationContext); + } + + protected RepositoryFactorySupport getFactoryInstance(ApplicationContext applicationContext) { + return new ReactiveCosmosRepositoryFactory(cosmosOperations, applicationContext); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + @Override + protected void setMappingContext(MappingContext mappingContext) { + super.setMappingContext(mappingContext); + this.mappingContextConfigured = true; + } + + @Override + public void afterPropertiesSet() { + super.afterPropertiesSet(); + + if (!this.mappingContextConfigured) { + if (cosmosOperations != null) { + setMappingContext(cosmosOperations.getConverter().getMappingContext()); + } else { + setMappingContext(new CosmosMappingContext()); + } + } + } + +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/SimpleCosmosRepository.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/SimpleCosmosRepository.java new file mode 100644 index 000000000000..92689a1e4f8e --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/SimpleCosmosRepository.java @@ -0,0 +1,285 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.repository.support; + +import com.azure.data.cosmos.CosmosContainerProperties; +import com.azure.data.cosmos.PartitionKey; +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosOperations; +import com.microsoft.azure.spring.data.cosmosdb.core.query.Criteria; +import com.microsoft.azure.spring.data.cosmosdb.core.query.CriteriaType; +import com.microsoft.azure.spring.data.cosmosdb.core.query.DocumentQuery; +import com.microsoft.azure.spring.data.cosmosdb.repository.CosmosRepository; +import org.springframework.context.ApplicationContext; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.StreamSupport; + +/** + * Repository class for simple Cosmos operation + */ +public class SimpleCosmosRepository implements CosmosRepository { + + private final CosmosOperations operation; + private final CosmosEntityInformation information; + + /** + * Initialization + * + * @param metadata for cosmos entity information + * @param applicationContext to get bean of CosmosOperations class + */ + public SimpleCosmosRepository(CosmosEntityInformation metadata, + ApplicationContext applicationContext) { + this.operation = applicationContext.getBean(CosmosOperations.class); + this.information = metadata; + + if (this.information.isAutoCreateContainer()) { + createContainerIfNotExists(); + } + } + + /** + * Initialization + * + * @param metadata for cosmos entity information + * @param dbOperations for cosmosdb operation + */ + public SimpleCosmosRepository(CosmosEntityInformation metadata, + CosmosOperations dbOperations) { + this.operation = dbOperations; + this.information = metadata; + + if (this.information.isAutoCreateContainer()) { + createContainerIfNotExists(); + } + } + + private CosmosContainerProperties createContainerIfNotExists() { + return this.operation.createContainerIfNotExists(this.information); + } + + /** + * save entity without partition + * + * @param entity to be saved + * @param type of entity + * @return entity + */ + @Override + public S save(S entity) { + Assert.notNull(entity, "entity must not be null"); + + // save entity + if (information.isNew(entity)) { + return operation.insert(information.getContainerName(), + entity, + createKey(information.getPartitionKeyFieldValue(entity))); + } else { + return operation.upsertAndReturnEntity(information.getContainerName(), + entity, createKey(information.getPartitionKeyFieldValue(entity))); + } + } + + private PartitionKey createKey(String partitionKeyValue) { + if (StringUtils.isEmpty(partitionKeyValue)) { + return PartitionKey.None; + } + + return new PartitionKey(partitionKeyValue); + } + + /** + * batch save entities + * + * @param entities Batch entities + * @param type of entities + * @return return the saved entities + */ + @Override + public Iterable saveAll(Iterable entities) { + Assert.notNull(entities, "Iterable entities should not be null"); + + final List savedEntities = new ArrayList<>(); + entities.forEach(entity -> { + final S savedEntity = this.save(entity); + savedEntities.add(savedEntity); + }); + + return savedEntities; + } + + /** + * find all entities from one container without configuring partition key value + * + * @return return Iterable of the found entities List + */ + @Override + public Iterable findAll() { + return operation.findAll(information.getContainerName(), information.getJavaType()); + } + + /** + * find entities based on id list from one container without partitions + * + * @param ids id list used to find entities + * @return return a List of all found entities + */ + @Override + public List findAllById(Iterable ids) { + Assert.notNull(ids, "Iterable ids should not be null"); + + return operation.findByIds(ids, information.getJavaType(), information.getContainerName()); + } + + /** + * find one entity per id without partitions + * + * @param id an id used to find entity + * @return return the searching result + */ + @Override + public Optional findById(ID id) { + Assert.notNull(id, "id must not be null"); + + if (id instanceof String + && !StringUtils.hasText((String) id)) { + return Optional.empty(); + } + + return Optional.ofNullable(operation.findById(information.getContainerName(), id, information.getJavaType())); + } + + @Override + public Optional findById(ID id, PartitionKey partitionKey) { + Assert.notNull(id, "id must not be null"); + + if (id instanceof String + && !StringUtils.hasText((String) id)) { + return Optional.empty(); + } + + return Optional.ofNullable(operation.findById(id, information.getJavaType(), partitionKey)); + } + + /** + * return count of documents in one container without partitions + * + * @return count of documents in one container without partitions + */ + @Override + public long count() { + return operation.count(information.getContainerName()); + } + + /** + * delete one document per id without configuring partition key value + * + * @param id an id used to specify the deleted document + */ + @Override + public void deleteById(ID id) { + Assert.notNull(id, "id to be deleted should not be null"); + + operation.deleteById(information.getContainerName(), id, null); + } + + @Override + public void deleteById(ID id, PartitionKey partitionKey) { + Assert.notNull(id, "id to be deleted should not be null"); + Assert.notNull(partitionKey, "partitionKey to be deleted should not be null"); + + operation.deleteById(information.getContainerName(), id, partitionKey); + } + + /** + * delete one document per entity + * + * @param entity the entity used to specify a document + */ + @Override + public void delete(T entity) { + Assert.notNull(entity, "entity to be deleted should not be null"); + + final String partitionKeyValue = information.getPartitionKeyFieldValue(entity); + + operation.deleteById(information.getContainerName(), + information.getId(entity), + partitionKeyValue == null ? null : new PartitionKey(partitionKeyValue)); + } + + /** + * delete all the domains of a container + */ + @Override + public void deleteAll() { + operation.deleteAll(information.getContainerName(), information.getJavaType()); + } + + /** + * delete list of entities without partitions + * + * @param entities list of entities to be deleted + */ + @Override + public void deleteAll(Iterable entities) { + Assert.notNull(entities, "Iterable entities should not be null"); + + StreamSupport.stream(entities.spliterator(), true).forEach(this::delete); + } + + /** + * check if an entity exists per id without partition + * + * @param primaryKey an id to specify an entity + * @return if the entity exists + */ + @Override + public boolean existsById(ID primaryKey) { + Assert.notNull(primaryKey, "primaryKey should not be null"); + + return findById(primaryKey).isPresent(); + } + + /** + * Returns all entities sorted by the given options. + * + * @param sort the Sort option for queries. + * @return all entities sorted by the given options + */ + @Override + public Iterable findAll(@NonNull Sort sort) { + Assert.notNull(sort, "sort of findAll should not be null"); + final DocumentQuery query = new DocumentQuery(Criteria.getInstance(CriteriaType.ALL)).with(sort); + + return operation.find(query, information.getJavaType(), information.getContainerName()); + } + + /** + * FindQuerySpecGenerator + * Returns a Page of entities meeting the paging restriction provided in the Pageable object. + * + * @param pageable the Pageable object providing paging restriction + * @return a page of entities + */ + @Override + public Page findAll(Pageable pageable) { + Assert.notNull(pageable, "pageable should not be null"); + + return operation.findAll(pageable, information.getJavaType(), information.getContainerName()); + } + + @Override + public List findAll(PartitionKey partitionKey) { + return operation.findAll(partitionKey, information.getJavaType()); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/SimpleReactiveCosmosRepository.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/SimpleReactiveCosmosRepository.java new file mode 100644 index 000000000000..fa57ae3925cd --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/SimpleReactiveCosmosRepository.java @@ -0,0 +1,242 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.support; + +import com.azure.data.cosmos.CosmosContainerResponse; +import com.azure.data.cosmos.PartitionKey; +import com.microsoft.azure.spring.data.cosmosdb.core.ReactiveCosmosOperations; +import com.microsoft.azure.spring.data.cosmosdb.core.query.Criteria; +import com.microsoft.azure.spring.data.cosmosdb.core.query.CriteriaType; +import com.microsoft.azure.spring.data.cosmosdb.core.query.DocumentQuery; +import com.microsoft.azure.spring.data.cosmosdb.repository.ReactiveCosmosRepository; +import org.reactivestreams.Publisher; +import org.springframework.context.ApplicationContext; +import org.springframework.data.domain.Sort; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.io.Serializable; + +/** + * Repository class for simple reactive Cosmos operation + */ +public class SimpleReactiveCosmosRepository implements ReactiveCosmosRepository { + + private final CosmosEntityInformation entityInformation; + private final ReactiveCosmosOperations cosmosOperations; + + /** + * Initialization with metadata and applicationContext will create container if required + * + * @param metadata for entityInformation + * @param applicationContext for cosmosOperations + */ + public SimpleReactiveCosmosRepository(CosmosEntityInformation metadata, + ApplicationContext applicationContext) { + this.cosmosOperations = applicationContext.getBean(ReactiveCosmosOperations.class); + this.entityInformation = metadata; + + if (this.entityInformation.isAutoCreateContainer()) { + createContainerIfNotExists(); + } + } + + /** + * Initialization with metadata and reactiveCosmosOperations + * + * @param metadata for entityInformation + * @param reactiveCosmosOperations for cosmosOperations + */ + public SimpleReactiveCosmosRepository(CosmosEntityInformation metadata, + ReactiveCosmosOperations reactiveCosmosOperations) { + this.cosmosOperations = reactiveCosmosOperations; + this.entityInformation = metadata; + + if (this.entityInformation.isAutoCreateContainer()) { + createContainerIfNotExists(); + } + } + + private CosmosContainerResponse createContainerIfNotExists() { + return this.cosmosOperations.createContainerIfNotExists(this.entityInformation).block(); + } + + @Override + public Flux findAll(Sort sort) { + Assert.notNull(sort, "Sort must not be null!"); + + final DocumentQuery query = + new DocumentQuery(Criteria.getInstance(CriteriaType.ALL)).with(sort); + + return cosmosOperations.find(query, entityInformation.getJavaType(), + entityInformation.getContainerName()); + } + + @Override + public Flux findAll(PartitionKey partitionKey) { + return cosmosOperations.findAll(partitionKey, entityInformation.getJavaType()); + } + + @Override + public Mono save(S entity) { + + Assert.notNull(entity, "Entity must not be null!"); + + if (entityInformation.isNew(entity)) { + return cosmosOperations.insert(entityInformation.getContainerName(), + entity, + createKey(entityInformation.getPartitionKeyFieldValue(entity))); + } else { + return cosmosOperations.upsert(entityInformation.getContainerName(), + entity, createKey(entityInformation.getPartitionKeyFieldValue(entity))); + } + } + + @Override + public Flux saveAll(Iterable entities) { + + Assert.notNull(entities, "The given Iterable of entities must not be null!"); + + return Flux.fromIterable(entities).flatMap(this::save); + } + + @Override + public Flux saveAll(Publisher entityStream) { + + Assert.notNull(entityStream, "The given Publisher of entities must not be null!"); + + return Flux.from(entityStream).flatMap(this::save); + } + + @Override + public Mono findById(K id) { + Assert.notNull(id, "The given id must not be null!"); + return cosmosOperations.findById(entityInformation.getContainerName(), id, + entityInformation.getJavaType()); + } + + @Override + public Mono findById(Publisher publisher) { + Assert.notNull(publisher, "The given id must not be null!"); + + return Mono.from(publisher).flatMap( + id -> cosmosOperations.findById(entityInformation.getContainerName(), + id, entityInformation.getJavaType())); + } + + @Override + public Mono findById(K id, PartitionKey partitionKey) { + Assert.notNull(id, "The given id must not be null!"); + return cosmosOperations.findById(id, + entityInformation.getJavaType(), partitionKey); + } + + @Override + public Mono existsById(K id) { + Assert.notNull(id, "The given id must not be null!"); + + return cosmosOperations.existsById(id, entityInformation.getJavaType(), + entityInformation.getContainerName()); + } + + @Override + public Mono existsById(Publisher publisher) { + Assert.notNull(publisher, "The given id must not be null!"); + + return Mono.from(publisher).flatMap(id -> cosmosOperations.existsById(id, + entityInformation.getJavaType(), + entityInformation.getContainerName())); + } + + @Override + public Flux findAll() { + return cosmosOperations.findAll(entityInformation.getContainerName(), + entityInformation.getJavaType()); + } + + @Override + public Flux findAllById(Iterable ids) { + Assert.notNull(ids, "Iterable ids should not be null"); + throw new UnsupportedOperationException(); + } + + @Override + public Flux findAllById(Publisher ids) { + Assert.notNull(ids, "The given Publisher of Id's must not be null!"); + throw new UnsupportedOperationException(); + } + + @Override + public Mono count() { + return cosmosOperations.count(entityInformation.getContainerName()); + } + + @Override + public Mono deleteById(K id) { + Assert.notNull(id, "The given id must not be null!"); + + return cosmosOperations.deleteById(entityInformation.getContainerName(), id, null); + } + + @Override + public Mono deleteById(Publisher publisher) { + Assert.notNull(publisher, "Id must not be null!"); + + return Mono.from(publisher).flatMap(id -> cosmosOperations.deleteById(entityInformation.getContainerName(), + id, null)).then(); + } + + @Override + public Mono deleteById(K id, PartitionKey partitionKey) { + Assert.notNull(id, "Id must not be null!"); + Assert.notNull(partitionKey, "PartitionKey must not be null!"); + + return cosmosOperations.deleteById(entityInformation.getContainerName(), id, partitionKey); + + } + + @Override + public Mono delete(@NonNull T entity) { + Assert.notNull(entity, "entity to be deleted must not be null!"); + + final Object id = entityInformation.getId(entity); + return cosmosOperations.deleteById(entityInformation.getContainerName(), + id, + createKey(entityInformation.getPartitionKeyFieldValue(entity))); + } + + @Override + public Mono deleteAll(Iterable entities) { + Assert.notNull(entities, "The given Iterable of entities must not be null!"); + + return Flux.fromIterable(entities).flatMap(this::delete).then(); + } + + @Override + public Mono deleteAll(Publisher entityStream) { + + Assert.notNull(entityStream, "The given Publisher of entities must not be null!"); + + return Flux.from(entityStream)// + .map(entityInformation::getRequiredId)// + .flatMap(this::deleteById)// + .then(); + } + + @Override + public Mono deleteAll() { + return cosmosOperations.deleteAll(entityInformation.getContainerName(), + entityInformation.getPartitionKeyFieldName()); + } + + private PartitionKey createKey(String partitionKeyValue) { + if (StringUtils.isEmpty(partitionKeyValue)) { + return null; + } + return new PartitionKey(partitionKeyValue); + } + +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/package-info.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/package-info.java new file mode 100644 index 000000000000..55c98f6a55cb --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * This package contains the support classes of setting up cosmosdb repositories and factories + */ +package com.microsoft.azure.spring.data.cosmosdb.repository.support; diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/resources/META-INF/project.properties b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/resources/META-INF/project.properties new file mode 100644 index 000000000000..90d42083480f --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/resources/META-INF/project.properties @@ -0,0 +1 @@ +project.version=@project.version@ diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/resources/META-INF/spring.factories b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000000..1884afe05dff --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.data.repository.core.support.RepositoryFactorySupport=com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosRepositoryFactory \ No newline at end of file diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/main/resources/telemetry.config b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/resources/telemetry.config new file mode 100644 index 000000000000..61b615fa0ef1 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/main/resources/telemetry.config @@ -0,0 +1 @@ +telemetry.instrumentationKey=@telemetry.instrumentationKey@ \ No newline at end of file diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/CosmosDbFactoryTestIT.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/CosmosDbFactoryTestIT.java new file mode 100644 index 000000000000..cd027302ec74 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/CosmosDbFactoryTestIT.java @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb; + +import com.microsoft.azure.spring.data.cosmosdb.config.CosmosDBConfig; +import com.microsoft.azure.spring.data.cosmosdb.exception.CosmosDBAccessException; +import com.microsoft.azure.spring.data.cosmosdb.repository.TestRepositoryConfig; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.*; +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class CosmosDbFactoryTestIT { + + @Value("${cosmosdb.uri:}") + private String cosmosDbUri; + + @Value("${cosmosdb.key:}") + private String cosmosDbKey; + + @Test(expected = IllegalArgumentException.class) + public void testEmptyKey() { + final CosmosDBConfig dbConfig = CosmosDBConfig.builder(COSMOSDB_FAKE_HOST, "", DB_NAME).build(); + new CosmosDbFactory(dbConfig); + } + + @Test + public void testInvalidEndpoint() { + final CosmosDBConfig dbConfig = + CosmosDBConfig.builder(COSMOSDB_FAKE_HOST, COSMOSDB_FAKE_KEY, DB_NAME).build(); + final CosmosDbFactory factory = new CosmosDbFactory(dbConfig); + + assertThat(factory).isNotNull(); + } + + @Test + public void testConnectWithConnectionString() { + final CosmosDBConfig dbConfig = + CosmosDBConfig.builder(COSMOSDB_FAKE_CONNECTION_STRING, DB_NAME).build(); + final CosmosDbFactory factory = new CosmosDbFactory(dbConfig); + + assertThat(factory).isNotNull(); + } + + @Test(expected = CosmosDBAccessException.class) + public void testInvalidConnectionString() { + CosmosDBConfig.builder(COSMOSDB_INVALID_FAKE_CONNECTION_STRING, DB_NAME).build(); + } + + @Test + public void testConnectionPolicyUserAgentKept() { + final CosmosDBConfig dbConfig = + CosmosDBConfig.builder(cosmosDbUri, cosmosDbKey, DB_NAME).build(); + final CosmosDbFactory factory = new CosmosDbFactory(dbConfig); + factory.getCosmosClient(); + + final String uaSuffix = factory.getConfig().getConnectionPolicy().userAgentSuffix(); + assertThat(uaSuffix).contains("spring-data"); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/UserAgentTestIT.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/UserAgentTestIT.java new file mode 100644 index 000000000000..3714460b8996 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/UserAgentTestIT.java @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb; + +import com.microsoft.azure.spring.data.cosmosdb.common.PropertyLoader; +import com.microsoft.azure.spring.data.cosmosdb.common.TestConstants; +import com.microsoft.azure.spring.data.cosmosdb.config.CosmosDBConfig; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.springframework.context.annotation.PropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +@Ignore("Cannot use fake uri and key with CosmosDbFactory as it tries the connection on new creation." + + "At the same time, cannot use mockito with real values, because it won't prepare PropertyLoader class for mocking") +@RunWith(PowerMockRunner.class) +@PrepareForTest(PropertyLoader.class) +@PropertySource(value = {"classpath:application.properties"}) +public class UserAgentTestIT { + + private static final String TEST_VERSION = "1.0.0-FOR-TEST"; + + @Test + public void testUserAgentSuffixAppended() { + PowerMockito.mockStatic(PropertyLoader.class); + Mockito.doReturn(TEST_VERSION).when(PropertyLoader.getProjectVersion()); + final CosmosDBConfig dbConfig = CosmosDBConfig.builder(TestConstants.COSMOSDB_FAKE_HOST, + TestConstants.COSMOSDB_FAKE_KEY, TestConstants.DB_NAME).build(); + final CosmosDbFactory factory = new CosmosDbFactory(dbConfig); + factory.getCosmosClient(); + assertThat(factory.getConfig().getConnectionPolicy().userAgentSuffix()).contains(TEST_VERSION); + } + +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/common/DynamicContainer.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/common/DynamicContainer.java new file mode 100644 index 000000000000..1f635b5f3a04 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/common/DynamicContainer.java @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.common; + +public class DynamicContainer { + private String containerName; + + public DynamicContainer(String containerName) { + this.containerName = containerName; + } + + public String getContainerName() { + return this.containerName; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/common/ExpressionResolverUnitTest.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/common/ExpressionResolverUnitTest.java new file mode 100644 index 000000000000..ccb3160225ca --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/common/ExpressionResolverUnitTest.java @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.common; + +import org.junit.Test; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * + * @author Domenico Sibilio + * + */ +public class ExpressionResolverUnitTest { + private static final String LITERAL_EXPRESSION = "literal expression"; + private static final String SPEL_EXPRESSION = "#{@environment.getProperty('dynamic.collection.name')}"; + + @Test + public void testLiteralExpressionsShouldNotBeAltered() { + assertEquals(LITERAL_EXPRESSION, ExpressionResolver.resolveExpression(LITERAL_EXPRESSION)); + } + + @Test + public void testExpressionsShouldBeResolved() { + final AnnotationConfigApplicationContext applicationContext = + new AnnotationConfigApplicationContext(TestConfiguration.class); + + assertNotNull(applicationContext.getBean(ExpressionResolver.class)); + assertEquals(TestConstants.DYNAMIC_PROPERTY_COLLECTION_NAME, + ExpressionResolver.resolveExpression(SPEL_EXPRESSION)); + } + + @Configuration + @PropertySource("application.properties") + static class TestConfiguration { + @Bean + public ExpressionResolver expressionResolver(ConfigurableBeanFactory beanFactory) { + return new ExpressionResolver(beanFactory); + } + } + +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/common/MemoizerUnitTest.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/common/MemoizerUnitTest.java new file mode 100644 index 000000000000..0da9f66b63ff --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/common/MemoizerUnitTest.java @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.common; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.stream.IntStream; + +/** + * + * @author Domenico Sibilio + * + */ +public class MemoizerUnitTest { + private static final String KEY = "key_1"; + private static final Map countMap = new HashMap<>(); + private static final Function memoizedFunction = + Memoizer.memoize(MemoizerUnitTest::incrCount); + + @Before + public void setUp() { + countMap.put(KEY, new AtomicInteger(0)); + } + + @Test + public void testMemoizedFunctionShouldBeCalledOnlyOnce() { + IntStream + .range(0, 10) + .forEach(number -> memoizedFunction.apply(KEY)); + + assertEquals(1, countMap.get(KEY).get()); + } + + @Test + public void testDifferentMemoizersShouldNotShareTheSameCache() { + IntStream + .range(0, 10) + .forEach(number -> Memoizer.memoize(MemoizerUnitTest::incrCount).apply(KEY)); + + assertEquals(10, countMap.get(KEY).get()); + } + + private static int incrCount(String key) { + return countMap.get(key).incrementAndGet(); + } + +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/common/PageTestUtils.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/common/PageTestUtils.java new file mode 100644 index 000000000000..193369f5978e --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/common/PageTestUtils.java @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.common; + +import com.microsoft.azure.spring.data.cosmosdb.core.query.CosmosPageRequest; +import org.json.JSONObject; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertTrue; + +public class PageTestUtils { + public static void validateLastPage(Page page, int pageSize) { + final Pageable pageable = page.getPageable(); + + assertThat(pageable).isInstanceOf(CosmosPageRequest.class); + assertTrue(continuationTokenIsNull((CosmosPageRequest) pageable)); + assertThat(pageable.getPageSize()).isEqualTo(pageSize); + } + + public static void validateNonLastPage(Page page, int pageSize) { + final Pageable pageable = page.getPageable(); + + assertThat(pageable).isInstanceOf(CosmosPageRequest.class); + assertThat(((CosmosPageRequest) pageable).getRequestContinuation()).isNotNull(); + assertThat(((CosmosPageRequest) pageable).getRequestContinuation()).isNotBlank(); + assertThat(pageable.getPageSize()).isEqualTo(pageSize); + } + + private static boolean continuationTokenIsNull(CosmosPageRequest pageRequest) { + final String tokenJson = pageRequest.getRequestContinuation(); + if (tokenJson == null) { + return true; + } + + final JSONObject jsonObject = new JSONObject(tokenJson); + + return jsonObject.isNull("compositeToken"); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/common/PropertyLoaderUnitTest.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/common/PropertyLoaderUnitTest.java new file mode 100644 index 000000000000..e66f6d6ece4e --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/common/PropertyLoaderUnitTest.java @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.common; + +import org.junit.Assert; +import org.junit.Test; + +public class PropertyLoaderUnitTest { + + @Test + public void testGetProjectVersion() { + final String version = PropertyLoader.getProjectVersion(); + + Assert.assertNotNull(version); + Assert.assertNotEquals(version, ""); + } + + @Test + public void testGetApplicationTelemetryAllowed() { + final boolean isAllowed = PropertyLoader.isApplicationTelemetryAllowed(); + + Assert.assertFalse(isAllowed); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/common/ResponseDiagnosticsTestUtils.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/common/ResponseDiagnosticsTestUtils.java new file mode 100644 index 000000000000..58049b859fb8 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/common/ResponseDiagnosticsTestUtils.java @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.common; + +import com.azure.data.cosmos.CosmosResponseDiagnostics; +import com.azure.data.cosmos.FeedResponseDiagnostics; +import com.microsoft.azure.spring.data.cosmosdb.core.ResponseDiagnostics; +import com.microsoft.azure.spring.data.cosmosdb.core.ResponseDiagnosticsProcessor; + + +public class ResponseDiagnosticsTestUtils { + + private final ResponseDiagnosticsProcessor responseDiagnosticsProcessor; + private ResponseDiagnostics diagnostics; + + public ResponseDiagnosticsTestUtils() { + responseDiagnosticsProcessor = responseDiagnostics -> { + diagnostics = responseDiagnostics; + }; + } + + public CosmosResponseDiagnostics getCosmosResponseDiagnostics() { + return diagnostics == null ? null : diagnostics.getCosmosResponseDiagnostics(); + } + + public FeedResponseDiagnostics getFeedResponseDiagnostics() { + return diagnostics == null ? null : diagnostics.getFeedResponseDiagnostics(); + } + + public ResponseDiagnostics.CosmosResponseStatistics getCosmosResponseStatistics() { + return diagnostics == null ? null : diagnostics.getCosmosResponseStatistics(); + } + + public ResponseDiagnosticsProcessor getResponseDiagnosticsProcessor() { + return responseDiagnosticsProcessor; + } + + public ResponseDiagnostics getDiagnostics() { + return diagnostics; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/common/TestConstants.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/common/TestConstants.java new file mode 100644 index 000000000000..5655cbba7a35 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/common/TestConstants.java @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.common; + +import com.azure.data.cosmos.IndexingMode; +import com.microsoft.azure.spring.data.cosmosdb.domain.Address; + +import java.util.Arrays; +import java.util.List; + +public final class TestConstants { + private static final int SUFFIX_LENGTH = 1; + + private static final Address ADDRESS_1 = new Address("201107", "Zixing Road", "Shanghai"); + private static final Address ADDRESS_2 = new Address("200000", "Xuhui", "Shanghai"); + public static final List HOBBIES = Arrays.asList("photography", "fishing"); + public static final List UPDATED_HOBBIES = Arrays.asList("updatedPhotography", "updatedFishing"); + public static final List
ADDRESSES = Arrays.asList(ADDRESS_1, ADDRESS_2); + + public static final int DEFAULT_TIME_TO_LIVE = -1; + public static final String DEFAULT_COLLECTION_NAME = "Person"; + public static final int DEFAULT_REQUEST_UNIT = 4000; + public static final boolean DEFAULT_INDEXINGPOLICY_AUTOMATIC = true; + public static final IndexingMode DEFAULT_INDEXINGPOLICY_MODE = IndexingMode.CONSISTENT; + public static final String[] DEFAULT_EXCLUDEDPATHS = {}; + public static final String[] DEFAULT_INCLUDEDPATHS = { + "{\"path\":\"/*\",\"indexes\":[" + + "{\"kind\":\"Range\",\"dataType\":\"Number\",\"precision\":-1}," + + "{\"kind\":\"Hash\",\"dataType\":\"String\",\"precision\":3}" + + "]}", + }; + + public static final String ROLE_COLLECTION_NAME = "RoleCollectionName"; + public static final int REQUEST_UNIT = 4000; + public static final int TIME_TO_LIVE = 5; + public static final boolean INDEXINGPOLICY_AUTOMATIC = false; + public static final IndexingMode INDEXINGPOLICY_MODE = IndexingMode.LAZY; + public static final String INCLUDEDPATH_0 = "{\"path\":\"/*\",\"indexes\":[" + + "{\"kind\":\"Range\",\"dataType\":\"Number\",\"precision\":2}," + + "{\"kind\":\"Hash\",\"dataType\":\"String\",\"precision\":2}," + + "{\"kind\":\"Spatial\",\"dataType\":\"Point\"}" + + "]}"; + public static final String INCLUDEDPATH_1 = "{\"path\":\"/cache/*\",\"indexes\":[" + + "{\"kind\":\"Range\",\"dataType\":\"Number\",\"precision\":3}," + + "{\"kind\":\"Hash\",\"dataType\":\"String\",\"precision\":3}," + + "{\"kind\":\"Spatial\",\"dataType\":\"LineString\"}" + + "]}"; + public static final String INCLUDEDPATH_2 = "{\"path\":\"/entities/*\",\"indexes\":[" + + "{\"kind\":\"Range\",\"dataType\":\"Number\",\"precision\":4}," + + "{\"kind\":\"Hash\",\"dataType\":\"String\",\"precision\":4}," + + "{\"kind\":\"Spatial\",\"dataType\":\"Polygon\"}" + + "]}"; + public static final String[] INCLUDEDPATHS = { + INCLUDEDPATH_0, + INCLUDEDPATH_1, + INCLUDEDPATH_2, + }; + public static final String EXCLUDEDPATH_0 = "{\"path\":\"/excluded/*\"}"; + public static final String EXCLUDEDPATH_1 = "{\"path\":\"/props/*\"}"; + public static final String[] EXCLUDEDPATHS = { + EXCLUDEDPATH_0, + EXCLUDEDPATH_1, + }; + + public static final String ORDER_BY_STRING_PATH = "{\"path\":\"/*\",\"indexes\":[" + + "{\"kind\":\"Range\",\"dataType\":\"String\",\"precision\":-1}," + + "]}"; + + public static final String STARTSWITH_INCLUDEDPATH = + "{\"path\":\"/*\",\"indexes\":[" + + "{\"kind\":\"Range\",\"dataType\":\"Number\",\"precision\":-1}," + + "{\"kind\":\"Range\",\"dataType\":\"String\",\"precision\":3}" + + "]}"; + + public static final String[] PERSON_INCLUDEDPATHS = { + STARTSWITH_INCLUDEDPATH + }; + + public static final String DB_NAME = "testdb"; + public static final String FIRST_NAME = "first_name_li"; + public static final String LAST_NAME = "last_name_p"; + public static final String ID_1 = "id-1"; + public static final String ID_2 = "id-2"; + public static final String ID_3 = "id-3"; + public static final String ID_4 = "id-4"; + public static final String NEW_FIRST_NAME = "new_first_name"; + public static final String NEW_LAST_NAME = "new_last_name"; + public static final String UPDATED_FIRST_NAME = "updated_first_name"; + public static final String UPDATED_LAST_NAME = "updated_last_name"; + public static final String LEVEL = "B"; + public static final String ROLE_NAME = "Developer"; + public static final String NOT_EXIST_ID = "non_exist_id"; + + public static final String DATE_STRING = "8/8/2017"; + public static final String DATE_BEFORE_STRING = "8/1/2017"; + public static final String DATE_AFTER_STRING = "8/13/2017"; + public static final String DATE_FUTURE_STRING_1 = "9/13/2017"; + public static final String DATE_FUTURE_STRING_2 = "9/14/2017"; + public static final String DATE_FORMAT = "dd/MM/yyyy"; + public static final String DATE_TIMEZONE_STRING = "1/1/2000 00:00 GMT"; + public static final String DATE_TIMEZONE_FORMAT = "dd/MM/yyyy HH:mm ZZZ"; + public static final long MILLI_SECONDS = 946684800000L; + + public static final String POSTAL_CODE = "98052"; + public static final String POSTAL_CODE_0 = "00000"; + public static final String POSTAL_CODE_1 = "11111"; + public static final String CITY = "testCity"; + public static final String CITY_0 = "testCityZero"; + public static final String CITY_1 = "testCityOne"; + public static final String UPDATED_CITY = "updatedCityOne"; + public static final String STREET = "testStreet"; + public static final String STREET_0 = "testStreetZero"; + public static final String STREET_1 = "testStreetOne"; + public static final String STREET_2 = "testStreetTwo"; + public static final String NEW_STREET = "newTestStreet"; + public static final String UPDATED_STREET = "updatedTestStreet"; + public static final String MESSAGE = "test pojo with date"; + public static final String NEW_MESSAGE = "new test message"; + + public static final String CRITERIA_KEY = "CriteriaTestKey"; + public static final String CRITERIA_FAKE_KEY = "CriteriaFakeKey"; + public static final String CRITERIA_OBJECT = "CriteriaTestObject"; + + public static final String COSMOSDB_FAKE_HOST = "https://fakeuri"; + public static final String COSMOSDB_FAKE_KEY = "fakekey"; + public static final String COSMOSDB_FAKE_CONNECTION_STRING = + "AccountEndpoint=https://fakeuri/;AccountKey=fakekey;"; + public static final String COSMOSDB_INVALID_FAKE_CONNECTION_STRING = "invalid connection string"; + + public static final String PROPERTY_ID = "id"; + public static final String PROPERTY_FIRST_NAME = "firstName"; + public static final String PROPERTY_LAST_NAME = "lastName"; + public static final String PROPERTY_HOBBIES = "hobbies"; + public static final String PROPERTY_SHIPPING_ADDRESSES = "shippingAddresses"; + + public static final String PROPERTY_CITY = "city"; + public static final String PROPERTY_STREET = "street"; + + public static final String PROPERTY_MESSAGE = "message"; + public static final String PROPERTY_DATE = "date"; + + public static final int PAGE_SIZE_1 = 1; + public static final int PAGE_SIZE_2 = 2; + public static final int PAGE_SIZE_3 = 3; + + public static final String DYNAMIC_PROPERTY_COLLECTION_NAME = "spel-property-collection"; + public static final String DYNAMIC_BEAN_COLLECTION_NAME = "spel-bean-collection"; + + private TestConstants() { + } +} + diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/common/TestUtils.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/common/TestUtils.java new file mode 100644 index 000000000000..eadb92eeecd1 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/common/TestUtils.java @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.common; + +import org.springframework.util.Assert; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class TestUtils { + public static List toList(Iterable iterable) { + if (iterable != null) { + final List list = new ArrayList<>(); + iterable.forEach(list::add); + return list; + } + return null; + } + + public static void testIndexingPolicyPathsEquals(List policyPaths, + String[] pathsExpected) { + if (policyPaths == null) { + throw new IllegalStateException("policyPaths should not be null"); + } else if (pathsExpected == null) { + throw new IllegalStateException("pathsExpected should not be null"); + } + + final Iterator pathIterator = policyPaths.iterator(); + + Assert.isTrue(pathsExpected.length == policyPaths.size(), "unmatched size of policy paths"); + + for (final String path: pathsExpected) { + Assert.isTrue(pathIterator.hasNext(), "policy path iterator should have next"); + final T includedPath = pathIterator.next(); + Assert.isTrue(includedPath.toString().equals(path), "unmatched policy path"); + } + } +} + diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/config/AbstractCosmosConfigurationIT.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/config/AbstractCosmosConfigurationIT.java new file mode 100644 index 000000000000..a3fdd33fc4ef --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/config/AbstractCosmosConfigurationIT.java @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.config; + +import com.azure.data.cosmos.ConsistencyLevel; +import com.azure.data.cosmos.CosmosClient; +import com.azure.data.cosmos.internal.RequestOptions; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.azure.spring.data.cosmosdb.Constants; +import com.microsoft.azure.spring.data.cosmosdb.CosmosDbFactory; +import com.microsoft.azure.spring.data.cosmosdb.common.ExpressionResolver; +import com.microsoft.azure.spring.data.cosmosdb.common.TestConstants; +import org.assertj.core.api.Assertions; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.Mock; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.util.StringUtils; + +import static org.junit.Assert.assertNotNull; + +public class AbstractCosmosConfigurationIT { + private static final String OBJECTMAPPER_BEAN_NAME = Constants.OBJECTMAPPER_BEAN_NAME; + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void containsExpressionResolver() { + final AbstractApplicationContext context = new AnnotationConfigApplicationContext( + TestCosmosConfiguration.class); + + assertNotNull(context.getBean(ExpressionResolver.class)); + } + + @Test + public void containsCosmosDbFactory() { + final AbstractApplicationContext context = new AnnotationConfigApplicationContext( + TestCosmosConfiguration.class); + + Assertions.assertThat(context.getBean(CosmosDbFactory.class)).isNotNull(); + } + + @Test(expected = NoSuchBeanDefinitionException.class) + public void defaultObjectMapperBeanNotExists() { + final AbstractApplicationContext context = new AnnotationConfigApplicationContext( + TestCosmosConfiguration.class); + + context.getBean(ObjectMapper.class); + } + + @Test + public void objectMapperIsConfigurable() { + final AbstractApplicationContext context = new AnnotationConfigApplicationContext( + ObjectMapperConfiguration.class); + + Assertions.assertThat(context.getBean(ObjectMapper.class)).isNotNull(); + Assertions.assertThat(context.getBean(OBJECTMAPPER_BEAN_NAME)).isNotNull(); + } + + @Test + public void testRequestOptionsConfigurable() { + final AbstractApplicationContext context = new AnnotationConfigApplicationContext( + RequestOptionsConfiguration.class); + final CosmosDbFactory factory = context.getBean(CosmosDbFactory.class); + + Assertions.assertThat(factory).isNotNull(); + + final RequestOptions options = factory.getConfig().getRequestOptions(); + + Assertions.assertThat(options).isNotNull(); + Assertions.assertThat(options.getConsistencyLevel()).isEqualTo(ConsistencyLevel.CONSISTENT_PREFIX); + Assertions.assertThat(options.isScriptLoggingEnabled()).isTrue(); + } + + @Configuration + @PropertySource(value = {"classpath:application.properties"}) + static class TestCosmosConfiguration extends AbstractCosmosConfiguration { + + @Value("${cosmosdb.uri:}") + private String cosmosDbUri; + + @Value("${cosmosdb.key:}") + private String cosmosDbKey; + + @Value("${cosmosdb.database:}") + private String database; + + @Mock + private CosmosClient mockClient; + + @Bean + public CosmosDBConfig getConfig() { + final String dbName = StringUtils.hasText(this.database) ? this.database : TestConstants.DB_NAME; + return CosmosDBConfig.builder(cosmosDbUri, cosmosDbKey, dbName).build(); + } + + @Override + public CosmosClient cosmosClient(CosmosDBConfig config) { + return mockClient; + } + } + + @Configuration + static class ObjectMapperConfiguration extends TestCosmosConfiguration { + @Bean(name = OBJECTMAPPER_BEAN_NAME) + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } + } + + @Configuration + @PropertySource(value = {"classpath:application.properties"}) + static class RequestOptionsConfiguration extends AbstractCosmosConfiguration { + + @Value("${cosmosdb.uri:}") + private String cosmosDbUri; + + @Value("${cosmosdb.key:}") + private String cosmosDbKey; + + @Value("${cosmosdb.database:}") + private String database; + + private RequestOptions getRequestOptions() { + final RequestOptions options = new RequestOptions(); + + options.setConsistencyLevel(ConsistencyLevel.CONSISTENT_PREFIX); + options.setScriptLoggingEnabled(true); + + return options; + } + + @Bean + public CosmosDBConfig getConfig() { + final String dbName = StringUtils.hasText(this.database) ? this.database : TestConstants.DB_NAME; + final RequestOptions options = getRequestOptions(); + return CosmosDBConfig.builder(cosmosDbUri, cosmosDbKey, dbName) + .requestOptions(options) + .build(); + } + + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/CosmosTemplateIT.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/CosmosTemplateIT.java new file mode 100644 index 000000000000..289a2e4d33ea --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/CosmosTemplateIT.java @@ -0,0 +1,438 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.core; + +import com.azure.data.cosmos.CosmosClientException; +import com.azure.data.cosmos.PartitionKey; +import com.microsoft.azure.spring.data.cosmosdb.CosmosDbFactory; +import com.microsoft.azure.spring.data.cosmosdb.common.ResponseDiagnosticsTestUtils; +import com.microsoft.azure.spring.data.cosmosdb.config.CosmosDBConfig; +import com.microsoft.azure.spring.data.cosmosdb.core.convert.MappingCosmosConverter; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.CosmosMappingContext; +import com.microsoft.azure.spring.data.cosmosdb.core.query.CosmosPageRequest; +import com.microsoft.azure.spring.data.cosmosdb.core.query.Criteria; +import com.microsoft.azure.spring.data.cosmosdb.core.query.CriteriaType; +import com.microsoft.azure.spring.data.cosmosdb.core.query.DocumentQuery; +import com.microsoft.azure.spring.data.cosmosdb.domain.Person; +import com.microsoft.azure.spring.data.cosmosdb.exception.CosmosDBAccessException; +import com.microsoft.azure.spring.data.cosmosdb.repository.TestRepositoryConfig; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.assertj.core.util.Lists; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.domain.EntityScanner; +import org.springframework.context.ApplicationContext; +import org.springframework.data.annotation.Persistent; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static com.microsoft.azure.spring.data.cosmosdb.common.PageTestUtils.validateLastPage; +import static com.microsoft.azure.spring.data.cosmosdb.common.PageTestUtils.validateNonLastPage; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.ADDRESSES; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.FIRST_NAME; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.HOBBIES; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.ID_1; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.ID_2; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.ID_3; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.LAST_NAME; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.NEW_FIRST_NAME; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.NEW_LAST_NAME; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.NOT_EXIST_ID; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.PAGE_SIZE_1; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.PAGE_SIZE_2; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.PAGE_SIZE_3; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.UPDATED_FIRST_NAME; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class CosmosTemplateIT { + private static final Person TEST_PERSON = new Person(ID_1, FIRST_NAME, LAST_NAME, HOBBIES, + ADDRESSES); + + private static final Person TEST_PERSON_2 = new Person(ID_2, + NEW_FIRST_NAME, + NEW_LAST_NAME, HOBBIES, ADDRESSES); + + private static final Person TEST_PERSON_3 = new Person(ID_3, + NEW_FIRST_NAME, + NEW_LAST_NAME, HOBBIES, ADDRESSES); + + private static final String PRECONDITION_IS_NOT_MET = "is not met"; + + private static final String WRONG_ETAG = "WRONG_ETAG"; + + private static CosmosTemplate cosmosTemplate; + private static CosmosEntityInformation personInfo; + private static String containerName; + private static boolean initialized; + + private Person insertedPerson; + + @Autowired + private ApplicationContext applicationContext; + @Autowired + private CosmosDBConfig dbConfig; + @Autowired + private ResponseDiagnosticsTestUtils responseDiagnosticsTestUtils; + + @Before + public void setUp() throws ClassNotFoundException { + if (!initialized) { + final CosmosDbFactory cosmosDbFactory = new CosmosDbFactory(dbConfig); + + final CosmosMappingContext mappingContext = new CosmosMappingContext(); + personInfo = new CosmosEntityInformation<>(Person.class); + containerName = personInfo.getContainerName(); + + mappingContext.setInitialEntitySet(new EntityScanner(this.applicationContext).scan(Persistent.class)); + + final MappingCosmosConverter cosmosConverter = new MappingCosmosConverter(mappingContext, + null); + cosmosTemplate = new CosmosTemplate(cosmosDbFactory, cosmosConverter, dbConfig.getDatabase()); + cosmosTemplate.createContainerIfNotExists(personInfo); + initialized = true; + } + + insertedPerson = cosmosTemplate.insert(Person.class.getSimpleName(), TEST_PERSON, null); + } + + @After + public void cleanup() { + cosmosTemplate.deleteAll(Person.class.getSimpleName(), Person.class); + } + + @AfterClass + public static void afterClassCleanup() { + cosmosTemplate.deleteContainer(personInfo.getContainerName()); + } + + @Test(expected = CosmosDBAccessException.class) + public void testInsertDuplicateId() { + cosmosTemplate.insert(Person.class.getSimpleName(), TEST_PERSON, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON))); + } + + @Test + public void testFindAll() { + final List result = cosmosTemplate.findAll(Person.class.getSimpleName(), Person.class); + assertThat(result.size()).isEqualTo(1); + assertThat(result.get(0)).isEqualTo(TEST_PERSON); + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } + + @Test + public void testFindById() { + final Person result = cosmosTemplate.findById(Person.class.getSimpleName(), + TEST_PERSON.getId(), Person.class); + assertEquals(result, TEST_PERSON); + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + + final Person nullResult = cosmosTemplate.findById(Person.class.getSimpleName(), + NOT_EXIST_ID, Person.class); + assertThat(nullResult).isNull(); + } + + @Test + public void testFindByMultiIds() { + cosmosTemplate.insert(TEST_PERSON_2, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2))); + cosmosTemplate.insert(TEST_PERSON_3, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_3))); + + final List ids = Lists.newArrayList(ID_1, ID_2, ID_3); + final List result = cosmosTemplate.findByIds(ids, Person.class, containerName); + + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + + final List expected = Lists.newArrayList(TEST_PERSON, TEST_PERSON_2, TEST_PERSON_3); + assertThat(result.size()).isEqualTo(expected.size()); + assertThat(result).containsAll(expected); + } + + @Test + public void testUpsertNewDocument() { + // Delete first as was inserted in setup + cosmosTemplate.deleteById(Person.class.getSimpleName(), TEST_PERSON.getId(), + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON))); + + final String firstName = NEW_FIRST_NAME + + "_" + + UUID.randomUUID().toString(); + final Person newPerson = new Person(TEST_PERSON.getId(), firstName, + NEW_FIRST_NAME, null, null); + + final Person person = cosmosTemplate.upsertAndReturnEntity(Person.class.getSimpleName(), + newPerson, + new PartitionKey(personInfo.getPartitionKeyFieldValue(newPerson))); + + assertThat(responseDiagnosticsTestUtils.getCosmosResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + + assertEquals(person.getFirstName(), firstName); + } + + @Test + public void testUpdateWithReturnEntity() { + final Person updated = new Person(TEST_PERSON.getId(), UPDATED_FIRST_NAME, + TEST_PERSON.getLastName(), TEST_PERSON.getHobbies(), TEST_PERSON.getShippingAddresses()); + updated.set_etag(insertedPerson.get_etag()); + + final Person updatedPerson = cosmosTemplate.upsertAndReturnEntity(Person.class.getSimpleName(), + updated, null); + + final Person findPersonById = cosmosTemplate.findById(Person.class.getSimpleName(), + updatedPerson.getId(), Person.class); + + assertEquals(updatedPerson, updated); + assertThat(updatedPerson.get_etag()).isEqualTo(findPersonById.get_etag()); + } + + @Test + public void testUpdate() { + final Person updated = new Person(TEST_PERSON.getId(), UPDATED_FIRST_NAME, + TEST_PERSON.getLastName(), TEST_PERSON.getHobbies(), TEST_PERSON.getShippingAddresses()); + updated.set_etag(insertedPerson.get_etag()); + + final Person person = cosmosTemplate.upsertAndReturnEntity(Person.class.getSimpleName(), + updated, null); + + assertThat(responseDiagnosticsTestUtils.getCosmosResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + + assertEquals(person, updated); + } + + @Test + public void testOptimisticLockWhenUpdatingWithWrongEtag() { + final Person updated = new Person(TEST_PERSON.getId(), UPDATED_FIRST_NAME, + TEST_PERSON.getLastName(), TEST_PERSON.getHobbies(), TEST_PERSON.getShippingAddresses()); + updated.set_etag(WRONG_ETAG); + + try { + cosmosTemplate.upsert(Person.class.getSimpleName(), updated, null); + } catch (CosmosDBAccessException e) { + assertThat(e.getCosmosClientException()).isNotNull(); + final Throwable cosmosClientException = e.getCosmosClientException(); + assertThat(cosmosClientException).isInstanceOf(CosmosClientException.class); + assertThat(cosmosClientException.getMessage()).contains(PRECONDITION_IS_NOT_MET); + + final Person unmodifiedPerson = cosmosTemplate.findById(Person.class.getSimpleName(), + TEST_PERSON.getId(), Person.class); + assertThat(unmodifiedPerson.getFirstName()).isEqualTo(insertedPerson.getFirstName()); + return; + } + + fail(); + } + + @Test + public void testDeleteById() { + cosmosTemplate.insert(TEST_PERSON_2, null); + assertThat(cosmosTemplate.findAll(Person.class).size()).isEqualTo(2); + + cosmosTemplate.deleteById(Person.class.getSimpleName(), TEST_PERSON.getId(), + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON))); + + assertThat(responseDiagnosticsTestUtils.getCosmosResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + + final List result = cosmosTemplate.findAll(Person.class); + + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + assertThat(result.size()).isEqualTo(1); + assertEquals(result.get(0), TEST_PERSON_2); + } + + @Test + public void testCountByContainer() { + final long prevCount = cosmosTemplate.count(containerName); + assertThat(prevCount).isEqualTo(1); + + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + + cosmosTemplate.insert(TEST_PERSON_2, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2))); + + assertThat(responseDiagnosticsTestUtils.getCosmosResponseDiagnostics()).isNotNull(); + + final long newCount = cosmosTemplate.count(containerName); + assertThat(newCount).isEqualTo(2); + + assertThat(responseDiagnosticsTestUtils.getCosmosResponseDiagnostics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } + + @Test + public void testCountByQuery() { + cosmosTemplate.insert(TEST_PERSON_2, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2))); + + assertThat(responseDiagnosticsTestUtils.getCosmosResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(TEST_PERSON_2.getFirstName())); + final DocumentQuery query = new DocumentQuery(criteria); + + final long count = cosmosTemplate.count(query, Person.class, containerName); + assertThat(count).isEqualTo(1); + + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } + + @Test + public void testFindAllPageableMultiPages() { + cosmosTemplate.insert(TEST_PERSON_2, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2))); + + assertThat(responseDiagnosticsTestUtils.getCosmosResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + + final CosmosPageRequest pageRequest = new CosmosPageRequest(0, PAGE_SIZE_1, null); + final Page page1 = cosmosTemplate.findAll(pageRequest, Person.class, containerName); + + assertThat(page1.getContent().size()).isEqualTo(PAGE_SIZE_1); + validateNonLastPage(page1, PAGE_SIZE_1); + + assertThat(responseDiagnosticsTestUtils.getCosmosResponseDiagnostics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + + final Page page2 = cosmosTemplate.findAll(page1.getPageable(), Person.class, + containerName); + assertThat(page2.getContent().size()).isEqualTo(1); + validateLastPage(page2, PAGE_SIZE_1); + + assertThat(responseDiagnosticsTestUtils.getCosmosResponseDiagnostics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } + + @Test + public void testPaginationQuery() { + cosmosTemplate.insert(TEST_PERSON_2, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2))); + + assertThat(responseDiagnosticsTestUtils.getCosmosResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(FIRST_NAME)); + final PageRequest pageRequest = new CosmosPageRequest(0, PAGE_SIZE_2, null); + final DocumentQuery query = new DocumentQuery(criteria).with(pageRequest); + + final Page page = cosmosTemplate.paginationQuery(query, Person.class, containerName); + assertThat(page.getContent().size()).isEqualTo(1); + validateLastPage(page, page.getContent().size()); + + assertThat(responseDiagnosticsTestUtils.getCosmosResponseDiagnostics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } + + @Test + public void testFindAllWithPageableAndSort() { + cosmosTemplate.insert(TEST_PERSON_2, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2))); + cosmosTemplate.insert(TEST_PERSON_3, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_3))); + + assertThat(responseDiagnosticsTestUtils.getCosmosResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + + final Sort sort = Sort.by(Sort.Direction.DESC, "firstName"); + final PageRequest pageRequest = new CosmosPageRequest(0, PAGE_SIZE_3, null, sort); + + final Page page = cosmosTemplate.findAll(pageRequest, Person.class, containerName); + assertThat(page.getContent().size()).isEqualTo(3); + validateLastPage(page, PAGE_SIZE_3); + + final List result = page.getContent(); + assertThat(result.get(0).getFirstName()).isEqualTo(NEW_FIRST_NAME); + assertThat(result.get(1).getFirstName()).isEqualTo(NEW_FIRST_NAME); + assertThat(result.get(2).getFirstName()).isEqualTo(FIRST_NAME); + + assertThat(responseDiagnosticsTestUtils.getCosmosResponseDiagnostics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + + } + + @Test + public void testFindAllWithTwoPagesAndVerifySortOrder() { + final Person testPerson4 = new Person("id_4", "barney", NEW_LAST_NAME, HOBBIES, ADDRESSES); + final Person testPerson5 = new Person("id_5", "fred", NEW_LAST_NAME, HOBBIES, ADDRESSES); + + cosmosTemplate.insert(TEST_PERSON_2, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2))); + cosmosTemplate.insert(TEST_PERSON_3, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_3))); + cosmosTemplate.insert(testPerson4, + new PartitionKey(personInfo.getPartitionKeyFieldValue(testPerson4))); + cosmosTemplate.insert(testPerson5, + new PartitionKey(personInfo.getPartitionKeyFieldValue(testPerson5))); + + final Sort sort = Sort.by(Sort.Direction.ASC, "firstName"); + final PageRequest pageRequest = new CosmosPageRequest(0, PAGE_SIZE_3, null, sort); + + final Page firstPage = cosmosTemplate.findAll(pageRequest, Person.class, + containerName); + + assertThat(firstPage.getContent().size()).isEqualTo(3); + validateNonLastPage(firstPage, firstPage.getContent().size()); + + final List firstPageResults = firstPage.getContent(); + assertThat(firstPageResults.get(0).getFirstName()).isEqualTo(testPerson4.getFirstName()); + assertThat(firstPageResults.get(1).getFirstName()).isEqualTo(FIRST_NAME); + assertThat(firstPageResults.get(2).getFirstName()).isEqualTo(testPerson5.getFirstName()); + + final Page secondPage = cosmosTemplate.findAll(firstPage.getPageable(), Person.class, + containerName); + + assertThat(secondPage.getContent().size()).isEqualTo(2); + validateLastPage(secondPage, secondPage.getContent().size()); + + final List secondPageResults = secondPage.getContent(); + assertThat(secondPageResults.get(0).getFirstName()).isEqualTo(NEW_FIRST_NAME); + assertThat(secondPageResults.get(1).getFirstName()).isEqualTo(NEW_FIRST_NAME); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/CosmosTemplateIllegalTest.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/CosmosTemplateIllegalTest.java new file mode 100644 index 000000000000..0c63ef1c49e9 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/CosmosTemplateIllegalTest.java @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.core; + +import com.azure.data.cosmos.PartitionKey; +import com.microsoft.azure.spring.data.cosmosdb.core.query.Criteria; +import com.microsoft.azure.spring.data.cosmosdb.core.query.DocumentQuery; +import com.microsoft.azure.spring.data.cosmosdb.domain.Person; +import org.apache.commons.lang3.StringUtils; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.util.Assert; + +import static com.microsoft.azure.spring.data.cosmosdb.core.query.CriteriaType.IS_EQUAL; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; + +@SuppressWarnings("unchecked") +@RunWith(MockitoJUnitRunner.class) +public class CosmosTemplateIllegalTest { + private static final String NULL_STR = null; + private static final String DUMMY_COLL = "dummy"; + private static final String DUMMY_ID = "ID_1"; + private static final PartitionKey DUMMY_KEY = new PartitionKey("dummy"); + private static final String EMPTY_STR = StringUtils.EMPTY; + private static final String WHITESPACES_STR = " "; + private static final String CHECK_FAILURE_MSG = "Illegal argument is not checked"; + + @Mock(answer = Answers.CALLS_REAL_METHODS) + private CosmosTemplate dbTemplate; + private Class dbTemplateClass; + + @Before + public void setUp() { + dbTemplateClass = dbTemplate.getClass(); + } + + @Test + public void deleteIllegalShouldFail() throws NoSuchMethodException { + final Method method = dbTemplateClass.getMethod("delete", DocumentQuery.class, Class.class, String.class); + final Criteria criteria = Criteria.getInstance(IS_EQUAL, "faker", Arrays.asList("faker-value")); + final DocumentQuery query = new DocumentQuery(criteria); + + checkIllegalArgument(method, null, Person.class, DUMMY_COLL); + checkIllegalArgument(method, query, null, DUMMY_COLL); + checkIllegalArgument(method, query, Person.class, null); + } + + @Test + public void deleteIllegalContainerShouldFail() throws NoSuchMethodException { + final Method method = dbTemplateClass.getDeclaredMethod("deleteAll", String.class, Class.class); + + checkIllegalArgument(method, NULL_STR, Person.class); + checkIllegalArgument(method, EMPTY_STR, Person.class); + checkIllegalArgument(method, WHITESPACES_STR, Person.class); + } + + @Test + public void deleteByIdIllegalArgsShouldFail() throws NoSuchMethodException { + final Method method = dbTemplateClass.getDeclaredMethod("deleteById", String.class, Object.class, + PartitionKey.class); + + // Test argument containerName + checkIllegalArgument(method, null, DUMMY_ID, DUMMY_KEY); + checkIllegalArgument(method, EMPTY_STR, DUMMY_ID, DUMMY_KEY); + checkIllegalArgument(method, WHITESPACES_STR, DUMMY_ID, DUMMY_KEY); + + // Test argument id + checkIllegalArgument(method, DUMMY_COLL, null, DUMMY_KEY); + checkIllegalArgument(method, DUMMY_COLL, EMPTY_STR, DUMMY_KEY); + checkIllegalArgument(method, DUMMY_COLL, WHITESPACES_STR, DUMMY_KEY); + } + + @Test + public void findByIdIllegalArgsShouldFail() throws NoSuchMethodException { + final Method method = dbTemplateClass.getDeclaredMethod("findById", Object.class, Class.class); + + checkIllegalArgument(method, DUMMY_ID, null); + } + + @Test + public void findByCollIdIllegalArgsShouldFail() throws NoSuchMethodException { + final Method method = dbTemplateClass.getDeclaredMethod("findById", String.class, + Object.class, Class.class); + + checkIllegalArgument(method, DUMMY_COLL, null, Person.class); + checkIllegalArgument(method, DUMMY_COLL, EMPTY_STR, Person.class); + checkIllegalArgument(method, DUMMY_COLL, WHITESPACES_STR, Person.class); + } + + /** + * Check IllegalArgumentException is thrown for illegal parameters + * @param method + * @param args Method invocation parameters + */ + private void checkIllegalArgument(Method method, Object... args) { + try { + method.invoke(dbTemplate, args); + } catch (IllegalAccessException | InvocationTargetException e) { + Assert.isTrue(e.getCause() instanceof IllegalArgumentException, CHECK_FAILURE_MSG); + return; // Test passed + } + + throw new IllegalStateException(CHECK_FAILURE_MSG, null); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/CosmosTemplatePartitionIT.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/CosmosTemplatePartitionIT.java new file mode 100644 index 000000000000..2094ff7058c0 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/CosmosTemplatePartitionIT.java @@ -0,0 +1,242 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.core; + +import com.azure.data.cosmos.PartitionKey; +import com.microsoft.azure.spring.data.cosmosdb.CosmosDbFactory; +import com.microsoft.azure.spring.data.cosmosdb.config.CosmosDBConfig; +import com.microsoft.azure.spring.data.cosmosdb.core.convert.MappingCosmosConverter; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.CosmosMappingContext; +import com.microsoft.azure.spring.data.cosmosdb.core.query.CosmosPageRequest; +import com.microsoft.azure.spring.data.cosmosdb.core.query.Criteria; +import com.microsoft.azure.spring.data.cosmosdb.core.query.CriteriaType; +import com.microsoft.azure.spring.data.cosmosdb.core.query.DocumentQuery; +import com.microsoft.azure.spring.data.cosmosdb.domain.PartitionPerson; +import com.microsoft.azure.spring.data.cosmosdb.repository.TestRepositoryConfig; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.domain.EntityScanner; +import org.springframework.context.ApplicationContext; +import org.springframework.data.annotation.Persistent; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import static com.microsoft.azure.spring.data.cosmosdb.common.PageTestUtils.validateLastPage; +import static com.microsoft.azure.spring.data.cosmosdb.common.PageTestUtils.validateNonLastPage; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.*; +import static com.microsoft.azure.spring.data.cosmosdb.core.query.CriteriaType.IS_EQUAL; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class CosmosTemplatePartitionIT { + private static final PartitionPerson TEST_PERSON = new PartitionPerson(ID_1, FIRST_NAME, LAST_NAME, + HOBBIES, ADDRESSES); + + private static final PartitionPerson TEST_PERSON_2 = new PartitionPerson(ID_2, NEW_FIRST_NAME, + TEST_PERSON.getLastName(), HOBBIES, ADDRESSES); + + private static CosmosTemplate cosmosTemplate; + private static String containerName; + private static CosmosEntityInformation personInfo; + private static boolean initialized; + + @Autowired + private ApplicationContext applicationContext; + @Autowired + private CosmosDBConfig dbConfig; + + @Before + public void setUp() throws ClassNotFoundException { + if (!initialized) { + final CosmosDbFactory cosmosDbFactory = new CosmosDbFactory(dbConfig); + final CosmosMappingContext mappingContext = new CosmosMappingContext(); + + personInfo = new CosmosEntityInformation<>(PartitionPerson.class); + mappingContext.setInitialEntitySet(new EntityScanner(this.applicationContext).scan(Persistent.class)); + + final MappingCosmosConverter dbConverter = new MappingCosmosConverter(mappingContext, null); + + cosmosTemplate = new CosmosTemplate(cosmosDbFactory, dbConverter, dbConfig.getDatabase()); + containerName = personInfo.getContainerName(); + + cosmosTemplate.createContainerIfNotExists(personInfo); + initialized = true; + } + + cosmosTemplate.insert(PartitionPerson.class.getSimpleName(), TEST_PERSON, + new PartitionKey(TEST_PERSON.getLastName())); + } + + @After + public void cleanup() { + cosmosTemplate.deleteAll(personInfo.getContainerName(), PartitionPerson.class); + } + + @AfterClass + public static void afterClassCleanup() { + cosmosTemplate.deleteContainer(personInfo.getContainerName()); + } + + @Test + public void testFindWithPartition() { + Criteria criteria = Criteria.getInstance(IS_EQUAL, PROPERTY_LAST_NAME, Arrays.asList(LAST_NAME)); + DocumentQuery query = new DocumentQuery(criteria); + List result = cosmosTemplate.find(query, PartitionPerson.class, + PartitionPerson.class.getSimpleName()); + + assertThat(result.size()).isEqualTo(1); + assertEquals(TEST_PERSON, result.get(0)); + + criteria = Criteria.getInstance(IS_EQUAL, PROPERTY_ID, Arrays.asList(ID_1)); + query = new DocumentQuery(criteria); + result = cosmosTemplate.find(query, PartitionPerson.class, PartitionPerson.class.getSimpleName()); + + assertThat(result.size()).isEqualTo(1); + assertEquals(TEST_PERSON, result.get(0)); + } + + + @Test + public void testFindByIdWithPartition() { + final PartitionPerson partitionPersonById = cosmosTemplate.findById(TEST_PERSON.getId(), + PartitionPerson.class, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON))); + + assertEquals(TEST_PERSON, partitionPersonById); + } + + @Test + public void testFindByNonExistIdWithPartition() { + final Criteria criteria = Criteria.getInstance(IS_EQUAL, PROPERTY_ID, Arrays.asList(NOT_EXIST_ID)); + final DocumentQuery query = new DocumentQuery(criteria); + + final List result = cosmosTemplate.find(query, PartitionPerson.class, + PartitionPerson.class.getSimpleName()); + assertThat(result.size()).isEqualTo(0); + } + + @Test + public void testUpsertNewDocumentPartition() { + final String firstName = NEW_FIRST_NAME + + "_" + UUID.randomUUID().toString(); + final PartitionPerson newPerson = new PartitionPerson(TEST_PERSON.getId(), + firstName, NEW_LAST_NAME, + null, null); + + final String partitionKeyValue = newPerson.getLastName(); + final PartitionPerson partitionPerson = + cosmosTemplate.upsertAndReturnEntity(PartitionPerson.class.getSimpleName(), newPerson, + new PartitionKey(partitionKeyValue)); + + final List result = cosmosTemplate.findAll(PartitionPerson.class); + + assertThat(result.size()).isEqualTo(2); + assertThat(partitionPerson.getFirstName()).isEqualTo(firstName); + } + + @Test + public void testUpdatePartition() { + final PartitionPerson updated = new PartitionPerson(TEST_PERSON.getId(), UPDATED_FIRST_NAME, + TEST_PERSON.getLastName(), TEST_PERSON.getHobbies(), TEST_PERSON.getShippingAddresses()); + final PartitionPerson partitionPerson = + cosmosTemplate.upsertAndReturnEntity(PartitionPerson.class.getSimpleName(), updated, + new PartitionKey(updated.getLastName())); + + assertEquals(partitionPerson, updated); + } + + @Test + public void testDeleteByIdPartition() { + // insert new document with same partition key + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(TEST_PERSON_2.getLastName())); + + final List inserted = cosmosTemplate.findAll(PartitionPerson.class); + assertThat(inserted.size()).isEqualTo(2); + assertThat(inserted.get(0).getLastName()).isEqualTo(TEST_PERSON.getLastName()); + assertThat(inserted.get(1).getLastName()).isEqualTo(TEST_PERSON.getLastName()); + + cosmosTemplate.deleteById(PartitionPerson.class.getSimpleName(), + TEST_PERSON.getId(), new PartitionKey(TEST_PERSON.getLastName())); + + final List result = cosmosTemplate.findAll(PartitionPerson.class); + assertThat(result.size()).isEqualTo(1); + assertEquals(result.get(0), TEST_PERSON_2); + } + + @Test + public void testCountForPartitionedCollection() { + final long prevCount = cosmosTemplate.count(containerName); + assertThat(prevCount).isEqualTo(1); + + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(TEST_PERSON_2.getLastName())); + + final long newCount = cosmosTemplate.count(containerName); + assertThat(newCount).isEqualTo(2); + } + + @Test + public void testCountForPartitionedCollectionByQuery() { + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(TEST_PERSON_2.getLastName())); + + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Arrays.asList(TEST_PERSON_2.getFirstName())); + final DocumentQuery query = new DocumentQuery(criteria); + + final long count = cosmosTemplate.count(query, PartitionPerson.class, containerName); + assertThat(count).isEqualTo(1); + } + + @Test + public void testNonExistFieldValue() { + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Arrays.asList("non-exist-first-name")); + final DocumentQuery query = new DocumentQuery(criteria); + + final long count = cosmosTemplate.count(query, PartitionPerson.class, containerName); + assertThat(count).isEqualTo(0); + } + + @Test + public void testPartitionedFindAllPageableMultiPages() { + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(TEST_PERSON_2.getLastName())); + + final CosmosPageRequest pageRequest = new CosmosPageRequest(0, PAGE_SIZE_1, null); + final Page page1 = cosmosTemplate.findAll(pageRequest, PartitionPerson.class, containerName); + + assertThat(page1.getContent().size()).isEqualTo(PAGE_SIZE_1); + validateNonLastPage(page1, PAGE_SIZE_1); + + final Page page2 = cosmosTemplate.findAll(page1.getPageable(), + PartitionPerson.class, containerName); + assertThat(page2.getContent().size()).isEqualTo(1); + validateLastPage(page2, PAGE_SIZE_1); + } + + @Test + public void testPartitionedPaginationQuery() { + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(TEST_PERSON_2.getLastName())); + + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Arrays.asList(FIRST_NAME)); + final PageRequest pageRequest = new CosmosPageRequest(0, PAGE_SIZE_2, null); + final DocumentQuery query = new DocumentQuery(criteria).with(pageRequest); + + final Page page = cosmosTemplate.paginationQuery(query, PartitionPerson.class, containerName); + assertThat(page.getContent().size()).isEqualTo(1); + validateLastPage(page, page.getContent().size()); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/CosmosTemplateUnitTest.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/CosmosTemplateUnitTest.java new file mode 100644 index 000000000000..09e796d184ec --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/CosmosTemplateUnitTest.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.core; + +import com.microsoft.azure.spring.data.cosmosdb.CosmosDbFactory; +import com.microsoft.azure.spring.data.cosmosdb.common.TestConstants; +import com.microsoft.azure.spring.data.cosmosdb.config.CosmosDBConfig; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class CosmosTemplateUnitTest { + + @Test(expected = IllegalArgumentException.class) + public void rejectNullDbFactory() { + final CosmosDBConfig dbConfig = CosmosDBConfig.builder("", "", TestConstants.DB_NAME).build(); + final CosmosDbFactory cosmosDbFactory = new CosmosDbFactory(dbConfig); + + new CosmosTemplate(cosmosDbFactory, null, TestConstants.DB_NAME); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/ReactiveCosmosTemplateIT.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/ReactiveCosmosTemplateIT.java new file mode 100644 index 000000000000..37395f876c36 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/ReactiveCosmosTemplateIT.java @@ -0,0 +1,403 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.core; + +import com.azure.data.cosmos.CosmosClientException; +import com.azure.data.cosmos.CosmosKeyCredential; +import com.azure.data.cosmos.PartitionKey; +import com.microsoft.azure.spring.data.cosmosdb.CosmosDbFactory; +import com.microsoft.azure.spring.data.cosmosdb.common.ResponseDiagnosticsTestUtils; +import com.microsoft.azure.spring.data.cosmosdb.common.TestConstants; +import com.microsoft.azure.spring.data.cosmosdb.config.CosmosDBConfig; +import com.microsoft.azure.spring.data.cosmosdb.core.convert.MappingCosmosConverter; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.CosmosMappingContext; +import com.microsoft.azure.spring.data.cosmosdb.core.query.Criteria; +import com.microsoft.azure.spring.data.cosmosdb.core.query.CriteriaType; +import com.microsoft.azure.spring.data.cosmosdb.core.query.DocumentQuery; +import com.microsoft.azure.spring.data.cosmosdb.domain.Person; +import com.microsoft.azure.spring.data.cosmosdb.exception.CosmosDBAccessException; +import com.microsoft.azure.spring.data.cosmosdb.repository.TestRepositoryConfig; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import io.reactivex.subscribers.TestSubscriber; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.domain.EntityScanner; +import org.springframework.context.ApplicationContext; +import org.springframework.data.annotation.Persistent; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.UPDATED_FIRST_NAME; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.fail; + +@SuppressWarnings("unchecked") +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class ReactiveCosmosTemplateIT { + private static final Person TEST_PERSON = new Person(TestConstants.ID_1, + TestConstants.FIRST_NAME, + TestConstants.LAST_NAME, TestConstants.HOBBIES, TestConstants.ADDRESSES); + + private static final Person TEST_PERSON_2 = new Person(TestConstants.ID_2, + TestConstants.NEW_FIRST_NAME, + TestConstants.NEW_LAST_NAME, TestConstants.HOBBIES, TestConstants.ADDRESSES); + + private static final Person TEST_PERSON_3 = new Person(TestConstants.ID_3, + TestConstants.NEW_FIRST_NAME, + TestConstants.NEW_LAST_NAME, TestConstants.HOBBIES, TestConstants.ADDRESSES); + + private static final Person TEST_PERSON_4 = new Person(TestConstants.ID_4, + TestConstants.NEW_FIRST_NAME, + TestConstants.NEW_LAST_NAME, TestConstants.HOBBIES, TestConstants.ADDRESSES); + + private static final String PRECONDITION_IS_NOT_MET = "is not met"; + private static final String WRONG_ETAG = "WRONG_ETAG"; + + @Value("${cosmosdb.secondaryKey}") + private String cosmosDbSecondaryKey; + + private static ReactiveCosmosTemplate cosmosTemplate; + private static String containerName; + private static CosmosEntityInformation personInfo; + private static CosmosKeyCredential cosmosKeyCredential; + + private static boolean initialized; + + private Person insertedPerson; + + @Autowired + private ApplicationContext applicationContext; + @Autowired + private CosmosDBConfig dbConfig; + @Autowired + private ResponseDiagnosticsTestUtils responseDiagnosticsTestUtils; + + @Before + public void setUp() throws ClassNotFoundException { + if (!initialized) { + cosmosKeyCredential = new CosmosKeyCredential(dbConfig.getKey()); + final CosmosDbFactory dbFactory = new CosmosDbFactory(dbConfig); + + final CosmosMappingContext mappingContext = new CosmosMappingContext(); + personInfo = new CosmosEntityInformation<>(Person.class); + containerName = personInfo.getContainerName(); + + mappingContext.setInitialEntitySet(new EntityScanner(this.applicationContext).scan(Persistent.class)); + + final MappingCosmosConverter dbConverter = + new MappingCosmosConverter(mappingContext, null); + cosmosTemplate = new ReactiveCosmosTemplate(dbFactory, dbConverter, dbConfig.getDatabase()); + cosmosTemplate.createContainerIfNotExists(personInfo).block().container(); + initialized = true; + } + + insertedPerson = cosmosTemplate.insert(TEST_PERSON, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON))).block(); + } + + @After + public void cleanup() { + // Reset master key + cosmosKeyCredential.key(dbConfig.getKey()); + cosmosTemplate.deleteAll(Person.class.getSimpleName(), + personInfo.getPartitionKeyFieldName()).block(); + } + + @AfterClass + public static void afterClassCleanup() { + cosmosTemplate.deleteContainer(personInfo.getContainerName()); + } + + @Test + public void testInsertDuplicateId() { + final Mono insertMono = cosmosTemplate.insert(TEST_PERSON, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON))); + final TestSubscriber testSubscriber = new TestSubscriber<>(); + insertMono.subscribe(testSubscriber); + testSubscriber.awaitTerminalEvent(); + testSubscriber.assertNotComplete(); + testSubscriber.assertTerminated(); + assertThat(testSubscriber.errors()).hasSize(1); + assertThat(((List) testSubscriber.getEvents().get(1)).get(0)) + .isInstanceOf(CosmosDBAccessException.class); + } + + @Test + public void testFindByID() { + final Mono findById = cosmosTemplate.findById(Person.class.getSimpleName(), + TEST_PERSON.getId(), + Person.class); + StepVerifier.create(findById) + .consumeNextWith(actual -> Assert.assertEquals(actual, TEST_PERSON)) + .verifyComplete(); + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } + + @Test + public void testFindByIDBySecondaryKey() { + cosmosKeyCredential.key(cosmosDbSecondaryKey); + final Mono findById = cosmosTemplate.findById(Person.class.getSimpleName(), + TEST_PERSON.getId(), + Person.class); + StepVerifier.create(findById).consumeNextWith(actual -> { + Assert.assertThat(actual.getFirstName(), is(equalTo(TEST_PERSON.getFirstName()))); + Assert.assertThat(actual.getLastName(), is(equalTo(TEST_PERSON.getLastName()))); + }).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } + + @Test + public void testFindAll() { + final Flux flux = cosmosTemplate.findAll(Person.class.getSimpleName(), + Person.class); + StepVerifier.create(flux).expectNextCount(1).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } + + @Test + public void testFindByIdWithContainerName() { + StepVerifier.create(cosmosTemplate.findById(Person.class.getSimpleName(), + TEST_PERSON.getId(), Person.class)) + .consumeNextWith(actual -> Assert.assertEquals(actual, TEST_PERSON)) + .verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } + + @Test + public void testInsert() { + StepVerifier.create(cosmosTemplate.insert(TEST_PERSON_3, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_3)))) + .expectNext(TEST_PERSON_3).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseDiagnostics()).isNotNull(); + } + + @Test + public void testInsertBySecondaryKey() { + cosmosKeyCredential.key(cosmosDbSecondaryKey); + StepVerifier.create(cosmosTemplate.insert(TEST_PERSON_3, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_3)))) + .expectNext(TEST_PERSON_3).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseDiagnostics()).isNotNull(); + } + + @Test + public void testInsertWithContainerName() { + StepVerifier.create(cosmosTemplate.insert(Person.class.getSimpleName(), TEST_PERSON_2, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2)))) + .expectNext(TEST_PERSON_2).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseDiagnostics()).isNotNull(); + } + + @Test + public void testUpsert() { + final Person p = TEST_PERSON_2; + p.set_etag(insertedPerson.get_etag()); + final ArrayList hobbies = new ArrayList<>(p.getHobbies()); + hobbies.add("more code"); + p.setHobbies(hobbies); + final Mono upsert = cosmosTemplate.upsert(p, + new PartitionKey(personInfo.getPartitionKeyFieldValue(p))); + StepVerifier.create(upsert).expectNextCount(1).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseDiagnostics()).isNotNull(); + } + + @Test + public void testOptimisticLockWhenUpdatingWithWrongEtag() { + final Person updated = new Person(TEST_PERSON.getId(), UPDATED_FIRST_NAME, + TEST_PERSON.getLastName(), TEST_PERSON.getHobbies(), TEST_PERSON.getShippingAddresses()); + updated.set_etag(WRONG_ETAG); + + try { + cosmosTemplate.upsert(updated, new PartitionKey(personInfo.getPartitionKeyFieldValue(updated))).block(); + } catch (CosmosDBAccessException cosmosDbAccessException) { + assertThat(cosmosDbAccessException.getCosmosClientException()).isNotNull(); + final Throwable cosmosClientException = cosmosDbAccessException.getCosmosClientException(); + assertThat(cosmosClientException).isInstanceOf(CosmosClientException.class); + assertThat(cosmosClientException.getMessage()).contains(PRECONDITION_IS_NOT_MET); + + final Mono unmodifiedPerson = cosmosTemplate.findById(Person.class.getSimpleName(), + TEST_PERSON.getId(), Person.class); + StepVerifier.create(unmodifiedPerson).expectNextMatches(person -> + person.getFirstName().equals(insertedPerson.getFirstName())).verifyComplete(); + return; + } + fail(); + } + + @Test + public void testUpsertBySecondaryKey() { + cosmosKeyCredential.key(cosmosDbSecondaryKey); + final Person p = TEST_PERSON_2; + final ArrayList hobbies = new ArrayList<>(p.getHobbies()); + hobbies.add("more code"); + p.setHobbies(hobbies); + final Mono upsert = cosmosTemplate.upsert(p, + new PartitionKey(personInfo.getPartitionKeyFieldValue(p))); + StepVerifier.create(upsert).expectNextCount(1).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseDiagnostics()).isNotNull(); + } + + @Test + public void testUpsertWithContainerName() { + final Person p = TEST_PERSON_2; + final ArrayList hobbies = new ArrayList<>(p.getHobbies()); + hobbies.add("more code"); + p.setHobbies(hobbies); + final Mono upsert = cosmosTemplate.upsert(Person.class.getSimpleName(), p, + new PartitionKey(personInfo.getPartitionKeyFieldValue(p))); + StepVerifier.create(upsert).expectNextCount(1).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseDiagnostics()).isNotNull(); + } + + @Test + public void testDeleteById() { + cosmosTemplate.insert(TEST_PERSON_4, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_4))).block(); + + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseDiagnostics()).isNotNull(); + + Flux flux = cosmosTemplate.findAll(Person.class.getSimpleName(), Person.class); + StepVerifier.create(flux).expectNextCount(2).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseDiagnostics()).isNull(); + + final Mono voidMono = cosmosTemplate.deleteById(Person.class.getSimpleName(), + TEST_PERSON_4.getId(), + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_4))); + StepVerifier.create(voidMono).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseDiagnostics()).isNotNull(); + + flux = cosmosTemplate.findAll(Person.class.getSimpleName(), Person.class); + StepVerifier.create(flux).expectNextCount(1).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseDiagnostics()).isNull(); + } + + @Test + public void testDeleteByIdBySecondaryKey() { + cosmosKeyCredential.key(cosmosDbSecondaryKey); + cosmosTemplate.insert(TEST_PERSON_4, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_4))).block(); + Flux flux = cosmosTemplate.findAll(Person.class.getSimpleName(), Person.class); + StepVerifier.create(flux).expectNextCount(2).verifyComplete(); + final Mono voidMono = cosmosTemplate.deleteById(Person.class.getSimpleName(), + TEST_PERSON_4.getId(), + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_4))); + StepVerifier.create(voidMono).verifyComplete(); + flux = cosmosTemplate.findAll(Person.class.getSimpleName(), Person.class); + StepVerifier.create(flux).expectNextCount(1).verifyComplete(); + } + + @Test + public void testFind() { + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Arrays.asList(TEST_PERSON.getFirstName())); + final DocumentQuery query = new DocumentQuery(criteria); + final Flux personFlux = cosmosTemplate.find(query, Person.class, + Person.class.getSimpleName()); + StepVerifier.create(personFlux).expectNextCount(1).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseDiagnostics()).isNull(); + } + + @Test + public void testExists() { + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Arrays.asList(TEST_PERSON.getFirstName())); + final DocumentQuery query = new DocumentQuery(criteria); + final Mono exists = cosmosTemplate.exists(query, Person.class, containerName); + StepVerifier.create(exists).expectNext(true).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseDiagnostics()).isNull(); + } + + @Test + public void testCount() { + final Mono count = cosmosTemplate.count(containerName); + StepVerifier.create(count).expectNext((long) 1).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getFeedResponseDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseDiagnostics()).isNull(); + } + + @Test + public void testCountBySecondaryKey() { + cosmosKeyCredential.key(cosmosDbSecondaryKey); + final Mono count = cosmosTemplate.count(containerName); + StepVerifier.create(count).expectNext((long) 1).verifyComplete(); + } + + @Test + public void testInvalidSecondaryKey() { + cosmosKeyCredential.key("Invalid secondary key"); + final Mono findById = cosmosTemplate.findById(Person.class.getSimpleName(), + TEST_PERSON.getId(), + Person.class); + StepVerifier.create(findById).expectError(IllegalArgumentException.class); + } + +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/ReactiveCosmosTemplatePartitionIT.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/ReactiveCosmosTemplatePartitionIT.java new file mode 100644 index 000000000000..fce527da71b0 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/ReactiveCosmosTemplatePartitionIT.java @@ -0,0 +1,204 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.core; + +import com.azure.data.cosmos.PartitionKey; +import com.microsoft.azure.spring.data.cosmosdb.CosmosDbFactory; +import com.microsoft.azure.spring.data.cosmosdb.config.CosmosDBConfig; +import com.microsoft.azure.spring.data.cosmosdb.core.convert.MappingCosmosConverter; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.CosmosMappingContext; +import com.microsoft.azure.spring.data.cosmosdb.core.query.Criteria; +import com.microsoft.azure.spring.data.cosmosdb.core.query.DocumentQuery; +import com.microsoft.azure.spring.data.cosmosdb.domain.PartitionPerson; +import com.microsoft.azure.spring.data.cosmosdb.repository.TestRepositoryConfig; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.domain.EntityScanner; +import org.springframework.context.ApplicationContext; +import org.springframework.data.annotation.Persistent; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.Arrays; +import java.util.UUID; + +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.ADDRESSES; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.FIRST_NAME; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.HOBBIES; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.ID_1; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.ID_2; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.LAST_NAME; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.NEW_FIRST_NAME; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.NEW_LAST_NAME; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.PROPERTY_LAST_NAME; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.UPDATED_FIRST_NAME; +import static com.microsoft.azure.spring.data.cosmosdb.core.query.CriteriaType.IS_EQUAL; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertTrue; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class ReactiveCosmosTemplatePartitionIT { + private static final PartitionPerson TEST_PERSON = new PartitionPerson(ID_1, FIRST_NAME, LAST_NAME, + HOBBIES, ADDRESSES); + + private static final PartitionPerson TEST_PERSON_2 = new PartitionPerson(ID_2, NEW_FIRST_NAME, + TEST_PERSON.getLastName(), HOBBIES, ADDRESSES); + + private static ReactiveCosmosTemplate cosmosTemplate; + private static String containerName; + private static CosmosEntityInformation personInfo; + + private static boolean initialized; + + @Autowired + private ApplicationContext applicationContext; + @Autowired + private CosmosDBConfig dbConfig; + + @Before + public void setUp() throws ClassNotFoundException { + if (!initialized) { + final CosmosDbFactory dbFactory = new CosmosDbFactory(dbConfig); + + final CosmosMappingContext mappingContext = new CosmosMappingContext(); + personInfo = + new CosmosEntityInformation<>(PartitionPerson.class); + containerName = personInfo.getContainerName(); + + mappingContext.setInitialEntitySet(new EntityScanner(this.applicationContext).scan(Persistent.class)); + + final MappingCosmosConverter dbConverter = new MappingCosmosConverter(mappingContext, + null); + cosmosTemplate = new ReactiveCosmosTemplate(dbFactory, dbConverter, dbConfig.getDatabase()); + cosmosTemplate.createContainerIfNotExists(personInfo).block(); + + initialized = true; + } + cosmosTemplate.insert(TEST_PERSON).block(); + } + + @After + public void cleanup() { + cosmosTemplate.deleteAll(PartitionPerson.class.getSimpleName(), + personInfo.getPartitionKeyFieldName()).block(); + } + + @AfterClass + public static void afterClassCleanup() { + cosmosTemplate.deleteContainer(personInfo.getContainerName()); + } + + @Test + public void testFindWithPartition() { + final Criteria criteria = Criteria.getInstance(IS_EQUAL, PROPERTY_LAST_NAME, + Arrays.asList(LAST_NAME)); + final DocumentQuery query = new DocumentQuery(criteria); + final Flux partitionPersonFlux = cosmosTemplate.find(query, + PartitionPerson.class, + PartitionPerson.class.getSimpleName()); + StepVerifier.create(partitionPersonFlux).consumeNextWith(actual -> { + Assert.assertThat(actual.getFirstName(), is(equalTo(TEST_PERSON.getFirstName()))); + Assert.assertThat(actual.getLastName(), is(equalTo(TEST_PERSON.getLastName()))); + }).verifyComplete(); + } + + + @Test + public void testFindByIdWithPartition() { + final Mono partitionPersonMono = cosmosTemplate.findById(TEST_PERSON.getId(), + PartitionPerson.class, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON))); + StepVerifier.create(partitionPersonMono).consumeNextWith(actual -> { + Assert.assertThat(actual.getFirstName(), is(equalTo(TEST_PERSON.getFirstName()))); + Assert.assertThat(actual.getLastName(), is(equalTo(TEST_PERSON.getLastName()))); + }).verifyComplete(); + } + + // @Test + // public void testFindByNonExistIdWithPartition() { + // + // } + + @Test + public void testUpsertNewDocumentPartition() { + final String firstName = NEW_FIRST_NAME + "_" + UUID.randomUUID().toString(); + final PartitionPerson newPerson = new PartitionPerson(UUID.randomUUID().toString(), + firstName, NEW_LAST_NAME, + null, null); + final String partitionKeyValue = newPerson.getLastName(); + final Mono upsert = cosmosTemplate.upsert(newPerson, + new PartitionKey(partitionKeyValue)); + StepVerifier.create(upsert).expectNextCount(1).verifyComplete(); + } + + @Test + public void testUpdateWithPartition() { + final PartitionPerson updated = new PartitionPerson(TEST_PERSON.getId(), UPDATED_FIRST_NAME, + TEST_PERSON.getLastName(), TEST_PERSON.getHobbies(), + TEST_PERSON.getShippingAddresses()); + cosmosTemplate.upsert(updated, new PartitionKey(updated.getLastName())).block(); + + final PartitionPerson person = cosmosTemplate + .findAll(PartitionPerson.class.getSimpleName(), PartitionPerson.class) + .toStream() + .filter(p -> TEST_PERSON.getId().equals(p.getId())) + .findFirst().get(); + assertTrue(person.equals(updated)); + } + + @Test + public void testDeleteByIdPartition() { + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(TEST_PERSON_2.getLastName())).block(); + StepVerifier.create(cosmosTemplate.findAll(PartitionPerson.class)).expectNextCount(2).verifyComplete(); + + cosmosTemplate.deleteById(PartitionPerson.class.getSimpleName(), + TEST_PERSON.getId(), new PartitionKey(TEST_PERSON.getLastName())).block(); + StepVerifier.create(cosmosTemplate.findAll(PartitionPerson.class)) + .expectNext(TEST_PERSON_2) + .verifyComplete(); + } + + @Test + public void testDeleteAll() { + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(TEST_PERSON_2.getLastName())).block(); + StepVerifier.create(cosmosTemplate.findAll(PartitionPerson.class)).expectNextCount(2).verifyComplete(); + final CosmosEntityInformation personInfo = + new CosmosEntityInformation<>(PartitionPerson.class); + cosmosTemplate.deleteAll(containerName, personInfo.getPartitionKeyFieldName()).block(); + StepVerifier.create(cosmosTemplate.findAll(PartitionPerson.class)) + .expectNextCount(0) + .verifyComplete(); + } + + @Test + public void testCountForPartitionedCollection() { + StepVerifier.create(cosmosTemplate.count(containerName)) + .expectNext((long) 1).verifyComplete(); + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(TEST_PERSON_2.getLastName())).block(); + StepVerifier.create(cosmosTemplate.count(containerName)) + .expectNext((long) 2).verifyComplete(); + } + + @Test + public void testCountForPartitionedCollectionByQuery() { + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(TEST_PERSON_2.getLastName())).block(); + final Criteria criteria = Criteria.getInstance(IS_EQUAL, "firstName", + Arrays.asList(TEST_PERSON_2.getFirstName())); + final DocumentQuery query = new DocumentQuery(criteria); + StepVerifier.create(cosmosTemplate.count(query, containerName)) + .expectNext((long) 1).verifyComplete(); + + } +} + diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/convert/ZonedDateTimeDeserializerTest.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/convert/ZonedDateTimeDeserializerTest.java new file mode 100644 index 000000000000..dc7622c948a4 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/convert/ZonedDateTimeDeserializerTest.java @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.core.convert; + +import org.junit.Test; + +import java.io.IOException; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME; +import static org.assertj.core.api.Java6Assertions.assertThat; + + +public class ZonedDateTimeDeserializerTest { + private static final ZonedDateTime ZONED_DATE_TIME + = ZonedDateTime.of(2018, 10, 8, 15, 6, 7, 992000000, + ZoneId.of("UTC")); + private static final String OFFSET_DATE_TIME_WRAPPER_JSON = "{ \"zonedDateTime\": \"" + + ZONED_DATE_TIME.format(ISO_OFFSET_DATE_TIME) + "\" }"; + private static final String ZONED_DATE_TIME_WRAPPER_JSON = "{ \"zonedDateTime\": \"" + + ZONED_DATE_TIME.format(ISO_OFFSET_DATE_TIME) + "\" }"; + + @Test + public void deserializeZonedDateTime() throws IOException { + final ZonedDateTimeWrapper wrapper = ObjectMapperFactory.getObjectMapper() + .readValue(ZONED_DATE_TIME_WRAPPER_JSON, ZonedDateTimeWrapper.class); + assertThat(wrapper.getZonedDateTime()).isEqualTo(ZONED_DATE_TIME); + } + + @Test + public void deserializeOffsetDateTime() throws IOException { + final ZonedDateTimeWrapper wrapper = ObjectMapperFactory.getObjectMapper() + .readValue(OFFSET_DATE_TIME_WRAPPER_JSON, ZonedDateTimeWrapper.class); + assertThat(wrapper.getZonedDateTime()).isEqualTo(ZONED_DATE_TIME); + } + + static final class ZonedDateTimeWrapper { + ZonedDateTime zonedDateTime; + + public ZonedDateTime getZonedDateTime() { + return zonedDateTime; + } + + public void setZonedDateTime(ZonedDateTime zonedDateTime) { + this.zonedDateTime = zonedDateTime; + } + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/converter/MappingCosmosConverterUnitTest.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/converter/MappingCosmosConverterUnitTest.java new file mode 100644 index 000000000000..c0d3c93947ae --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/converter/MappingCosmosConverterUnitTest.java @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.core.converter; + +import com.azure.data.cosmos.CosmosItemProperties; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.azure.spring.data.cosmosdb.common.TestConstants; +import com.microsoft.azure.spring.data.cosmosdb.core.convert.MappingCosmosConverter; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.CosmosMappingContext; +import com.microsoft.azure.spring.data.cosmosdb.domain.Address; +import com.microsoft.azure.spring.data.cosmosdb.domain.Memo; +import com.microsoft.azure.spring.data.cosmosdb.domain.Importance; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.context.ApplicationContext; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(MockitoJUnitRunner.class) +public class MappingCosmosConverterUnitTest { + private static final SimpleDateFormat DATE = new SimpleDateFormat(TestConstants.DATE_FORMAT); + private static final SimpleDateFormat TIMEZONE_DATE = new SimpleDateFormat(TestConstants.DATE_TIMEZONE_FORMAT); + + private MappingCosmosConverter mappingCosmosConverter; + + @Mock + ApplicationContext applicationContext; + + @Before + public void setUp() { + final CosmosMappingContext mappingContext = new CosmosMappingContext(); + final ObjectMapper objectMapper = new ObjectMapper(); + + mappingContext.setApplicationContext(applicationContext); + mappingContext.afterPropertiesSet(); + mappingContext.getPersistentEntity(Address.class); + + mappingCosmosConverter = new MappingCosmosConverter(mappingContext, objectMapper); + } + + @Test + public void covertAddressToDocumentCorrectly() { + final Address testAddress = new Address(TestConstants.POSTAL_CODE, TestConstants.CITY, TestConstants.STREET); + final CosmosItemProperties cosmosItemProperties = mappingCosmosConverter.writeCosmosItemProperties(testAddress); + + assertThat(cosmosItemProperties.id()).isEqualTo(testAddress.getPostalCode()); + assertThat(cosmosItemProperties.getString(TestConstants.PROPERTY_CITY)).isEqualTo(testAddress.getCity()); + assertThat(cosmosItemProperties.getString(TestConstants.PROPERTY_STREET)).isEqualTo(testAddress.getStreet()); + } + + @Test + public void convertDocumentToAddressCorrectly() { + final JSONObject jsonObject = new JSONObject(); + jsonObject.put(TestConstants.PROPERTY_CITY, TestConstants.CITY); + jsonObject.put(TestConstants.PROPERTY_STREET, TestConstants.STREET); + + final CosmosItemProperties cosmosItemProperties = new CosmosItemProperties(jsonObject.toString()); + cosmosItemProperties.id(TestConstants.POSTAL_CODE); + + final Address address = mappingCosmosConverter.read(Address.class, cosmosItemProperties); + + assertThat(address.getPostalCode()).isEqualTo(TestConstants.POSTAL_CODE); + assertThat(address.getCity()).isEqualTo(TestConstants.CITY); + assertThat(address.getStreet()).isEqualTo(TestConstants.STREET); + } + + @Test + public void canWritePojoWithDateToDocument() throws ParseException { + final Memo memo = new Memo(TestConstants.ID_1, TestConstants.MESSAGE, DATE.parse(TestConstants.DATE_STRING), + Importance.NORMAL); + final CosmosItemProperties cosmosItemProperties = mappingCosmosConverter.writeCosmosItemProperties(memo); + + assertThat(cosmosItemProperties.id()).isEqualTo(memo.getId()); + assertThat(cosmosItemProperties.getString(TestConstants.PROPERTY_MESSAGE)).isEqualTo(memo.getMessage()); + assertThat(cosmosItemProperties.getLong(TestConstants.PROPERTY_DATE)).isEqualTo(memo.getDate().getTime()); + } + + @Test + public void canReadPojoWithDateFromDocument() throws ParseException { + final JSONObject jsonObject = new JSONObject(); + jsonObject.put(TestConstants.PROPERTY_MESSAGE, TestConstants.MESSAGE); + + final long date = DATE.parse(TestConstants.DATE_STRING).getTime(); + jsonObject.put(TestConstants.PROPERTY_DATE, date); + + final CosmosItemProperties cosmosItemProperties = new CosmosItemProperties(jsonObject.toString()); + cosmosItemProperties.id(TestConstants.ID_1); + + final Memo memo = mappingCosmosConverter.read(Memo.class, cosmosItemProperties); + assertThat(cosmosItemProperties.id()).isEqualTo(memo.getId()); + assertThat(cosmosItemProperties.getString(TestConstants.PROPERTY_MESSAGE)).isEqualTo(TestConstants.MESSAGE); + assertThat(cosmosItemProperties.getLong(TestConstants.PROPERTY_DATE)).isEqualTo(date); + } + + @Test + public void convertDateValueToMilliSeconds() throws ParseException { + final Date date = TIMEZONE_DATE.parse(TestConstants.DATE_TIMEZONE_STRING); + final long time = (Long) MappingCosmosConverter.toCosmosDbValue(date); + + assertThat(time).isEqualTo(TestConstants.MILLI_SECONDS); + } +} + diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/BasicCosmosPersistentEntityUnitTest.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/BasicCosmosPersistentEntityUnitTest.java new file mode 100644 index 000000000000..8c5282ff1a1f --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/BasicCosmosPersistentEntityUnitTest.java @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.core.mapping; + +import com.microsoft.azure.spring.data.cosmosdb.domain.Person; +import org.junit.Test; +import org.springframework.data.util.ClassTypeInformation; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BasicCosmosPersistentEntityUnitTest { + + @Test + public void testGetCollection() { + final BasicCosmosPersistentEntity entity = new BasicCosmosPersistentEntity( + ClassTypeInformation.from(Person.class)); + assertThat(entity.getContainer()).isEqualTo(""); + } + + @Test + public void testGetLanguage() { + final BasicCosmosPersistentEntity entity = new BasicCosmosPersistentEntity( + ClassTypeInformation.from(Person.class)); + assertThat(entity.getLanguage()).isEqualTo(""); + } + +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/CosmosMappingContextUnitTest.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/CosmosMappingContextUnitTest.java new file mode 100644 index 000000000000..db6fd8cbed87 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/mapping/CosmosMappingContextUnitTest.java @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.core.mapping; + +import org.junit.Test; +import org.mockito.Mock; +import org.springframework.context.ApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CosmosMappingContextUnitTest { + + @Mock + ApplicationContext context; + + @Test + public void mappingContextWithImplicitIdProperty() { + final CosmosMappingContext context = new CosmosMappingContext(); + final BasicCosmosPersistentEntity entity = context.getPersistentEntity(ClassWithId.class); + + assertThat(entity).isNotNull(); + } + + class ClassWithId { + String field; + String id; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/query/CriteriaUnitTest.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/query/CriteriaUnitTest.java new file mode 100644 index 000000000000..7a56dc01436b --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/query/CriteriaUnitTest.java @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.core.query; + +import com.microsoft.azure.spring.data.cosmosdb.core.generator.FindQuerySpecGenerator; +import com.microsoft.azure.spring.data.cosmosdb.exception.IllegalQueryException; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.CRITERIA_KEY; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.CRITERIA_OBJECT; + +public class CriteriaUnitTest { + + @Test + public void testUnaryCriteria() { + final List values = Arrays.asList(CRITERIA_OBJECT); + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, CRITERIA_KEY, values); + + Assert.assertTrue(criteria.getSubCriteria().isEmpty()); + Assert.assertEquals(values, criteria.getSubjectValues()); + Assert.assertEquals(CriteriaType.IS_EQUAL, criteria.getType()); + Assert.assertEquals(CRITERIA_KEY, criteria.getSubject()); + Assert.assertTrue(CriteriaType.isBinary(criteria.getType())); + } + + @Test + public void testBinaryCriteria() { + final List values = Arrays.asList(CRITERIA_OBJECT); + final Criteria leftCriteria = Criteria.getInstance(CriteriaType.IS_EQUAL, CRITERIA_KEY, values); + final Criteria rightCriteria = Criteria.getInstance(CriteriaType.IS_EQUAL, CRITERIA_OBJECT, values); + final Criteria criteria = Criteria.getInstance(CriteriaType.AND, leftCriteria, rightCriteria); + + Assert.assertNotNull(criteria.getSubCriteria()); + Assert.assertNull(criteria.getSubjectValues()); + Assert.assertNull(criteria.getSubject()); + Assert.assertEquals(criteria.getType(), CriteriaType.AND); + Assert.assertTrue(CriteriaType.isClosed(criteria.getType())); + + Assert.assertEquals(2, criteria.getSubCriteria().size()); + Assert.assertEquals(leftCriteria, criteria.getSubCriteria().get(0)); + Assert.assertEquals(rightCriteria, criteria.getSubCriteria().get(1)); + } + + @Test(expected = IllegalQueryException.class) + public void testInvalidInKeywordParameter() { + final List values = Collections.singletonList(CRITERIA_OBJECT); + final Criteria criteria = Criteria.getInstance(CriteriaType.IN, CRITERIA_KEY, values); + final DocumentQuery query = new DocumentQuery(criteria); + + new FindQuerySpecGenerator().generateCosmos(query); + } + + @Test(expected = IllegalQueryException.class) + public void testInvalidInKeywordType() { + final List values = Collections.singletonList(new IllegalQueryException("")); + final Criteria criteria = Criteria.getInstance(CriteriaType.IN, CRITERIA_KEY, values); + final DocumentQuery query = new DocumentQuery(criteria); + + new FindQuerySpecGenerator().generateCosmos(query); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/query/DocumentQueryUnitTest.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/query/DocumentQueryUnitTest.java new file mode 100644 index 000000000000..0f65f3635d0f --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/core/query/DocumentQueryUnitTest.java @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.core.query; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +import java.util.Arrays; + +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.CRITERIA_KEY; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.CRITERIA_OBJECT; + +public class DocumentQueryUnitTest { + + @Test + public void testDocumentQueryCreate() { + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, CRITERIA_KEY, + Arrays.asList(CRITERIA_OBJECT)); + + final DocumentQuery query = new DocumentQuery(criteria); + + Assert.assertEquals(criteria, query.getCriteria()); + Assert.assertEquals(Sort.unsorted(), query.getSort()); + Assert.assertEquals(Pageable.unpaged(), query.getPageable()); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Address.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Address.java new file mode 100644 index 000000000000..3539bc569a90 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Address.java @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.domain; + +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.PartitionKey; +import org.springframework.data.annotation.Id; + +import java.util.Objects; + +@Document() +public class Address { + @Id + String postalCode; + String street; + @PartitionKey + String city; + + public String getPostalCode() { + return postalCode; + } + + public void setPostalCode(String postalCode) { + this.postalCode = postalCode; + } + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final Address address = (Address) o; + return Objects.equals(postalCode, address.postalCode) + && Objects.equals(street, address.street) + && Objects.equals(city, address.city); + } + + @Override + public int hashCode() { + return Objects.hash(postalCode, street, city); + } + + @Override + public String toString() { + return "Address{" + + "postalCode='" + + postalCode + + '\'' + + ", street='" + + street + + '\'' + + ", city='" + + city + + '\'' + + '}'; + } + + public Address(String postalCode, String street, String city) { + this.postalCode = postalCode; + this.street = street; + this.city = city; + } + + public Address() { + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Contact.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Contact.java new file mode 100644 index 000000000000..3c2bfc9c5dd4 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Contact.java @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.domain; + +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document; +import org.springframework.data.annotation.Id; + +import java.util.Objects; + +@Document() +public class Contact { + @Id + private String logicId; + + private String title; + + public Contact(String logicId, String title) { + this.logicId = logicId; + this.title = title; + } + + public String getLogicId() { + return logicId; + } + + public void setLogicId(String logicId) { + this.logicId = logicId; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Contact contact = (Contact) o; + return Objects.equals(logicId, contact.logicId) + && Objects.equals(title, contact.title); + } + + @Override + public int hashCode() { + return Objects.hash(logicId, title); + } + + @Override + public String toString() { + return "Contact{" + + "logicId='" + + logicId + + '\'' + + ", title='" + + title + + '\'' + + '}'; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Course.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Course.java new file mode 100644 index 000000000000..872e92b2dd0f --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Course.java @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.domain; + +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.PartitionKey; +import org.springframework.data.annotation.Id; + +import java.util.Objects; + +@Document +public class Course { + + @Id + private String courseId; + private String name; + @PartitionKey + private String department; + + public Course(String courseId, String name, String department) { + this.courseId = courseId; + this.name = name; + this.department = department; + } + + public Course() { + } + + public String getCourseId() { + return courseId; + } + + public void setCourseId(String courseId) { + this.courseId = courseId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDepartment() { + return department; + } + + public void setDepartment(String department) { + this.department = department; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Course course = (Course) o; + return courseId.equals(course.courseId) + && name.equals(course.name) + && department.equals(course.department); + } + + @Override + public int hashCode() { + return Objects.hash(courseId, name, department); + } + + @Override + public String toString() { + return "Course{" + + "courseId='" + + courseId + + '\'' + + ", name='" + + name + + '\'' + + ", department='" + + department + + '\'' + + '}'; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Customer.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Customer.java new file mode 100644 index 000000000000..e79d5a49b696 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Customer.java @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.domain; + +import org.springframework.data.annotation.Id; + +import java.util.Objects; + +public class Customer { + + @Id + private String id; + + private Long level; + + private User user; + + public Customer(String id, Long level, User user) { + this.id = id; + this.level = level; + this.user = user; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public Long getLevel() { + return level; + } + + public void setLevel(Long level) { + this.level = level; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Customer customer = (Customer) o; + return Objects.equals(id, customer.id) + && Objects.equals(level, customer.level) + && Objects.equals(user, customer.user); + } + + @Override + public int hashCode() { + return Objects.hash(id, level, user); + } + + @Override + public String toString() { + return "Customer{" + + "id='" + + id + + '\'' + + ", level=" + + level + + ", user=" + + user + + '}'; + } + + public static class User { + + private String name; + + private Long age; + + public User(String name, Long age) { + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getAge() { + return age; + } + + public void setAge(Long age) { + this.age = age; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + User user = (User) o; + return Objects.equals(name, user.name) + && Objects.equals(age, user.age); + } + + @Override + public int hashCode() { + return Objects.hash(name, age); + } + + @Override + public String toString() { + return "User{" + + "name='" + + name + + '\'' + + ", age=" + + age + + '}'; + } + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Importance.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Importance.java new file mode 100644 index 000000000000..0362ff9c4a43 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Importance.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.domain; + +public enum Importance { + HIGH, LOW, NORMAL; +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/IntegerIdDomain.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/IntegerIdDomain.java new file mode 100644 index 000000000000..0dc90ea96a1d --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/IntegerIdDomain.java @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.domain; + +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document; +import org.springframework.data.annotation.Id; + +import java.util.Objects; + +@Document +public class IntegerIdDomain { + + @Id + private Integer number; + + private String name; + + public IntegerIdDomain(Integer number, String name) { + this.number = number; + this.name = name; + } + + public IntegerIdDomain() { + } + + public Integer getNumber() { + return number; + } + + public void setNumber(Integer number) { + this.number = number; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + IntegerIdDomain that = (IntegerIdDomain) o; + return Objects.equals(number, that.number) + && Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(number, name); + } + + @Override + public String toString() { + return "IntegerIdDomain{" + + "number=" + + number + + ", name='" + + name + + '\'' + + '}'; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Memo.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Memo.java new file mode 100644 index 000000000000..998b03810ef3 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Memo.java @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.domain; + +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document; + +import java.util.Date; +import java.util.Objects; + +/** + * For testing date and enum purpose + */ +@Document() +public class Memo { + private String id; + private String message; + private Date date; + private Importance importance; + + public Memo(String id, String message, Date date, Importance importance) { + this.id = id; + this.message = message; + this.date = date; + this.importance = importance; + } + + public Memo() { + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + public Importance getImportance() { + return importance; + } + + public void setImportance(Importance importance) { + this.importance = importance; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Memo memo = (Memo) o; + return Objects.equals(id, memo.id) + && Objects.equals(message, memo.message) + && Objects.equals(date, memo.date) + && importance == memo.importance; + } + + @Override + public int hashCode() { + return Objects.hash(id, message, date, importance); + } + + @Override + public String toString() { + return "Memo{" + + "id='" + + id + + '\'' + + ", message='" + + message + + '\'' + + ", date=" + + date + + ", importance=" + + importance + + '}'; + } +} + diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/NoDBAnnotationPerson.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/NoDBAnnotationPerson.java new file mode 100644 index 000000000000..ef76b39d97c2 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/NoDBAnnotationPerson.java @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.domain; + + +import java.util.List; +import java.util.Objects; + +public class NoDBAnnotationPerson { + private String id; + private String firstName; + private String lastName; + private List hobbies; + private List
shippingAddresses; + + public NoDBAnnotationPerson(String id, String firstName, String lastName, List hobbies, List
shippingAddresses) { + this.id = id; + this.firstName = firstName; + this.lastName = lastName; + this.hobbies = hobbies; + this.shippingAddresses = shippingAddresses; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public List getHobbies() { + return hobbies; + } + + public void setHobbies(List hobbies) { + this.hobbies = hobbies; + } + + public List
getShippingAddresses() { + return shippingAddresses; + } + + public void setShippingAddresses(List
shippingAddresses) { + this.shippingAddresses = shippingAddresses; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NoDBAnnotationPerson that = (NoDBAnnotationPerson) o; + return Objects.equals(id, that.id) + && Objects.equals(firstName, that.firstName) + && Objects.equals(lastName, that.lastName) + && Objects.equals(hobbies, that.hobbies) + && Objects.equals(shippingAddresses, that.shippingAddresses); + } + + @Override + public int hashCode() { + return Objects.hash(id, firstName, lastName, hobbies, shippingAddresses); + } + + @Override + public String toString() { + return "NoDBAnnotationPerson{" + + "id='" + + id + + '\'' + + ", firstName='" + + firstName + + '\'' + + ", lastName='" + + lastName + + '\'' + + ", hobbies=" + + hobbies + + ", shippingAddresses=" + + shippingAddresses + + '}'; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/PageableAddress.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/PageableAddress.java new file mode 100644 index 000000000000..947b30baf43c --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/PageableAddress.java @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.domain; + +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.PartitionKey; +import org.springframework.data.annotation.Id; + +import java.util.Objects; + +@Document() +public class PageableAddress { + @Id + private String postalCode; + private String street; + @PartitionKey + private String city; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final PageableAddress address = (PageableAddress) o; + return Objects.equals(postalCode, address.postalCode) + && Objects.equals(street, address.street) + && Objects.equals(city, address.city); + } + + @Override + public int hashCode() { + return Objects.hash(postalCode, street, city); + } + + public PageableAddress(String postalCode, String street, String city) { + this.postalCode = postalCode; + this.street = street; + this.city = city; + } + + public String getPostalCode() { + return postalCode; + } + + public void setPostalCode(String postalCode) { + this.postalCode = postalCode; + } + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + @Override + public String toString() { + return "PageableAddress{" + + "postalCode='" + + postalCode + + '\'' + + ", street='" + + street + + '\'' + + ", city='" + + city + + '\'' + + '}'; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/PageableMemo.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/PageableMemo.java new file mode 100644 index 000000000000..5796f18e8af7 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/PageableMemo.java @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.domain; + +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document; + +import java.util.Date; +import java.util.Objects; + +/** + * For testing date and enum purpose + */ +@Document() +public class PageableMemo { + private String id; + private String message; + private Date date; + private Importance importance; + + public PageableMemo(String id, String message, Date date, Importance importance) { + this.id = id; + this.message = message; + this.date = date; + this.importance = importance; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + public Importance getImportance() { + return importance; + } + + public void setImportance(Importance importance) { + this.importance = importance; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PageableMemo that = (PageableMemo) o; + return Objects.equals(id, that.id) + && Objects.equals(message, that.message) + && Objects.equals(date, that.date) + && importance == that.importance; + } + + @Override + public int hashCode() { + return Objects.hash(id, message, date, importance); + } + + @Override + public String toString() { + return "PageableMemo{" + + "id='" + + id + + '\'' + + ", message='" + + message + + '\'' + + ", date=" + + date + + ", importance=" + + importance + + '}'; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/PageablePerson.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/PageablePerson.java new file mode 100644 index 000000000000..44ac9016bbf9 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/PageablePerson.java @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.domain; + +import com.microsoft.azure.spring.data.cosmosdb.common.TestConstants; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.DocumentIndexingPolicy; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.PartitionKey; +import org.springframework.data.annotation.Version; + +import java.util.List; +import java.util.Objects; + +@Document() +@DocumentIndexingPolicy(includePaths = TestConstants.ORDER_BY_STRING_PATH) +public class PageablePerson { + private String id; + private String firstName; + + @PartitionKey + private String lastName; + private List hobbies; + private List
shippingAddresses; + @Version + private String _etag; + + public PageablePerson(String id, String firstName, String lastName, + List hobbies, List
shippingAddresses) { + this.id = id; + this.firstName = firstName; + this.lastName = lastName; + this.hobbies = hobbies; + this.shippingAddresses = shippingAddresses; + } + + public PageablePerson() { + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public List getHobbies() { + return hobbies; + } + + public void setHobbies(List hobbies) { + this.hobbies = hobbies; + } + + public List
getShippingAddresses() { + return shippingAddresses; + } + + public void setShippingAddresses(List
shippingAddresses) { + this.shippingAddresses = shippingAddresses; + } + + public String get_etag() { + return _etag; + } + + public void set_etag(String _etag) { + this._etag = _etag; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PageablePerson that = (PageablePerson) o; + return Objects.equals(id, that.id) + && Objects.equals(firstName, that.firstName) + && Objects.equals(lastName, that.lastName) + && Objects.equals(hobbies, that.hobbies) + && Objects.equals(shippingAddresses, that.shippingAddresses); + } + + @Override + public int hashCode() { + return Objects.hash(id, firstName, lastName, hobbies, shippingAddresses); + } + + @Override + public String toString() { + return "PageablePerson{" + + "id='" + + id + + '\'' + + ", firstName='" + + firstName + + '\'' + + ", lastName='" + + lastName + + '\'' + + ", hobbies=" + + hobbies + + ", shippingAddresses=" + + shippingAddresses + + ", _etag='" + + _etag + + '\'' + + '}'; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/PartitionPerson.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/PartitionPerson.java new file mode 100644 index 000000000000..fefe9b080a9d --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/PartitionPerson.java @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.domain; + +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.PartitionKey; + +import java.util.List; +import java.util.Objects; + +@Document() +public class PartitionPerson { + + private String id; + + private String firstName; + + @PartitionKey + private String lastName; + + private List hobbies; + + private List
shippingAddresses; + + public PartitionPerson(String id, String firstName, String lastName, List hobbies, List
shippingAddresses) { + this.id = id; + this.firstName = firstName; + this.lastName = lastName; + this.hobbies = hobbies; + this.shippingAddresses = shippingAddresses; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public List getHobbies() { + return hobbies; + } + + public void setHobbies(List hobbies) { + this.hobbies = hobbies; + } + + public List
getShippingAddresses() { + return shippingAddresses; + } + + public void setShippingAddresses(List
shippingAddresses) { + this.shippingAddresses = shippingAddresses; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PartitionPerson that = (PartitionPerson) o; + return Objects.equals(id, that.id) + && Objects.equals(firstName, that.firstName) + && Objects.equals(lastName, that.lastName) + && Objects.equals(hobbies, that.hobbies) + && Objects.equals(shippingAddresses, that.shippingAddresses); + } + + @Override + public int hashCode() { + return Objects.hash(id, firstName, lastName, hobbies, shippingAddresses); + } + + @Override + public String toString() { + return "PartitionPerson{" + + "id='" + + id + + '\'' + + ", firstName='" + + firstName + + '\'' + + ", lastName='" + + lastName + + '\'' + + ", hobbies=" + + hobbies + + ", shippingAddresses=" + + shippingAddresses + + '}'; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Person.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Person.java new file mode 100644 index 000000000000..8038c1826df0 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Person.java @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.domain; + +import java.util.List; +import java.util.Objects; + +import com.microsoft.azure.spring.data.cosmosdb.common.TestConstants; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.DocumentIndexingPolicy; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.PartitionKey; + +import org.springframework.data.annotation.Version; + +@Document() +@DocumentIndexingPolicy(includePaths = TestConstants.ORDER_BY_STRING_PATH) +public class Person { + private String id; + private String firstName; + + @PartitionKey + private String lastName; + private List hobbies; + private List
shippingAddresses; + @Version + private String _etag; + + public Person(String id, String firstName, String lastName, List hobbies, List
shippingAddresses) { + this.id = id; + this.firstName = firstName; + this.lastName = lastName; + this.hobbies = hobbies; + this.shippingAddresses = shippingAddresses; + } + + public Person() { + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public List getHobbies() { + return hobbies; + } + + public void setHobbies(List hobbies) { + this.hobbies = hobbies; + } + + public List
getShippingAddresses() { + return shippingAddresses; + } + + public void setShippingAddresses(List
shippingAddresses) { + this.shippingAddresses = shippingAddresses; + } + + public String get_etag() { + return _etag; + } + + public void set_etag(String _etag) { + this._etag = _etag; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Person person = (Person) o; + return Objects.equals(id, person.id) + && Objects.equals(firstName, person.firstName) + && Objects.equals(lastName, person.lastName) + && Objects.equals(hobbies, person.hobbies) + && Objects.equals(shippingAddresses, person.shippingAddresses); + } + + @Override + public int hashCode() { + return Objects.hash(id, firstName, lastName, hobbies, shippingAddresses); + } + + @Override + public String toString() { + return "Person{" + + "id='" + + id + + '\'' + + ", firstName='" + + firstName + + '\'' + + ", lastName='" + + lastName + + '\'' + + ", hobbies=" + + hobbies + + ", shippingAddresses=" + + shippingAddresses + + ", _etag='" + + _etag + + '\'' + + '}'; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Project.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Project.java new file mode 100644 index 000000000000..436b040198fa --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Project.java @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.domain; + +import com.microsoft.azure.spring.data.cosmosdb.common.TestConstants; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.DocumentIndexingPolicy; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.PartitionKey; +import org.springframework.data.annotation.Id; + +import java.util.Objects; + +@Document() +@DocumentIndexingPolicy(includePaths = TestConstants.ORDER_BY_STRING_PATH) +public class Project { + + @Id + private String id; + + private String name; + + @PartitionKey + private String creator; + + private Boolean hasReleased; + + private Long starCount; + + private Long forkCount; + + public Project(String id, String name, String creator, Boolean hasReleased, Long starCount, Long forkCount) { + this.id = id; + this.name = name; + this.creator = creator; + this.hasReleased = hasReleased; + this.starCount = starCount; + this.forkCount = forkCount; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCreator() { + return creator; + } + + public void setCreator(String creator) { + this.creator = creator; + } + + public Boolean getHasReleased() { + return hasReleased; + } + + public void setHasReleased(Boolean hasReleased) { + this.hasReleased = hasReleased; + } + + public Long getStarCount() { + return starCount; + } + + public void setStarCount(Long starCount) { + this.starCount = starCount; + } + + public Long getForkCount() { + return forkCount; + } + + public void setForkCount(Long forkCount) { + this.forkCount = forkCount; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Project project = (Project) o; + return Objects.equals(id, project.id) + && Objects.equals(name, project.name) + && Objects.equals(creator, project.creator) + && Objects.equals(hasReleased, project.hasReleased) + && Objects.equals(starCount, project.starCount) + && Objects.equals(forkCount, project.forkCount); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, creator, hasReleased, starCount, forkCount); + } + + @Override + public String toString() { + return "Project{" + + "id='" + + id + + '\'' + + ", name='" + + name + + '\'' + + ", creator='" + + creator + + '\'' + + ", hasReleased=" + + hasReleased + + ", starCount=" + + starCount + + ", forkCount=" + + forkCount + + '}'; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Question.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Question.java new file mode 100644 index 000000000000..541b32fab8f0 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Question.java @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.domain; + +import com.azure.data.cosmos.IndexingMode; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.DocumentIndexingPolicy; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.PartitionKey; +import org.springframework.data.annotation.Id; + +import java.util.Objects; +import java.util.UUID; + +@Document +@DocumentIndexingPolicy(mode = IndexingMode.LAZY) +public class Question { + + @Id + @PartitionKey + private String id = UUID.randomUUID().toString(); + + private String url; + + public Question(String id, String url) { + this.id = id; + this.url = url; + } + + public Question() { + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Question question = (Question) o; + return Objects.equals(id, question.id) + && Objects.equals(url, question.url); + } + + @Override + public int hashCode() { + return Objects.hash(id, url); + } + + @Override + public String toString() { + return "Question{" + + "id='" + + id + + '\'' + + ", url='" + + url + + '\'' + + '}'; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Role.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Role.java new file mode 100644 index 000000000000..6bc7ca4233d0 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Role.java @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.domain; + +import com.azure.data.cosmos.IndexingMode; +import com.microsoft.azure.spring.data.cosmosdb.common.TestConstants; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.DocumentIndexingPolicy; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.PartitionKey; +import org.springframework.data.annotation.Id; + +import java.util.Objects; + +@DocumentIndexingPolicy( + mode = IndexingMode.LAZY, + automatic = TestConstants.INDEXINGPOLICY_AUTOMATIC, + includePaths = { + TestConstants.INCLUDEDPATH_0, + TestConstants.INCLUDEDPATH_1, + TestConstants.INCLUDEDPATH_2, + }, + excludePaths = { + TestConstants.EXCLUDEDPATH_0, + TestConstants.EXCLUDEDPATH_1, + }) +@Document(collection = TestConstants.ROLE_COLLECTION_NAME, + autoCreateCollection = false) +public class Role { + @Id + String id; + + @PartitionKey + String name; + + String level; + + public Role(String id, String name, String level) { + this.id = id; + this.name = name; + this.level = level; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getLevel() { + return level; + } + + public void setLevel(String level) { + this.level = level; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Role role = (Role) o; + return Objects.equals(id, role.id) + && Objects.equals(name, role.name) + && Objects.equals(level, role.level); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, level); + } + + @Override + public String toString() { + return "Role{" + + "id='" + + id + + '\'' + + ", name='" + + name + + '\'' + + ", level='" + + level + + '\'' + + '}'; + } +} + diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/SortedProject.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/SortedProject.java new file mode 100644 index 000000000000..38437509175d --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/SortedProject.java @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.domain; + +import com.microsoft.azure.spring.data.cosmosdb.common.TestConstants; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.DocumentIndexingPolicy; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.PartitionKey; +import org.springframework.data.annotation.Id; + +import java.util.Objects; + +@Document() +@DocumentIndexingPolicy(includePaths = TestConstants.ORDER_BY_STRING_PATH) +public class SortedProject { + + @Id + private String id; + + private String name; + + @PartitionKey + private String creator; + + private Boolean hasReleased; + + private Long starCount; + + private Long forkCount; + + public SortedProject(String id, String name, String creator, Boolean hasReleased, Long starCount, Long forkCount) { + this.id = id; + this.name = name; + this.creator = creator; + this.hasReleased = hasReleased; + this.starCount = starCount; + this.forkCount = forkCount; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCreator() { + return creator; + } + + public void setCreator(String creator) { + this.creator = creator; + } + + public Boolean getHasReleased() { + return hasReleased; + } + + public void setHasReleased(Boolean hasReleased) { + this.hasReleased = hasReleased; + } + + public Long getStarCount() { + return starCount; + } + + public void setStarCount(Long starCount) { + this.starCount = starCount; + } + + public Long getForkCount() { + return forkCount; + } + + public void setForkCount(Long forkCount) { + this.forkCount = forkCount; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SortedProject that = (SortedProject) o; + return Objects.equals(id, that.id) + && Objects.equals(name, that.name) + && Objects.equals(creator, that.creator) + && Objects.equals(hasReleased, that.hasReleased) + && Objects.equals(starCount, that.starCount) + && Objects.equals(forkCount, that.forkCount); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, creator, hasReleased, starCount, forkCount); + } + + @Override + public String toString() { + return "SortedProject{" + + "id='" + + id + + '\'' + + ", name='" + + name + + '\'' + + ", creator='" + + creator + + '\'' + + ", hasReleased=" + + hasReleased + + ", starCount=" + + starCount + + ", forkCount=" + + forkCount + + '}'; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/SpELBeanStudent.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/SpELBeanStudent.java new file mode 100644 index 000000000000..eb698245156f --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/SpELBeanStudent.java @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.domain; + +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document; + +import java.util.Objects; + +@Document(collection = "#{@dynamicContainer.getContainerName()}") +public class SpELBeanStudent { + private String id; + private String firstName; + private String lastName; + + public SpELBeanStudent(String id, String firstName, String lastName) { + this.id = id; + this.firstName = firstName; + this.lastName = lastName; + } + + public SpELBeanStudent() { + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SpELBeanStudent that = (SpELBeanStudent) o; + return Objects.equals(id, that.id) + && Objects.equals(firstName, that.firstName) + && Objects.equals(lastName, that.lastName); + } + + @Override + public int hashCode() { + return Objects.hash(id, firstName, lastName); + } + + @Override + public String toString() { + return "SpELBeanStudent{" + + "id='" + + id + + '\'' + + ", firstName='" + + firstName + + '\'' + + ", lastName='" + + lastName + + '\'' + + '}'; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/SpELPropertyStudent.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/SpELPropertyStudent.java new file mode 100644 index 000000000000..83daf0e542d2 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/SpELPropertyStudent.java @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.domain; + +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document; + +import java.util.Objects; + +@Document(collection = "${dynamic.collection.name}") +public class SpELPropertyStudent { + private String id; + private String firstName; + private String lastName; + + public SpELPropertyStudent(String id, String firstName, String lastName) { + this.id = id; + this.firstName = firstName; + this.lastName = lastName; + } + + public SpELPropertyStudent() { + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SpELPropertyStudent that = (SpELPropertyStudent) o; + return Objects.equals(id, that.id) + && Objects.equals(firstName, that.firstName) + && Objects.equals(lastName, that.lastName); + } + + @Override + public int hashCode() { + return Objects.hash(id, firstName, lastName); + } + + @Override + public String toString() { + return "SpELPropertyStudent{" + + "id='" + + id + + '\'' + + ", firstName='" + + firstName + + '\'' + + ", lastName='" + + lastName + + '\'' + + '}'; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Student.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Student.java new file mode 100644 index 000000000000..6e8057275916 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/Student.java @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.domain; + +import com.microsoft.azure.spring.data.cosmosdb.common.TestConstants; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.DocumentIndexingPolicy; + +import java.util.Objects; + +@DocumentIndexingPolicy(includePaths = TestConstants.STARTSWITH_INCLUDEDPATH) +public class Student { + private String id; + private String firstName; + private String lastName; + + public Student(String id, String firstName, String lastName) { + this.id = id; + this.firstName = firstName; + this.lastName = lastName; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Student student = (Student) o; + return Objects.equals(id, student.id) + && Objects.equals(firstName, student.firstName) + && Objects.equals(lastName, student.lastName); + } + + @Override + public int hashCode() { + return Objects.hash(id, firstName, lastName); + } + + @Override + public String toString() { + return "Student{" + + "id='" + + id + + '\'' + + ", firstName='" + + firstName + + '\'' + + ", lastName='" + + lastName + + '\'' + + '}'; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/TimeToLiveSample.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/TimeToLiveSample.java new file mode 100644 index 000000000000..3a6093ec6b6f --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/TimeToLiveSample.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.domain; + +import com.microsoft.azure.spring.data.cosmosdb.common.TestConstants; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document; + +@Document(timeToLive = TestConstants.TIME_TO_LIVE) +public class TimeToLiveSample { + private String id; + + public TimeToLiveSample(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/inheritance/Shape.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/inheritance/Shape.java new file mode 100644 index 000000000000..5166e51fb51f --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/inheritance/Shape.java @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.domain.inheritance; + +import java.util.Objects; + +public abstract class Shape { + int area; + + public int getArea() { + return area; + } + + public void setArea(int area) { + this.area = area; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Shape shape = (Shape) o; + return area == shape.area; + } + + @Override + public int hashCode() { + return Objects.hash(area); + } + + @Override + public String toString() { + return "Shape{" + + "area=" + + area + + '}'; + } + + public Shape(int area) { + this.area = area; + } + + public Shape() { + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/inheritance/Square.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/inheritance/Square.java new file mode 100644 index 000000000000..e82e168f8fc6 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/domain/inheritance/Square.java @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.domain.inheritance; + +import org.springframework.data.annotation.Id; + +import java.util.Objects; + +public class Square extends Shape { + @Id + private String id; + + private int length; + + public Square(String id, int length, int area) { + this.id = id; + this.length = length; + this.area = area; + } + + public Square() { + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public int getLength() { + return length; + } + + public void setLength(int length) { + this.length = length; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + Square square = (Square) o; + return length == square.length + && Objects.equals(id, square.id); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), id, length); + } + + @Override + public String toString() { + return "Square{" + + "id='" + + id + + '\'' + + ", length=" + + length + + '}'; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/PerfConfiguration.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/PerfConfiguration.java new file mode 100644 index 000000000000..d893bbc89282 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/PerfConfiguration.java @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.performance; + +import com.microsoft.azure.spring.data.cosmosdb.config.AbstractCosmosConfiguration; +import com.microsoft.azure.spring.data.cosmosdb.config.CosmosDBConfig; +import com.microsoft.azure.spring.data.cosmosdb.performance.utils.Constants; +import com.microsoft.azure.spring.data.cosmosdb.repository.config.EnableCosmosRepositories; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +@Configuration +@PropertySource(value = {"classpath:application.properties"}) +@EnableCosmosRepositories +public class PerfConfiguration extends AbstractCosmosConfiguration { + @Value("${cosmosdb.uri:}") + private String cosmosDbUri; + + @Value("${cosmosdb.key:}") + private String cosmosDbKey; + + @Bean + public CosmosDBConfig getConfig() { + return CosmosDBConfig.builder(cosmosDbUri, cosmosDbKey, Constants.PERF_DATABASE_NAME).build(); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/PerformanceCompare.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/PerformanceCompare.java new file mode 100644 index 000000000000..97d7698ecf54 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/PerformanceCompare.java @@ -0,0 +1,289 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.performance; + +import com.azure.data.cosmos.CosmosClient; +import com.azure.data.cosmos.CosmosClientException; +import com.azure.data.cosmos.sync.CosmosSyncClient; +import com.microsoft.azure.spring.data.cosmosdb.core.query.CosmosPageRequest; +import com.microsoft.azure.spring.data.cosmosdb.performance.domain.PerfPerson; +import com.microsoft.azure.spring.data.cosmosdb.performance.repository.PerfPersonRepository; +import com.microsoft.azure.spring.data.cosmosdb.performance.service.SdkService; +import com.microsoft.azure.spring.data.cosmosdb.performance.utils.Constants; +import com.microsoft.azure.spring.data.cosmosdb.performance.utils.DatabaseUtils; +import com.microsoft.azure.spring.data.cosmosdb.performance.utils.PerfDataProvider; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static com.microsoft.azure.spring.data.cosmosdb.performance.utils.FunctionUtils.*; +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = PerfConfiguration.class) +public class PerformanceCompare { + + private static final Logger LOGGER = LoggerFactory.getLogger(PerformanceCompare.class); + + @Value("${perf.recursive.times:20}") + private int recurTimes; + + @Value("${perf.batch.size:5}") + private int batchSize; + + @Value("${perf.acceptance.percentage:15}") + private int acceptanceDiffPercentage; + + private float acceptanceDiff; + + @Autowired + private CosmosSyncClient cosmosSyncClient; + + @Autowired + private CosmosClient asyncClient; + + @Autowired + private PerfPersonRepository repository; + + private static boolean hasInit = false; + private static SdkService sdkService; + private static PerformanceReport report = new PerformanceReport(); + + @Before + public void setUp() throws CosmosClientException { + if (!hasInit) { + DatabaseUtils.createDatabase(cosmosSyncClient, Constants.PERF_DATABASE_NAME); + DatabaseUtils.createContainer(cosmosSyncClient, Constants.PERF_DATABASE_NAME, + Constants.SPRING_COLLECTION_NAME); + DatabaseUtils.createContainer(cosmosSyncClient, + Constants.PERF_DATABASE_NAME, Constants.SDK_COLLECTION_NAME); + + sdkService = new SdkService(cosmosSyncClient, Constants.PERF_DATABASE_NAME, + Constants.SDK_COLLECTION_NAME, asyncClient); + hasInit = true; + } + + acceptanceDiff = (float) acceptanceDiffPercentage / 100; + LOGGER.info("Running performance test for {} time(s), batch size {} and acceptance diff {}.", + recurTimes, batchSize, acceptanceDiff); + } + + @After + public void clear() { + repository.deleteAll(); + sdkService.deleteAll(); + } + + @AfterClass + public static void printReport() { + report.getPerfItems().forEach(System.out::println); + } + + @Test + public void saveOneRecordTest() { + final List personList = PerfDataProvider.getPerfData(recurTimes); + + final long springCost = applyInputListFunc(personList, repository::save); + final long sdkCost = applyInputListFunc(personList, sdkService::save); + + verifyResult(OperationType.SAVE_ONE, springCost, sdkCost, acceptanceDiff); + } + + @Test + public void saveMultipleRecordsTest() { + final List> personList = PerfDataProvider.getMultiPerfData(batchSize, recurTimes); + + final long springCost = acceptInputListFunc(personList, repository::saveAll); + final long sdkCost = acceptInputListFunc(personList, sdkService::saveAll); + + verifyResult(OperationType.SAVE_ALL, springCost, sdkCost, acceptanceDiff); + } + + @Test + public void deleteOneRecordTest() { + final List personList = prepareListData(recurTimes); + + final long springCost = acceptInputListFunc(personList, repository::delete); + final long sdkCost = acceptInputListFunc(personList, sdkService::delete); + + verifyResult(OperationType.DELETE_ONE, springCost, sdkCost, acceptanceDiff); + } + + @Test + public void deleteAllRecordsTest() { + final List> personList = prepareListBatchData(recurTimes, batchSize); + + final long springCost = acceptInputListFunc(personList, repository::deleteAll); + final long sdkCost = acceptInputListFunc(personList, sdkService::deleteAll); + + verifyResult(OperationType.DELETE_ALL, springCost, sdkCost, acceptanceDiff); + } + + @Test + public void findByIdTest() { + final List idList = prepareListData(recurTimes).stream().map(PerfPerson::getId) + .collect(Collectors.toList()); + + final long springCost = applyInputListFunc(idList, repository::findById); + final long sdkCost = applyInputListFunc(idList, sdkService::findById); + + verifyResult(OperationType.FIND_BY_ID, springCost, sdkCost, acceptanceDiff); + } + + @Test + public void findByMultipleIdsTest() { + final List> idList = listBatchIds(recurTimes, batchSize); + + final long springCost = acceptInputListFunc(idList, repository::findAllById); + final long sdkCost = acceptInputListFunc(idList, sdkService::findAllById); + + verifyResult(OperationType.FIND_BY_IDS, springCost, sdkCost, acceptanceDiff); + } + + @Test + public void findAllTest() { + prepareListData(recurTimes); + + final long springCost = getSupplier(recurTimes, repository::findAll); + final long sdkCost = getSupplier(recurTimes, sdkService::findAll); + + verifyResult(OperationType.FIND_ALL, springCost, sdkCost, acceptanceDiff); + } + + @Test + public void deleteAllTest() { + final long springCost = getSupplier(recurTimes, this::springDeleteAll); + final long sdkCost = getSupplier(recurTimes, sdkService::deleteAll); + + verifyResult(OperationType.DELETE_ALL, springCost, sdkCost, acceptanceDiff); + } + + @Test + public void findBySortingTest() { + prepareListData(recurTimes); + + final Sort sort = Sort.by(Sort.Direction.ASC, "name"); + final List sortList = buildSortList(sort, recurTimes); + + final long springCost = applyInputListFunc(sortList, repository::findAll); + final long sdkCost = applyInputListFunc(sortList, sdkService::searchDocuments); + + verifyResult(OperationType.FIND_BY_SORT, springCost, sdkCost, acceptanceDiff); + } + + @Test + public void findByPagingTest() { + prepareListData(recurTimes); + int pageSize = recurTimes / 2; + pageSize = pageSize >= 1 ? pageSize : 1; + + final long springCost = runConsumerForTimes(recurTimes, pageSize, this::queryTwoPages); + final long sdkCost = runConsumerForTimes(recurTimes, pageSize, sdkService::queryTwoPages); + + verifyResult(OperationType.FIND_BY_PAGING, springCost, sdkCost, acceptanceDiff); + } + + @Test + public void findByFieldTest() { + final List data = prepareListData(recurTimes); + + final String name = data.get(recurTimes / 2).getName(); + + final long springCost = runFunctionForTimes(recurTimes, name, repository::findByName); + final long sdkCost = runConsumerForTimes(recurTimes, name, sdkService::findByName); + + verifyResult(OperationType.FIND_BY_FIELD, springCost, sdkCost, acceptanceDiff); + } + + @Test + public void countTest() { + prepareListData(recurTimes); + + final long springCost = getSupplier(recurTimes, repository::count); + final long sdkCost = getSupplier(recurTimes, sdkService::count); + + verifyResult(OperationType.COUNT, springCost, sdkCost, acceptanceDiff); + } + + /** + * Check whether two time cost fall into the acceptable range. + * + * @param timeCostSpring + * @param timeCostSdk + * @param acceptanceDiff The acceptable diff between two time cost. + */ + private void assertPerf(long timeCostSpring, long timeCostSdk, float acceptanceDiff) { + final long diff = timeCostSpring - timeCostSdk; + final float actualDiff = (float) diff / timeCostSdk; + + assertThat(actualDiff).isLessThan(acceptanceDiff); + } + + private void verifyResult(OperationType type, long timeCostSpring, long timeCostSdk, float acceptanceDiff) { + report.addItem(new PerfItem(type, timeCostSpring, timeCostSdk, recurTimes)); + assertPerf(timeCostSpring, timeCostSdk, acceptanceDiff); + } + + private boolean springDeleteAll() { + repository.deleteAll(); + return true; // To provide return value for Supplier + } + + private List buildSortList(Sort sort, int times) { + final List sorts = new ArrayList<>(); + for (int i = 0; i < times; i++) { + sorts.add(sort); + } + + return sorts; + } + + private void queryTwoPages(int pageSize) { + final Pageable pageable = new CosmosPageRequest(0, pageSize, null); + final Page page = this.repository.findAll(pageable); + this.repository.findAll(page.getPageable()); + } + + private List prepareListData(int count) { + final List personList = PerfDataProvider.getPerfData(count); + + applyInputListFunc(personList, repository::save); + applyInputListFunc(personList, sdkService::save); + + return personList; + } + + + private List> prepareListBatchData(int times, int batchSize) { + final List> personList = PerfDataProvider.getMultiPerfData(batchSize, times); + + applyInputListFunc(personList, repository::saveAll); + applyInputListFunc(personList, sdkService::saveAll); + + return personList; + } + + private List> listBatchIds(int times, int batchSize) { + return prepareListBatchData(times, batchSize).stream() + .map(iterable -> { + final List batchIds = new ArrayList<>(); + iterable.forEach(person -> batchIds.add(person.getId())); + return batchIds; + }).collect(Collectors.toList()); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/PerformanceReport.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/PerformanceReport.java new file mode 100644 index 000000000000..f992d5b16117 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/PerformanceReport.java @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.performance; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class PerformanceReport { + private static final String NEW_LINE = System.lineSeparator(); + + private final List perfItems = new ArrayList<>(); + + public void addItem(PerfItem item) { + perfItems.add(item); + } + + public List getPerfItems() { + return this.perfItems; + } + + public String toString() { + final StringBuilder sb = new StringBuilder(); + perfItems.forEach(item -> sb.append(item.toString()).append(NEW_LINE)); + + return sb.toString(); + } +} + +class PerfItem { + private OperationType type; + private long springCost; + private long sdkCost; + private int times; + private float diffToSdk; + + PerfItem(OperationType type, long springCost, long sdkCost, int times) { + this.type = type; + this.springCost = springCost; + this.sdkCost = sdkCost; + this.times = times; + this.diffToSdk = (float) (springCost - sdkCost) / sdkCost; + } + + public OperationType getType() { + return type; + } + + public void setType(OperationType type) { + this.type = type; + } + + public long getSpringCost() { + return springCost; + } + + public void setSpringCost(long springCost) { + this.springCost = springCost; + } + + public long getSdkCost() { + return sdkCost; + } + + public void setSdkCost(long sdkCost) { + this.sdkCost = sdkCost; + } + + public int getTimes() { + return times; + } + + public void setTimes(int times) { + this.times = times; + } + + public float getDiffToSdk() { + return diffToSdk; + } + + public void setDiffToSdk(float diffToSdk) { + this.diffToSdk = diffToSdk; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PerfItem perfItem = (PerfItem) o; + return springCost == perfItem.springCost + && sdkCost == perfItem.sdkCost + && times == perfItem.times + && Float.compare(perfItem.diffToSdk, diffToSdk) == 0 + && type == perfItem.type; + } + + @Override + public int hashCode() { + return Objects.hash(type, springCost, sdkCost, times, diffToSdk); + } + + @Override + public String toString() { + return "[type=" + + type.toString() + + ", springCost=" + + springCost + + ", sdkCost=" + + sdkCost + + ", times=" + + times + + ", diffToSdk=" + + (diffToSdk * 100 + + "%") + + "];"; + } +} + +enum OperationType { + SAVE_ONE("save one"), SAVE_ALL("save all"), DELETE_ONE("delete one"), DELETE_ALL("delete all"), + FIND_BY_ID("find by id"), FIND_BY_IDS("find by ids"), FIND_ALL("find all"), FIND_BY_SORT("find by sort"), + FIND_BY_PAGING("find by paging"), FIND_BY_FIELD("find by field"), COUNT("count"); + + private String type; + + OperationType(String type) { + this.type = type; + } + + public String toString() { + return this.type; + } + +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/domain/PerfPerson.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/domain/PerfPerson.java new file mode 100644 index 000000000000..4be9232067b5 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/domain/PerfPerson.java @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.performance.domain; + +import com.microsoft.azure.spring.data.cosmosdb.common.TestConstants; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.DocumentIndexingPolicy; +import com.microsoft.azure.spring.data.cosmosdb.performance.utils.Constants; + +import java.util.Objects; + +@Document(collection = Constants.SPRING_COLLECTION_NAME) +@DocumentIndexingPolicy(includePaths = TestConstants.ORDER_BY_STRING_PATH) +public class PerfPerson { + private String id; + private String name; + + public PerfPerson(String id, String name) { + this.id = id; + this.name = name; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PerfPerson that = (PerfPerson) o; + return Objects.equals(id, that.id) + && Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(id, name); + } + + @Override + public String toString() { + return "PerfPerson{" + + "id='" + + id + + '\'' + + ", name='" + + name + + '\'' + + '}'; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/repository/PerfPersonRepository.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/repository/PerfPersonRepository.java new file mode 100644 index 000000000000..7f41ee743800 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/repository/PerfPersonRepository.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.performance.repository; + +import com.microsoft.azure.spring.data.cosmosdb.performance.domain.PerfPerson; +import com.microsoft.azure.spring.data.cosmosdb.repository.CosmosRepository; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface PerfPersonRepository extends CosmosRepository { + List findAll(Sort sort); + + List findByName(String name); +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/service/SdkService.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/service/SdkService.java new file mode 100644 index 000000000000..bf5462d42c1a --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/service/SdkService.java @@ -0,0 +1,219 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.performance.service; + +import com.azure.data.cosmos.CosmosClient; +import com.azure.data.cosmos.CosmosClientException; +import com.azure.data.cosmos.CosmosItemProperties; +import com.azure.data.cosmos.CosmosItemRequestOptions; +import com.azure.data.cosmos.FeedOptions; +import com.azure.data.cosmos.FeedResponse; +import com.azure.data.cosmos.PartitionKey; +import com.azure.data.cosmos.sync.CosmosSyncClient; +import com.google.gson.Gson; +import com.microsoft.azure.spring.data.cosmosdb.performance.domain.PerfPerson; +import com.microsoft.azure.spring.data.cosmosdb.performance.utils.DatabaseUtils; +import org.assertj.core.util.Lists; +import org.springframework.data.domain.Sort; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +public class SdkService { + private static Gson gson = new Gson(); + + private final CosmosSyncClient cosmosSyncClient; + private final String dbName; + private final String containerName; + + public SdkService(CosmosSyncClient client, String dbName, String containerName, CosmosClient asyncClient) { + this.cosmosSyncClient = client; + this.dbName = dbName; + this.containerName = containerName; + } + + public PerfPerson save(PerfPerson person) { + try { + final String personJson = gson.toJson(person); + final CosmosItemProperties personDoc = new CosmosItemProperties(personJson); + + final CosmosItemProperties doc = cosmosSyncClient.getDatabase(dbName) + .getContainer(containerName) + .createItem(personDoc) + .properties(); + + return gson.fromJson(doc.toJson(), PerfPerson.class); + } catch (Exception e) { + throw new IllegalStateException(e); // Runtime exception to fail directly + } + } + + public List saveAll(Iterable personIterable) { + final List result = new ArrayList<>(); + personIterable.forEach(person -> result.add(save(person))); + + return result; + } + + public void delete(PerfPerson person) { + try { + final String docLink = DatabaseUtils.getDocumentLink(dbName, containerName, person.getId()); + cosmosSyncClient.getDatabase(dbName) + .getContainer(containerName) + .getItem(person.getId(), PartitionKey.None) + .delete(new CosmosItemRequestOptions()); + + } catch (CosmosClientException e) { + throw new IllegalStateException(e); // Runtime exception to fail directly + } + } + + public void deleteAll(Iterable personIterable) { + personIterable.forEach(person -> delete(person)); + } + + public CosmosItemProperties findById(String id) { + final Iterator> feedResponseIterator = + cosmosSyncClient.getDatabase(dbName) + .getContainer(containerName) + .queryItems("SELECT * FROM " + + containerName + + " WHERE " + + containerName + + ".id='" + + id + + "'", new FeedOptions()); + CosmosItemProperties itemProperties = null; + if (feedResponseIterator.hasNext()) { + final List results = feedResponseIterator.next().results(); + if (!results.isEmpty()) { + itemProperties = results.get(0); + } + } + + return itemProperties; + } + + public List findAllById(Iterable ids) { + final String idsInList = String.join(",", + Arrays.asList(ids).stream().map(id -> "'" + id + "'").collect(Collectors.toList())); + final String sql = "SELECT * FROM " + containerName + " WHERE " + containerName + ".id IN (" + + idsInList + ")"; + + final FeedOptions feedOptions = new FeedOptions(); + feedOptions.enableCrossPartitionQuery(true); + + final List docs = new ArrayList<>(); + + final Iterator> feedResponseIterator = cosmosSyncClient.getDatabase(dbName) + .getContainer(containerName) + .queryItems(sql, feedOptions); + while (feedResponseIterator.hasNext()) { + final FeedResponse next = feedResponseIterator.next(); + docs.addAll(next.results()); + } + + + return fromDocuments(docs); + } + + public List findAll() { + final String sql = "SELECT * FROM " + containerName; + final List docs = getCosmosItemPropertiesList(sql); + return fromDocuments(docs); + } + + public boolean deleteAll() { + final String sql = "SELECT * FROM " + containerName; + final List documents = getCosmosItemPropertiesList(sql); + + documents.forEach(document -> { + try { + cosmosSyncClient.getDatabase(dbName) + .getContainer(containerName) + .getItem(document.id(), PartitionKey.None) + .delete(new CosmosItemRequestOptions().partitionKey(PartitionKey.None)); + } catch (CosmosClientException e) { + throw new IllegalStateException(e); + } + }); + + return true; + } + + private List getCosmosItemPropertiesList(String sql) { + final List documents = new ArrayList<>(); + final Iterator> feedResponseIterator = + cosmosSyncClient.getDatabase(dbName) + .getContainer(containerName) + .queryItems(sql, new FeedOptions().enableCrossPartitionQuery(true)); + while (feedResponseIterator.hasNext()) { + final FeedResponse next = feedResponseIterator.next(); + documents.addAll(next.results()); + } + return documents; + } + + public List searchDocuments(Sort sort) { + final Sort.Order order = sort.iterator().next(); // Only one Order supported + final String sql = "SELECT * FROM " + containerName + " ORDER BY " + containerName + "." + + order.getProperty() + " " + order.getDirection().name(); + final List docs = getCosmosItemPropertiesList(sql); + + return fromDocuments(docs); + } + + public long count() { + final String sql = "SELECT VALUE COUNT(1) FROM " + containerName; + final Iterator> feedResponseIterator = cosmosSyncClient.getDatabase(dbName) + .getContainer(containerName) + .queryItems(sql, new FeedOptions()); + final Object result = feedResponseIterator.next().results().get(0).get("_aggregate"); + + return result instanceof Integer ? Long.valueOf((Integer) result) : (Long) result; + } + + public List findByName(String name) { + final String sql = "SELECT * FROM " + containerName + " WHERE " + containerName + ".name='" + + name + "'"; + final Iterator result = getCosmosItemPropertiesList(sql).iterator(); + return fromDocuments(Lists.newArrayList(result)); + } + + public void queryTwoPages(int pageSize) { + final FeedOptions options = new FeedOptions(); + options.maxItemCount(pageSize); + options.requestContinuation(null); + + searchBySize(pageSize, options); + searchBySize(pageSize, options); + } + + private List searchBySize(int size, FeedOptions options) { + final String sql = "SELECT * FROM " + containerName; + + final Iterator it = getCosmosItemPropertiesList(sql).iterator(); + final List entities = new ArrayList<>(); + int i = 0; + while (it.hasNext() + && i++ < size) { + // This convert here is in order to mock data conversion in real use case, in order to compare with + // Spring Data mapping + final CosmosItemProperties d = it.next(); + final PerfPerson entity = gson.fromJson(d.toJson(), PerfPerson.class); + entities.add(entity); + } + + count(); // Mock same behavior with Spring pageable query, requires total elements count + + return entities; + } + + private List fromDocuments(List documents) { + return documents.stream().map(d -> gson.fromJson(d.toJson(), PerfPerson.class)) + .collect(Collectors.toList()); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/utils/Constants.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/utils/Constants.java new file mode 100644 index 000000000000..0813cd7fb40d --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/utils/Constants.java @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.performance.utils; + +public class Constants { + public static final String PERF_DATABASE_NAME = "perf_database"; + public static final String SPRING_COLLECTION_NAME = "spring_perf_coll"; + public static final String SDK_COLLECTION_NAME = "sdk_perf_coll"; +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/utils/DatabaseUtils.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/utils/DatabaseUtils.java new file mode 100644 index 000000000000..061e6a97e4d5 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/utils/DatabaseUtils.java @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.performance.utils; + +import com.azure.data.cosmos.CosmosClientException; +import com.azure.data.cosmos.CosmosContainerProperties; +import com.azure.data.cosmos.IncludedPath; +import com.azure.data.cosmos.IndexingPolicy; +import com.azure.data.cosmos.PartitionKeyDefinition; +import com.azure.data.cosmos.internal.RequestOptions; +import com.azure.data.cosmos.sync.CosmosSyncClient; + +import java.util.Collections; + +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.ORDER_BY_STRING_PATH; + +public class DatabaseUtils { + public static void createDatabase(CosmosSyncClient documentClient, String databaseName) + throws CosmosClientException { + try { + // Can use sync api once ready + documentClient.getDatabase(databaseName).delete(); + } catch (Exception e) { + // Ignore delete failure + } + + documentClient.createDatabase(databaseName); + } + + public static void deleteContainer(CosmosSyncClient documentClient, String databaseName, String containerName) + throws CosmosClientException { + final RequestOptions requestOptions = new RequestOptions(); + requestOptions.setOfferThroughput(1000); + + documentClient.getDatabase(databaseName).getContainer(containerName).delete(); + } + + public static void createContainer(CosmosSyncClient documentClient, String databaseName, String containerName) + throws CosmosClientException { + final CosmosContainerProperties containerProperties = new CosmosContainerProperties(containerName, + new PartitionKeyDefinition().paths(Collections.singletonList("/mypk"))); + + final IndexingPolicy policy = new IndexingPolicy(); + policy.setIncludedPaths(Collections.singletonList(new IncludedPath(ORDER_BY_STRING_PATH))); + containerProperties.indexingPolicy(policy); + + documentClient.getDatabase(databaseName).createContainer(containerProperties); + } + + public static String getDocumentLink(String databaseName, String containerName, Object documentId) { + return getContainerLink(databaseName, containerName) + "/docs/" + documentId; + } + + public static String getDatabaseLink(String databaseName) { + return "dbs/" + databaseName; + } + + public static String getContainerLink(String databaseName, String containerName) { + return getDatabaseLink(databaseName) + "/colls/" + containerName; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/utils/FunctionUtils.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/utils/FunctionUtils.java new file mode 100644 index 000000000000..ba890c121215 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/utils/FunctionUtils.java @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.performance.utils; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +public class FunctionUtils { + /** + * Run supplier for times. + * @param times How many times the supplier will run. + * @param supplier Supplier to run. + * @param + * @return time cost in milli-seconds of running the supplier for times + */ + public static long getSupplier(int times, Supplier supplier) { + final long startTime = System.currentTimeMillis(); + for (int i = 0; i < times; i++) { + supplier.get(); + } + final long endTime = System.currentTimeMillis(); + + return endTime - startTime; + } + + + /** + * Run function for each element in the inputList + * @param inputList list of input data to be processed by function + * @param function @see java.util.function.Function to process data in the inputList + * @param the type of the input to the function + * @param the type of the result of the function + * @return time cost in milli-seconds of processing the whole inputList by function + */ + public static long applyInputListFunc(List inputList, Function function) { + final long startTime = System.currentTimeMillis(); + inputList.forEach(function::apply); + final long endTime = System.currentTimeMillis(); + + return endTime - startTime; + } + + /** + * + * @param times How many times the function will apply + * @param argument argument for the function + * @param function @see java.util.function.Function to process the argument + * @param the type of the input to the function + * @param the type of the result of the function + * @return time cost in milli-seconds of running the function for times + */ + public static long runFunctionForTimes(int times, T argument, Function function) { + final long startTime = System.currentTimeMillis(); + for (int i = 0; i < times; i++) { + function.apply(argument); + } + final long endTime = System.currentTimeMillis(); + + return endTime - startTime; + } + + /** + * Run consumer for each element in the inputList + * @param inputList list of input data to be processed by consumer + * @param consumer @see java.util.function.Consumer to process data in the inputList + * @param the type of the input to the consumer + * @return time cost in milli-seconds of processing the whole inputList by consumer + */ + public static long acceptInputListFunc(List inputList, Consumer consumer) { + final long startTime = System.currentTimeMillis(); + inputList.forEach(consumer::accept); + final long endTime = System.currentTimeMillis(); + + return endTime - startTime; + } + + /** + * Run consumer with given argument for given times. + * @param times How many times the consumer will run + * @param argument argument for the consumer + * @param consumer @see java.util.function.Consumer to process the input + * @param the type of the input to the consumer + * @return time cost in milli-seconds of running the consumer for times + */ + public static long runConsumerForTimes(int times, T argument, Consumer consumer) { + final long startTime = System.currentTimeMillis(); + for (int i = 0; i < times; i++) { + consumer.accept(argument); + } + final long endTime = System.currentTimeMillis(); + + return endTime - startTime; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/utils/PerfDataProvider.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/utils/PerfDataProvider.java new file mode 100644 index 000000000000..3c5472ca8465 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/performance/utils/PerfDataProvider.java @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.performance.utils; + +import com.microsoft.azure.spring.data.cosmosdb.performance.domain.PerfPerson; +import org.assertj.core.util.Lists; + +import java.util.List; +import java.util.UUID; + +public class PerfDataProvider { + public static List getPerfData(int count) { + final List personList = Lists.newArrayList(); + + for (int i = 1; i <= count; i++) { + final PerfPerson person = new PerfPerson(randomId(), "fake name-" + randomSuffix()); + personList.add(person); + } + + return personList; + } + + public static List> getMultiPerfData(int size, int listCount) { + final List> personList = Lists.newArrayList(); + + for (int i = 0; i < listCount; i++) { + personList.add(getPerfData(size)); + } + + return personList; + } + + private static String randomId() { + return UUID.randomUUID().toString().substring(0, 10); + } + + private static String randomSuffix() { + // It's not random, but just for distinguishing + return UUID.randomUUID().toString().substring(0, 4); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/CosmosAnnotationUnitTest.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/CosmosAnnotationUnitTest.java new file mode 100644 index 000000000000..920fc29a5327 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/CosmosAnnotationUnitTest.java @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository; + +import com.azure.data.cosmos.IndexingPolicy; +import com.microsoft.azure.spring.data.cosmosdb.common.TestConstants; +import com.microsoft.azure.spring.data.cosmosdb.common.TestUtils; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.DocumentIndexingPolicy; +import com.microsoft.azure.spring.data.cosmosdb.domain.NoDBAnnotationPerson; +import com.microsoft.azure.spring.data.cosmosdb.domain.Role; +import com.microsoft.azure.spring.data.cosmosdb.domain.TimeToLiveSample; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.junit.Before; +import org.junit.Test; +import org.springframework.util.Assert; + + +public class CosmosAnnotationUnitTest { + private CosmosEntityInformation personInfo; + private CosmosEntityInformation roleInfo; + + @Before + public void setUp() { + personInfo = new CosmosEntityInformation<>(NoDBAnnotationPerson.class); + roleInfo = new CosmosEntityInformation<>(Role.class); + } + + @Test + public void testDefaultIndexingPolicyAnnotation() { + final IndexingPolicy policy = personInfo.getIndexingPolicy(); + final Document documentAnnotation = NoDBAnnotationPerson.class.getAnnotation(Document.class); + final DocumentIndexingPolicy policyAnnotation = + NoDBAnnotationPerson.class.getAnnotation(DocumentIndexingPolicy.class); + + Assert.isNull(documentAnnotation, "NoDBAnnotationPerson class should not have Document annotation"); + Assert.isNull(policyAnnotation, "NoDBAnnotationPerson class should not have DocumentIndexingPolicy annotation"); + Assert.notNull(policy, "NoDBAnnotationPerson class collection policy should not be null"); + + // ContainerName, RequestUnit, Automatic and IndexingMode + Assert.isTrue(personInfo.getContainerName().equals(NoDBAnnotationPerson.class.getSimpleName()), + "should be default collection name"); + Assert.isTrue(personInfo.getRequestUnit() == TestConstants.DEFAULT_REQUEST_UNIT, + "should be default request unit"); + Assert.isTrue(policy.automatic() == TestConstants.DEFAULT_INDEXINGPOLICY_AUTOMATIC, + "should be default indexing policy automatic"); + Assert.isTrue(policy.indexingMode() == TestConstants.DEFAULT_INDEXINGPOLICY_MODE, + "should be default indexing policy mode"); + + // IncludedPaths and ExcludedPaths + // We do not use testIndexingPolicyPathsEquals generic here, for unit test do not create cosmosdb instance, + // and the paths of policy will never be set from azure service. + // testIndexingPolicyPathsEquals(policy.getIncludedPaths(), TestConstants.DEFAULT_INCLUDEDPATHS); + // testIndexingPolicyPathsEquals(policy.getExcludedPaths(), TestConstants.DEFAULT_EXCLUDEDPATHS); + Assert.isTrue(policy.includedPaths().isEmpty(), "default includedpaths size must be 0"); + Assert.isTrue(policy.excludedPaths().isEmpty(), "default excludedpaths size must be 0"); + } + + @Test + public void testIndexingPolicyAnnotation() { + final IndexingPolicy policy = roleInfo.getIndexingPolicy(); + final Document documentAnnotation = Role.class.getAnnotation(Document.class); + final DocumentIndexingPolicy policyAnnotation = Role.class.getAnnotation(DocumentIndexingPolicy.class); + + // ContainerName, RequestUnit, Automatic and IndexingMode + Assert.notNull(documentAnnotation, "NoDBAnnotationPerson class should have Document annotation"); + Assert.notNull(policyAnnotation, "NoDBAnnotationPerson class should have DocumentIndexingPolicy annotation"); + Assert.notNull(policy, "NoDBAnnotationPerson class collection policy should not be null"); + + Assert.isTrue(roleInfo.getContainerName().equals(TestConstants.ROLE_COLLECTION_NAME), + "should be Role(class) collection name"); + Assert.isTrue(roleInfo.getRequestUnit() == TestConstants.REQUEST_UNIT, + "should be Role(class) request unit"); + Assert.isTrue(policy.automatic() == TestConstants.INDEXINGPOLICY_AUTOMATIC, + "should be Role(class) indexing policy automatic"); + Assert.isTrue(policy.indexingMode() == TestConstants.INDEXINGPOLICY_MODE, + "should be Role(class) indexing policy mode"); + + // IncludedPaths and ExcludedPaths + TestUtils.testIndexingPolicyPathsEquals(policy.includedPaths(), TestConstants.INCLUDEDPATHS); + TestUtils.testIndexingPolicyPathsEquals(policy.excludedPaths(), TestConstants.EXCLUDEDPATHS); + } + + @Test + public void testAutoCreateCollectionAnnotation() { + final boolean autoCreateCollectionRoleInfo = roleInfo.isAutoCreateContainer(); + final boolean autoCreateCollectionPersonInfo = personInfo.isAutoCreateContainer(); + + Assert.isTrue(!autoCreateCollectionRoleInfo, "autoCreateContainer in role should be false"); + Assert.isTrue(autoCreateCollectionPersonInfo, "autoCreateContainer in person should be true"); + } + + @Test + public void testDefaultDocumentAnnotationTimeToLive() { + final Integer timeToLive = personInfo.getTimeToLive(); + + Assert.notNull(timeToLive, "timeToLive should not be null"); + Assert.isTrue(timeToLive == TestConstants.DEFAULT_TIME_TO_LIVE, "should be default time to live"); + } + + @Test + public void testDocumentAnnotationTimeToLive() { + final CosmosEntityInformation info = + new CosmosEntityInformation<>(TimeToLiveSample.class); + final Integer timeToLive = info.getTimeToLive(); + + Assert.notNull(timeToLive, "timeToLive should not be null"); + Assert.isTrue(timeToLive == TestConstants.TIME_TO_LIVE, "should be the same time to live"); + } +} + diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/SimpleCosmosRepositoryIllegalTest.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/SimpleCosmosRepositoryIllegalTest.java new file mode 100644 index 000000000000..117d3bbf74ae --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/SimpleCosmosRepositoryIllegalTest.java @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository; + +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosOperations; +import com.microsoft.azure.spring.data.cosmosdb.domain.Person; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.SimpleCosmosRepository; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class SimpleCosmosRepositoryIllegalTest { + private SimpleCosmosRepository repository; + + @Mock + CosmosOperations dbOperations; + @Mock + CosmosEntityInformation entityInformation; + + @Before + public void setUp() { + repository = new SimpleCosmosRepository<>(entityInformation, dbOperations); + } + + @Test(expected = IllegalArgumentException.class) + public void deleteNullShouldFail() { + repository.delete(null); + } + + @Test(expected = IllegalArgumentException.class) + public void deleteIterableNullShouldFail() { + repository.deleteAll(null); + } + + @Test(expected = IllegalArgumentException.class) + public void deleteNullIdShouldFail() { + repository.deleteById(null); + } + + @Test(expected = IllegalArgumentException.class) + public void existsNullIdShouldFail() { + repository.existsById(null); + } + + @Test(expected = IllegalArgumentException.class) + public void findNullIterableIdsShouldFail() { + repository.findAllById(null); + } + + @Test(expected = IllegalArgumentException.class) + public void findByNullIdShouldFail() { + repository.findById(null); + } + + @Test(expected = IllegalArgumentException.class) + public void saveNullShouldFail() { + repository.save(null); + } + + @Test(expected = IllegalArgumentException.class) + public void saveNullIterableShouldFail() { + repository.saveAll(null); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/SimpleCosmosRepositoryUnitTest.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/SimpleCosmosRepositoryUnitTest.java new file mode 100644 index 000000000000..6ec48cb66bc3 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/SimpleCosmosRepositoryUnitTest.java @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.repository; + +import com.microsoft.azure.spring.data.cosmosdb.common.TestConstants; +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosOperations; +import com.microsoft.azure.spring.data.cosmosdb.domain.Address; +import com.microsoft.azure.spring.data.cosmosdb.domain.Person; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.SimpleCosmosRepository; +import org.assertj.core.util.Lists; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class SimpleCosmosRepositoryUnitTest { + private static final Person TEST_PERSON = + new Person(TestConstants.ID_1, TestConstants.FIRST_NAME, TestConstants.LAST_NAME, + TestConstants.HOBBIES, TestConstants.ADDRESSES); + + private static final String PARTITION_VALUE_REQUIRED_MSG = + "PartitionKey value must be supplied for this operation."; + + private SimpleCosmosRepository repository; + @Mock + CosmosOperations cosmosOperations; + @Mock + CosmosEntityInformation entityInformation; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Before + public void setUp() { + when(entityInformation.getJavaType()).thenReturn(Person.class); + when(entityInformation.getContainerName()).thenReturn(Person.class.getSimpleName()); + when(cosmosOperations.findAll(anyString(), any())).thenReturn(Arrays.asList(TEST_PERSON)); + + repository = new SimpleCosmosRepository<>(entityInformation, cosmosOperations); + } + + @Test + public void testSave() { + repository.save(TEST_PERSON); + + final List result = Lists.newArrayList(repository.findAll()); + assertEquals(1, result.size()); + assertEquals(TEST_PERSON, result.get(0)); + } + + @Test + public void testFindOne() { + when(cosmosOperations.findById(anyString(), anyString(), any())).thenReturn(TEST_PERSON); + + repository.save(TEST_PERSON); + + final Person result = repository.findById(TEST_PERSON.getId()).get(); + assertEquals(TEST_PERSON, result); + } + + @Test + public void testFindOneExceptionForPartitioned() { + expectedException.expect(UnsupportedOperationException.class); + expectedException.expectMessage(PARTITION_VALUE_REQUIRED_MSG); + + repository.save(TEST_PERSON); + + when(cosmosOperations.findById(anyString(), anyString(), any())) + .thenThrow(new UnsupportedOperationException(PARTITION_VALUE_REQUIRED_MSG)); + + final Person result = repository.findById(TEST_PERSON.getId()).get(); + } + + @Test + public void testUpdate() { + final List
updatedAddress = + Arrays.asList(new Address(TestConstants.POSTAL_CODE, TestConstants.UPDATED_CITY, + TestConstants.UPDATED_STREET)); + final Person updatedPerson = + new Person(TEST_PERSON.getId(), TestConstants.UPDATED_FIRST_NAME, TestConstants.UPDATED_LAST_NAME, + TestConstants.UPDATED_HOBBIES, updatedAddress); + repository.save(updatedPerson); + + when(cosmosOperations.findById(anyString(), anyString(), any())).thenReturn(updatedPerson); + + final Person result = repository.findById(TEST_PERSON.getId()).get(); + assertEquals(updatedPerson, result); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/TestRepositoryConfig.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/TestRepositoryConfig.java new file mode 100644 index 000000000000..7079a4683ec6 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/TestRepositoryConfig.java @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository; + +import com.azure.data.cosmos.ConsistencyLevel; +import com.azure.data.cosmos.internal.RequestOptions; +import com.microsoft.azure.spring.data.cosmosdb.common.DynamicContainer; +import com.microsoft.azure.spring.data.cosmosdb.common.ResponseDiagnosticsTestUtils; +import com.microsoft.azure.spring.data.cosmosdb.common.TestConstants; +import com.microsoft.azure.spring.data.cosmosdb.config.AbstractCosmosConfiguration; +import com.microsoft.azure.spring.data.cosmosdb.config.CosmosDBConfig; +import com.microsoft.azure.spring.data.cosmosdb.repository.config.EnableCosmosRepositories; +import com.microsoft.azure.spring.data.cosmosdb.repository.config.EnableReactiveCosmosRepositories; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.util.StringUtils; + +@Configuration +@PropertySource(value = {"classpath:application.properties"}) +@EnableCosmosRepositories +@EnableReactiveCosmosRepositories +public class TestRepositoryConfig extends AbstractCosmosConfiguration { + @Value("${cosmosdb.uri:}") + private String cosmosDbUri; + + @Value("${cosmosdb.key:}") + private String cosmosDbKey; + + @Value("${cosmosdb.connection-string:}") + private String connectionString; + + @Value("${cosmosdb.database:}") + private String database; + + @Value("${cosmosdb.populateQueryMetrics}") + private boolean populateQueryMetrics; + + private RequestOptions getRequestOptions() { + final RequestOptions options = new RequestOptions(); + + options.setConsistencyLevel(ConsistencyLevel.SESSION); +// options.setDisableRUPerMinuteUsage(true); + options.setScriptLoggingEnabled(true); + + return options; + } + + @Bean + public ResponseDiagnosticsTestUtils responseDiagnosticsTestUtils() { + return new ResponseDiagnosticsTestUtils(); + } + + @Bean + public CosmosDBConfig getConfig() { + final String dbName = StringUtils.hasText(this.database) ? this.database : TestConstants.DB_NAME; + final RequestOptions options = getRequestOptions(); + final CosmosDBConfig.CosmosDBConfigBuilder builder; + + if (StringUtils.hasText(this.cosmosDbUri) + && StringUtils.hasText(this.cosmosDbKey)) { + builder = CosmosDBConfig.builder(cosmosDbUri, cosmosDbKey, dbName); + } else { + builder = CosmosDBConfig.builder(connectionString, dbName); + } + return builder.requestOptions(options) + .populateQueryMetrics(populateQueryMetrics) + .responseDiagnosticsProcessor(responseDiagnosticsTestUtils().getResponseDiagnosticsProcessor()) + .build(); + } + + @Bean + public DynamicContainer dynamicContainer() { + return new DynamicContainer(TestConstants.DYNAMIC_BEAN_COLLECTION_NAME); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/CosmosRepositoryConfigurationExtensionUnitTest.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/CosmosRepositoryConfigurationExtensionUnitTest.java new file mode 100644 index 000000000000..b6960ab484c3 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/CosmosRepositoryConfigurationExtensionUnitTest.java @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.repository.config; + +import com.microsoft.azure.spring.data.cosmosdb.repository.CosmosRepository; +import org.junit.Test; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.core.env.Environment; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.type.StandardAnnotationMetadata; +import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; +import org.springframework.data.repository.config.RepositoryConfiguration; +import org.springframework.data.repository.config.RepositoryConfigurationSource; + +import java.util.Collection; + +import static org.assertj.core.api.Assertions.fail; + +public class CosmosRepositoryConfigurationExtensionUnitTest { + + StandardAnnotationMetadata metadata = new StandardAnnotationMetadata(Config.class, true); + ResourceLoader loader = new PathMatchingResourcePatternResolver(); + Environment environment = new StandardEnvironment(); + RepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource(metadata, + EnableCosmosRepositories.class, loader, environment, new DefaultListableBeanFactory()); + + private static void assertHashRepo(Class repositoryInterface, + Collection> configs) { + for (final RepositoryConfiguration config : configs) { + if (config.getRepositoryInterface().equals(repositoryInterface.getName())) { + return; + } + } + + fail("expected to find config for repository interface " + + repositoryInterface.getName() + ", but got: " + configs.toString()); + } + + @Test + public void isStrictMatchIfRepositoryExtendsStoreSpecificBase() { + final CosmosRepositoryConfigurationExtension extension = new CosmosRepositoryConfigurationExtension(); + assertHashRepo(TestRepository.class, extension.getRepositoryConfigurations(configurationSource, loader, true)); + } + + interface TestRepository extends CosmosRepository { + } + + @EnableCosmosRepositories(considerNestedRepositories = true) + static class Config { + + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/ReactiveCosmosRepositoryConfigurationExtensionUnitTest.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/ReactiveCosmosRepositoryConfigurationExtensionUnitTest.java new file mode 100644 index 000000000000..1e32fb0fe25b --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/config/ReactiveCosmosRepositoryConfigurationExtensionUnitTest.java @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.data.cosmosdb.repository.config; + +import com.microsoft.azure.spring.data.cosmosdb.repository.ReactiveCosmosRepository; +import org.junit.Test; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.core.env.Environment; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.type.StandardAnnotationMetadata; +import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; +import org.springframework.data.repository.config.RepositoryConfiguration; +import org.springframework.data.repository.config.RepositoryConfigurationSource; + +import java.util.Collection; + +import static org.assertj.core.api.Assertions.fail; + +public class ReactiveCosmosRepositoryConfigurationExtensionUnitTest { + + StandardAnnotationMetadata metadata = new StandardAnnotationMetadata(Config.class, true); + ResourceLoader loader = new PathMatchingResourcePatternResolver(); + Environment environment = new StandardEnvironment(); + RepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource(metadata, + EnableReactiveCosmosRepositories.class, loader, environment, new DefaultListableBeanFactory()); + + private static void assertHashRepo(Class repositoryInterface, + Collection> configs) { + for (final RepositoryConfiguration config : configs) { + if (config.getRepositoryInterface().equals(repositoryInterface.getName())) { + return; + } + } + + fail("expected to find config for repository interface " + + repositoryInterface.getName() + ", but got: " + configs.toString()); + } + + @Test + public void isStrictMatchIfRepositoryExtendsStoreSpecificBase() { + final ReactiveCosmosRepositoryConfigurationExtension extension = + new ReactiveCosmosRepositoryConfigurationExtension(); + assertHashRepo(TestRepository.class, + extension.getRepositoryConfigurations(configurationSource, loader, true)); + } + + interface TestRepository extends ReactiveCosmosRepository { + } + + @EnableReactiveCosmosRepositories(considerNestedRepositories = true) + static class Config { + + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/AddressRepositoryIT.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/AddressRepositoryIT.java new file mode 100644 index 000000000000..78526ac34dd6 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/AddressRepositoryIT.java @@ -0,0 +1,238 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.integration; + +import com.azure.data.cosmos.PartitionKey; +import com.microsoft.azure.spring.data.cosmosdb.common.TestConstants; +import com.microsoft.azure.spring.data.cosmosdb.common.TestUtils; +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosTemplate; +import com.microsoft.azure.spring.data.cosmosdb.domain.Address; +import com.microsoft.azure.spring.data.cosmosdb.repository.TestRepositoryConfig; +import com.microsoft.azure.spring.data.cosmosdb.repository.repository.AddressRepository; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.assertj.core.util.Lists; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class AddressRepositoryIT { + + private static final Address TEST_ADDRESS1_PARTITION1 = new Address( + TestConstants.POSTAL_CODE, TestConstants.STREET, TestConstants.CITY); + private static final Address TEST_ADDRESS2_PARTITION1 = new Address( + TestConstants.POSTAL_CODE_0, TestConstants.STREET_0, TestConstants.CITY); + private static final Address TEST_ADDRESS1_PARTITION2 = new Address( + TestConstants.POSTAL_CODE_1, TestConstants.STREET_1, TestConstants.CITY_0); + private static final Address TEST_ADDRESS4_PARTITION3 = new Address( + TestConstants.POSTAL_CODE, TestConstants.STREET_2, TestConstants.CITY_1); + + private static final CosmosEntityInformation entityInformation + = new CosmosEntityInformation<>(Address.class); + + private static CosmosTemplate staticTemplate; + private static boolean isSetupDone; + + @Autowired + AddressRepository repository; + + @Autowired + private CosmosTemplate template; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Before + public void setUp() { + if (!isSetupDone) { + staticTemplate = template; + template.createContainerIfNotExists(entityInformation); + } + repository.save(TEST_ADDRESS1_PARTITION1); + repository.saveAll(Lists.newArrayList(TEST_ADDRESS1_PARTITION2, + TEST_ADDRESS2_PARTITION1, TEST_ADDRESS4_PARTITION3)); + isSetupDone = true; + } + + @After + public void cleanup() { + repository.deleteAll(); + } + + @AfterClass + public static void afterClassCleanup() { + staticTemplate.deleteContainer(entityInformation.getContainerName()); + } + + @Test + public void testFindAll() { + // findAll cross partition + final List
result = TestUtils.toList(repository.findAll()); + + assertThat(result.size()).isEqualTo(4); + } + + @Test + public void testFindByIdWithPartitionKey() { + final Optional
addressById = repository.findById(TEST_ADDRESS1_PARTITION1.getPostalCode(), + new PartitionKey(entityInformation.getPartitionKeyFieldValue(TEST_ADDRESS1_PARTITION1))); + + if (!addressById.isPresent()) { + fail("address not found"); + return; + } + assertThat(addressById.get()).isEqualTo(TEST_ADDRESS1_PARTITION1); + } + + @Test + public void testFindByIdForPartitionedCollection() { + final List
addresses = repository.findByPostalCode(TestConstants.POSTAL_CODE); + + assertThat(addresses.size()).isEqualTo(2); + assertThat(addresses.get(0).getPostalCode()).isEqualTo(TestConstants.POSTAL_CODE); + assertThat(addresses.get(1).getPostalCode()).isEqualTo(TestConstants.POSTAL_CODE); + } + + @Test + public void testFindByPartitionedCity() { + final String city = TEST_ADDRESS1_PARTITION1.getCity(); + final List
result = TestUtils.toList(repository.findByCity(city)); + + assertThat(result.size()).isEqualTo(2); + assertThat(result.get(0).getCity()).isEqualTo(city); + assertThat(result.get(1).getCity()).isEqualTo(city); + } + + @Test + public void testFindByStreetOrCity() { + final String city = TEST_ADDRESS1_PARTITION1.getCity(); + final String street = TEST_ADDRESS1_PARTITION2.getStreet(); + + final List
result = repository.findByStreetOrCity(street, city); + final List
reference = Arrays.asList( + TEST_ADDRESS1_PARTITION1, TEST_ADDRESS1_PARTITION2, TEST_ADDRESS2_PARTITION1); + + result.sort(Comparator.comparing(Address::getPostalCode)); + reference.sort(Comparator.comparing(Address::getPostalCode)); + + Assert.assertEquals(reference.size(), result.size()); + Assert.assertEquals(reference, result); + } + + @Test + public void testCount() { + final long count = repository.count(); + assertThat(count).isEqualTo(4); + + repository.deleteByCity(TestConstants.CITY); + final long newCount = repository.count(); + assertThat(newCount).isEqualTo(2); + } + + @Test + public void deleteWithoutPartitionedColumnShouldFail() { + expectedException.expect(Exception.class); + + repository.deleteById(TEST_ADDRESS1_PARTITION1.getPostalCode()); + } + + @Test + public void canDeleteByIdAndPartitionedCity() { + final long count = repository.count(); + assertThat(count).isEqualTo(4); + + repository.deleteByPostalCodeAndCity( + TEST_ADDRESS1_PARTITION1.getPostalCode(), TEST_ADDRESS1_PARTITION1.getCity()); + + final List
result = TestUtils.toList(repository.findAll()); + + assertThat(result.size()).isEqualTo(3); + } + + @Test + public void canDeleteByPartitionedCity() { + final long count = repository.count(); + assertThat(count).isEqualTo(4); + + repository.deleteByCity(TEST_ADDRESS1_PARTITION1.getCity()); + + final List
result = TestUtils.toList(repository.findAll()); + + assertThat(result.size()).isEqualTo(2); + assertThat(result.get(0).getCity()).isNotEqualTo(TEST_ADDRESS1_PARTITION1.getCity()); + } + + @Test + public void testDeleteByIdAndPartitionKey() { + final long count = repository.count(); + assertThat(count).isEqualTo(4); + + Optional
addressById = repository.findById(TEST_ADDRESS1_PARTITION1.getPostalCode(), + new PartitionKey(TEST_ADDRESS1_PARTITION1.getCity())); + assertThat(addressById.isPresent()).isTrue(); + + repository.deleteById(TEST_ADDRESS1_PARTITION1.getPostalCode(), + new PartitionKey(TEST_ADDRESS1_PARTITION1.getCity())); + + final List
result = TestUtils.toList(repository.findAll()); + assertThat(result.size()).isEqualTo(3); + + addressById = repository.findById(TEST_ADDRESS1_PARTITION1.getPostalCode(), + new PartitionKey(TEST_ADDRESS1_PARTITION1.getCity())); + + assertThat(addressById.isPresent()).isFalse(); + } + + @Test + public void testFindAllByPartitionKey() { + List
findAll = + repository.findAll(new PartitionKey(TEST_ADDRESS1_PARTITION1.getCity())); + // Since there are two addresses with partition1 + assertThat(findAll.size()).isEqualTo(2); + assertThat(findAll.containsAll(Lists.newArrayList(TEST_ADDRESS1_PARTITION1, + TEST_ADDRESS2_PARTITION1))).isTrue(); + + findAll = repository.findAll(new PartitionKey(TEST_ADDRESS1_PARTITION2.getCity())); + // Since there is one address with partition2 + assertThat(findAll.size()).isEqualTo(1); + assertThat(findAll.contains(TEST_ADDRESS1_PARTITION2)).isTrue(); + + + findAll = repository.findAll(new PartitionKey(TEST_ADDRESS4_PARTITION3.getCity())); + // Since there is one address with partition3 + assertThat(findAll.size()).isEqualTo(1); + assertThat(findAll.contains(TEST_ADDRESS4_PARTITION3)).isTrue(); + } + + @Test + public void testUpdateEntity() { + final Address updatedAddress = new Address(TEST_ADDRESS1_PARTITION1.getPostalCode(), TestConstants.NEW_STREET, + TEST_ADDRESS1_PARTITION1.getCity()); + + repository.save(updatedAddress); + + final List
results = + repository.findByPostalCodeAndCity(updatedAddress.getPostalCode(), updatedAddress.getCity()); + + assertThat(results.size()).isEqualTo(1); + assertThat(results.get(0).getStreet()).isEqualTo(updatedAddress.getStreet()); + assertThat(results.get(0).getPostalCode()).isEqualTo(updatedAddress.getPostalCode()); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/ContactRepositoryIT.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/ContactRepositoryIT.java new file mode 100644 index 000000000000..ca4368e987c0 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/ContactRepositoryIT.java @@ -0,0 +1,202 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.integration; + +import com.microsoft.azure.spring.data.cosmosdb.common.TestUtils; +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosTemplate; +import com.microsoft.azure.spring.data.cosmosdb.domain.Contact; +import com.microsoft.azure.spring.data.cosmosdb.repository.TestRepositoryConfig; +import com.microsoft.azure.spring.data.cosmosdb.repository.repository.ContactRepository; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.assertj.core.util.Lists; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class ContactRepositoryIT { + + private static final Contact TEST_CONTACT = new Contact("testId", "faketitle"); + + private static final CosmosEntityInformation entityInformation = + new CosmosEntityInformation<>(Contact.class); + + private static CosmosTemplate staticTemplate; + private static boolean isSetupDone; + + @Autowired + ContactRepository repository; + + @Autowired + private CosmosTemplate template; + + @Before + public void setUp() { + if (!isSetupDone) { + staticTemplate = template; + template.createContainerIfNotExists(entityInformation); + } + repository.save(TEST_CONTACT); + isSetupDone = true; + } + + @After + public void cleanup() { + repository.deleteAll(); + } + + @AfterClass + public static void afterClassCleanup() { + staticTemplate.deleteContainer(entityInformation.getContainerName()); + } + + @Test + public void testFindAll() { + final List result = TestUtils.toList(repository.findAll()); + + assertThat(result.size()).isEqualTo(1); + assertThat(result.get(0).getLogicId()).isEqualTo(TEST_CONTACT.getLogicId()); + assertThat(result.get(0).getTitle()).isEqualTo(TEST_CONTACT.getTitle()); + + final Contact contact = repository.findById(TEST_CONTACT.getLogicId()).get(); + + assertThat(contact.getLogicId()).isEqualTo(TEST_CONTACT.getLogicId()); + assertThat(contact.getTitle()).isEqualTo(TEST_CONTACT.getTitle()); + } + + @Test + public void testCountAndDeleteByID() { + final Contact contact2 = new Contact("newid", "newtitle"); + repository.save(contact2); + final List all = TestUtils.toList(repository.findAll()); + assertThat(all.size()).isEqualTo(2); + + long count = repository.count(); + assertThat(count).isEqualTo(2); + + repository.deleteById(contact2.getLogicId()); + + final List result = TestUtils.toList(repository.findAll()); + + assertThat(result.size()).isEqualTo(1); + assertThat(result.get(0).getLogicId()).isEqualTo(TEST_CONTACT.getLogicId()); + assertThat(result.get(0).getTitle()).isEqualTo(TEST_CONTACT.getTitle()); + + count = repository.count(); + assertThat(count).isEqualTo(1); + } + + @Test + public void testCountAndDeleteEntity() { + final Contact contact2 = new Contact("newid", "newtitle"); + repository.save(contact2); + final List all = TestUtils.toList(repository.findAll()); + assertThat(all.size()).isEqualTo(2); + + repository.delete(contact2); + + final List result = TestUtils.toList(repository.findAll()); + + assertThat(result.size()).isEqualTo(1); + assertThat(result.get(0).getLogicId()).isEqualTo(TEST_CONTACT.getLogicId()); + assertThat(result.get(0).getTitle()).isEqualTo(TEST_CONTACT.getTitle()); + } + + @Test + public void testUpdateEntity() { + final Contact updatedContact = new Contact(TEST_CONTACT.getLogicId(), "updated"); + + final Contact savedContact = repository.save(updatedContact); + + // Test save operation return saved entity + assertThat(savedContact.getLogicId()).isEqualTo(updatedContact.getLogicId()); + assertThat(savedContact.getTitle()).isEqualTo(updatedContact.getTitle()); + + final Contact contact = repository.findById(TEST_CONTACT.getLogicId()).get(); + + assertThat(contact.getLogicId()).isEqualTo(updatedContact.getLogicId()); + assertThat(contact.getTitle()).isEqualTo(updatedContact.getTitle()); + } + + @Test + public void testBatchOperations() { + + final Contact contact1 = new Contact("newid1", "newtitle"); + final Contact contact2 = new Contact("newid2", "newtitle"); + final ArrayList contacts = new ArrayList(); + contacts.add(contact1); + contacts.add(contact2); + final Iterable savedContacts = repository.saveAll(contacts); + + final AtomicInteger savedCount = new AtomicInteger(); + savedContacts.forEach(se -> { + savedCount.incrementAndGet(); + assertThat(contacts.contains(se)).isTrue(); + }); + + assertThat(savedCount.get()).isEqualTo(contacts.size()); + + final ArrayList ids = new ArrayList(); + ids.add(contact1.getLogicId()); + ids.add(contact2.getLogicId()); + final List result = Lists.newArrayList(repository.findAllById(ids)); + + assertThat(result.size()).isEqualTo(2); + + repository.deleteAll(contacts); + + final List result2 = Lists.newArrayList(repository.findAllById(ids)); + assertThat(result2.size()).isEqualTo(0); + } + + @Test + public void testCustomQuery() { + final List result = repository.findByTitle(TEST_CONTACT.getTitle()); + + assertThat(result.size()).isEqualTo(1); + assertThat(result.get(0).getLogicId()).isEqualTo(TEST_CONTACT.getLogicId()); + assertThat(result.get(0).getTitle()).isEqualTo(TEST_CONTACT.getTitle()); + + } + + @Test + @Ignore // TODO(kuthapar): v3 doesn't support creation of items without id. + public void testNullIdContact() { + final Contact nullIdContact = new Contact(null, "testTitile"); + final Contact savedContact = repository.save(nullIdContact); + + Assert.assertNotNull(savedContact.getLogicId()); + Assert.assertEquals(nullIdContact.getTitle(), savedContact.getTitle()); + } + + @Test + public void testFindById() { + final Optional optional = repository.findById(TEST_CONTACT.getLogicId()); + + Assert.assertTrue(optional.isPresent()); + Assert.assertEquals(TEST_CONTACT, optional.get()); + Assert.assertFalse(repository.findById("").isPresent()); + } + + @Test + public void testFindByIdNotFound() { + final Optional optional = repository.findById("unknown-id"); + + Assert.assertFalse(optional.isPresent()); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/CosmosAnnotationIT.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/CosmosAnnotationIT.java new file mode 100644 index 000000000000..943dfe2072bc --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/CosmosAnnotationIT.java @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.integration; + +import com.azure.data.cosmos.CosmosContainerProperties; +import com.azure.data.cosmos.IndexingPolicy; +import com.microsoft.azure.spring.data.cosmosdb.CosmosDbFactory; +import com.microsoft.azure.spring.data.cosmosdb.common.TestConstants; +import com.microsoft.azure.spring.data.cosmosdb.common.TestUtils; +import com.microsoft.azure.spring.data.cosmosdb.config.CosmosDBConfig; +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosTemplate; +import com.microsoft.azure.spring.data.cosmosdb.core.convert.MappingCosmosConverter; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.CosmosMappingContext; +import com.microsoft.azure.spring.data.cosmosdb.domain.Role; +import com.microsoft.azure.spring.data.cosmosdb.domain.TimeToLiveSample; +import com.microsoft.azure.spring.data.cosmosdb.repository.TestRepositoryConfig; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.domain.EntityScanner; +import org.springframework.context.ApplicationContext; +import org.springframework.data.annotation.Persistent; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.util.Assert; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class CosmosAnnotationIT { + private static final Role TEST_ROLE = new Role(TestConstants.ID_1, TestConstants.LEVEL, + TestConstants.ROLE_NAME); + + @Autowired + private ApplicationContext applicationContext; + @Autowired + private CosmosDBConfig dbConfig; + + private static CosmosTemplate cosmosTemplate; + private static CosmosContainerProperties collectionRole; + private static CosmosContainerProperties collectionSample; + private static CosmosEntityInformation roleInfo; + private static CosmosEntityInformation sampleInfo; + + private static boolean initialized; + + @Before + public void setUp() throws ClassNotFoundException { + if (!initialized) { + final CosmosDbFactory cosmosDbFactory = new CosmosDbFactory(dbConfig); + + roleInfo = new CosmosEntityInformation<>(Role.class); + sampleInfo = new CosmosEntityInformation<>(TimeToLiveSample.class); + final CosmosMappingContext dbContext = new CosmosMappingContext(); + + dbContext.setInitialEntitySet(new EntityScanner(this.applicationContext).scan(Persistent.class)); + + final MappingCosmosConverter mappingConverter = new MappingCosmosConverter(dbContext, null); + + cosmosTemplate = new CosmosTemplate(cosmosDbFactory, mappingConverter, dbConfig.getDatabase()); + initialized = true; + } + collectionRole = cosmosTemplate.createContainerIfNotExists(roleInfo); + + collectionSample = cosmosTemplate.createContainerIfNotExists(sampleInfo); + + cosmosTemplate.insert(roleInfo.getContainerName(), TEST_ROLE, null); + } + + @AfterClass + public static void afterClassCleanup() { + cosmosTemplate.deleteContainer(roleInfo.getContainerName()); + cosmosTemplate.deleteContainer(sampleInfo.getContainerName()); + } + + @Test + public void testTimeToLiveAnnotation() { + Integer timeToLive = sampleInfo.getTimeToLive(); + assertThat(timeToLive).isEqualTo(collectionSample.defaultTimeToLive()); + + timeToLive = roleInfo.getTimeToLive(); + assertThat(timeToLive).isEqualTo(collectionRole.defaultTimeToLive()); + } + + @Test + @Ignore // TODO(kuthapar): Ignore this test case for now, will update this from service update. + public void testIndexingPolicyAnnotation() { + final IndexingPolicy policy = collectionRole.indexingPolicy(); + + Assert.isTrue(policy.indexingMode() == TestConstants.INDEXINGPOLICY_MODE, + "unmatched collection policy indexing mode of class Role"); + Assert.isTrue(policy.automatic() == TestConstants.INDEXINGPOLICY_AUTOMATIC, + "unmatched collection policy automatic of class Role"); + + TestUtils.testIndexingPolicyPathsEquals(policy.includedPaths(), TestConstants.INCLUDEDPATHS); + TestUtils.testIndexingPolicyPathsEquals(policy.excludedPaths(), TestConstants.EXCLUDEDPATHS); + } +} + diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/CustomerRepositoryIT.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/CustomerRepositoryIT.java new file mode 100644 index 000000000000..c476c4a57d0e --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/CustomerRepositoryIT.java @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.integration; + +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosTemplate; +import com.microsoft.azure.spring.data.cosmosdb.domain.Customer; +import com.microsoft.azure.spring.data.cosmosdb.repository.TestRepositoryConfig; +import com.microsoft.azure.spring.data.cosmosdb.repository.repository.CustomerRepository; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.NonNull; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class CustomerRepositoryIT { + + private static final String USER_NAME_0 = "username-0"; + private static final String USER_NAME_1 = "username-1"; + private static final String FAKE_USER_NAME = "username-fake"; + + private static final Long USER_AGE_0 = 34L; + private static final Long USER_AGE_1 = 45L; + + private static final String CUSTOMER_ID_0 = "id-0"; + private static final String CUSTOMER_ID_1 = "id-1"; + private static final String CUSTOMER_ID_2 = "id-2"; + + private static final Long CUSTOMER_LEVEL_0 = 1L; + private static final Long CUSTOMER_LEVEL_1 = 2L; + + private static final Customer.User USER_0 = new Customer.User(USER_NAME_0, USER_AGE_0); + private static final Customer.User USER_1 = new Customer.User(USER_NAME_1, USER_AGE_1); + private static final Customer.User USER_2 = new Customer.User(USER_NAME_0, USER_AGE_1); + + private static final Customer CUSTOMER_0 = new Customer(CUSTOMER_ID_0, CUSTOMER_LEVEL_0, USER_0); + private static final Customer CUSTOMER_1 = new Customer(CUSTOMER_ID_1, CUSTOMER_LEVEL_1, USER_1); + private static final Customer CUSTOMER_2 = new Customer(CUSTOMER_ID_2, CUSTOMER_LEVEL_1, USER_2); + + private static final CosmosEntityInformation entityInformation = + new CosmosEntityInformation<>(Customer.class); + + private static CosmosTemplate staticTemplate; + private static boolean isSetupDone; + + @Autowired + private CustomerRepository repository; + + @Autowired + private CosmosTemplate template; + + @Before + public void setUp() { + if (!isSetupDone) { + staticTemplate = template; + template.createContainerIfNotExists(entityInformation); + } + this.repository.saveAll(Arrays.asList(CUSTOMER_0, CUSTOMER_1, CUSTOMER_2)); + isSetupDone = true; + } + + @After + public void cleanup() { + this.repository.deleteAll(); + } + + @AfterClass + public static void afterClassCleanup() { + staticTemplate.deleteContainer(entityInformation.getContainerName()); + } + + private void assertCustomerListEquals(@NonNull List customers, @NonNull List reference) { + Assert.assertEquals(reference.size(), customers.size()); + + customers.sort(Comparator.comparing(Customer::getId)); + reference.sort(Comparator.comparing(Customer::getId)); + + Assert.assertEquals(reference, customers); + } + + @Test + public void testFindByUserAndLevel() { + final List references = Arrays.asList(CUSTOMER_0, CUSTOMER_2); + List customers = this.repository.findByUser_Name(USER_NAME_0); + + assertCustomerListEquals(references, customers); + + customers = this.repository.findByUser_Name(FAKE_USER_NAME); + + Assert.assertTrue(customers.isEmpty()); + } + +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/IntegerIdDomainRepositoryIT.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/IntegerIdDomainRepositoryIT.java new file mode 100644 index 000000000000..67573dd759f6 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/IntegerIdDomainRepositoryIT.java @@ -0,0 +1,268 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.integration; + +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosTemplate; +import com.microsoft.azure.spring.data.cosmosdb.core.query.CosmosPageRequest; +import com.microsoft.azure.spring.data.cosmosdb.domain.IntegerIdDomain; +import com.microsoft.azure.spring.data.cosmosdb.exception.CosmosDBAccessException; +import com.microsoft.azure.spring.data.cosmosdb.repository.TestRepositoryConfig; +import com.microsoft.azure.spring.data.cosmosdb.repository.repository.IntegerIdDomainRepository; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Sort; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class IntegerIdDomainRepositoryIT { + + private static final Integer ID = 231234; + private static final String NAME = "panli"; + private static final IntegerIdDomain DOMAIN = new IntegerIdDomain(ID, NAME); + + private static final CosmosEntityInformation entityInformation = + new CosmosEntityInformation<>(IntegerIdDomain.class); + + private static CosmosTemplate staticTemplate; + private static boolean isSetupDone; + + @Autowired + private CosmosTemplate template; + + @Autowired + private IntegerIdDomainRepository repository; + + @Before + public void setUp() { + if (!isSetupDone) { + staticTemplate = template; + template.createContainerIfNotExists(entityInformation); + } + this.repository.save(DOMAIN); + isSetupDone = true; + } + + @After + public void cleanup() { + this.repository.deleteAll(); + } + + @AfterClass + public static void afterClassCleanup() { + staticTemplate.deleteContainer(entityInformation.getContainerName()); + } + + @Test + public void testIntegerIdDomain() { + this.repository.deleteAll(); + Assert.assertFalse(this.repository.findById(ID).isPresent()); + + this.repository.save(DOMAIN); + final Optional foundOptional = this.repository.findById(ID); + + Assert.assertTrue(foundOptional.isPresent()); + Assert.assertEquals(DOMAIN.getNumber(), foundOptional.get().getNumber()); + Assert.assertEquals(DOMAIN.getName(), foundOptional.get().getName()); + + this.repository.delete(DOMAIN); + + Assert.assertFalse(this.repository.findById(ID).isPresent()); + } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidDomain() { + new CosmosEntityInformation(InvalidDomain.class); + } + + @Test + public void testBasicQuery() { + final IntegerIdDomain save = this.repository.save(DOMAIN); + Assert.assertNotNull(save); + } + + @Test + public void testSaveAndFindById() { + Assert.assertNotNull(this.repository.save(DOMAIN)); + + final Optional savedEntity = this.repository.findById(DOMAIN.getNumber()); + Assert.assertTrue(savedEntity.isPresent()); + Assert.assertEquals(DOMAIN, savedEntity.get()); + } + + @Test + public void testSaveAllAndFindAll() { + Assert.assertTrue(this.repository.findAll().iterator().hasNext()); + + final Set entitiesToSave = Collections.singleton(DOMAIN); + this.repository.saveAll(entitiesToSave); + + final Set savedEntities = StreamSupport.stream(this.repository.findAll().spliterator(), false) + .collect(Collectors.toSet()); + + Assert.assertTrue(entitiesToSave.containsAll(savedEntities)); + } + + @Test + @Ignore // TODO(kuthapar): findById IN clause not working in case of Integer + public void testFindAllById() { + final Iterable allById = + this.repository.findAllById(Collections.singleton(DOMAIN.getNumber())); + Assert.assertTrue(allById.iterator().hasNext()); + } + + @Test + public void testCount() { + Assert.assertEquals(1, repository.count()); + } + + @Test + public void testDeleteById() { + this.repository.save(DOMAIN); + this.repository.deleteById(DOMAIN.getNumber()); + Assert.assertEquals(0, this.repository.count()); + } + + @Test(expected = CosmosDBAccessException.class) + public void testDeleteByIdShouldFailIfNothingToDelete() { + this.repository.deleteAll(); + this.repository.deleteById(DOMAIN.getNumber()); + } + + @Test + public void testDelete() { + this.repository.save(DOMAIN); + this.repository.delete(DOMAIN); + Assert.assertEquals(0, this.repository.count()); + } + + @Test(expected = CosmosDBAccessException.class) + public void testDeleteShouldFailIfNothingToDelete() { + this.repository.deleteAll(); + this.repository.delete(DOMAIN); + } + + @Test + public void testDeleteAll() { + this.repository.save(DOMAIN); + this.repository.deleteAll(Collections.singleton(DOMAIN)); + Assert.assertEquals(0, this.repository.count()); + } + + @Test + public void testExistsById() { + this.repository.save(DOMAIN); + Assert.assertTrue(this.repository.existsById(DOMAIN.getNumber())); + } + + @Test + public void testFindAllSort() { + final IntegerIdDomain other = new IntegerIdDomain(DOMAIN.getNumber() + 1, "other-name"); + this.repository.save(other); + this.repository.save(DOMAIN); + + final Sort ascSort = Sort.by(Sort.Direction.ASC, "number"); + final List ascending = StreamSupport + .stream(this.repository.findAll(ascSort).spliterator(), false) + .collect(Collectors.toList()); + Assert.assertEquals(2, ascending.size()); + Assert.assertEquals(DOMAIN, ascending.get(0)); + Assert.assertEquals(other, ascending.get(1)); + + final Sort descSort = Sort.by(Sort.Direction.DESC, "number"); + final List descending = StreamSupport + .stream(this.repository.findAll(descSort).spliterator(), false) + .collect(Collectors.toList()); + Assert.assertEquals(2, descending.size()); + Assert.assertEquals(other, descending.get(0)); + Assert.assertEquals(DOMAIN, descending.get(1)); + + } + + @Test + public void testFindAllPageable() { + final IntegerIdDomain other = new IntegerIdDomain(DOMAIN.getNumber() + 1, "other-name"); + this.repository.save(DOMAIN); + this.repository.save(other); + + final Page page1 = this.repository.findAll(new CosmosPageRequest(0, 1, null)); + final Iterator page1Iterator = page1.iterator(); + Assert.assertTrue(page1Iterator.hasNext()); + Assert.assertEquals(DOMAIN, page1Iterator.next()); + + final Page page2 = this.repository.findAll(new CosmosPageRequest(1, 1, null)); + final Iterator page2Iterator = page2.iterator(); + Assert.assertTrue(page2Iterator.hasNext()); + Assert.assertEquals(DOMAIN, page2Iterator.next()); + } + + private static class InvalidDomain { + + private int count; + + private String location; + + InvalidDomain(int count, String location) { + this.count = count; + this.location = location; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + InvalidDomain that = (InvalidDomain) o; + return count == that.count + && Objects.equals(location, that.location); + } + + @Override + public int hashCode() { + return Objects.hash(count, location); + } + + @Override + public String toString() { + return "InvalidDomain{" + + "count=" + + count + + ", location='" + + location + + '\'' + + '}'; + } + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/MemoRepositoryIT.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/MemoRepositoryIT.java new file mode 100644 index 000000000000..4fa4053db39b --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/MemoRepositoryIT.java @@ -0,0 +1,262 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.integration; + +import com.microsoft.azure.spring.data.cosmosdb.common.TestConstants; +import com.microsoft.azure.spring.data.cosmosdb.common.TestUtils; +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosTemplate; +import com.microsoft.azure.spring.data.cosmosdb.domain.Importance; +import com.microsoft.azure.spring.data.cosmosdb.domain.Memo; +import com.microsoft.azure.spring.data.cosmosdb.exception.CosmosDBAccessException; +import com.microsoft.azure.spring.data.cosmosdb.repository.TestRepositoryConfig; +import com.microsoft.azure.spring.data.cosmosdb.repository.repository.MemoRepository; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Date; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class MemoRepositoryIT { + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat(TestConstants.DATE_FORMAT); + + private static Date memoDate; + private static Date memoDateBefore; + private static Date memoDateAfter; + private static Date futureDate1; + private static Date futureDate2; + + private static Memo testMemo1; + private static Memo testMemo2; + private static Memo testMemo3; + + private static final CosmosEntityInformation entityInformation = + new CosmosEntityInformation<>(Memo.class); + + private static CosmosTemplate staticTemplate; + private static boolean isSetupDone; + + @Autowired + private CosmosTemplate template; + + @Autowired + MemoRepository repository; + + @BeforeClass + public static void init() throws ParseException { + memoDate = DATE_FORMAT.parse(TestConstants.DATE_STRING); + memoDateBefore = DATE_FORMAT.parse(TestConstants.DATE_BEFORE_STRING); + memoDateAfter = DATE_FORMAT.parse(TestConstants.DATE_AFTER_STRING); + futureDate1 = DATE_FORMAT.parse(TestConstants.DATE_FUTURE_STRING_1); + futureDate2 = DATE_FORMAT.parse(TestConstants.DATE_FUTURE_STRING_2); + testMemo1 = new Memo(TestConstants.ID_1, TestConstants.MESSAGE, memoDateBefore, Importance.HIGH); + testMemo2 = new Memo(TestConstants.ID_2, TestConstants.NEW_MESSAGE, memoDate, Importance.LOW); + testMemo3 = new Memo(TestConstants.ID_3, TestConstants.NEW_MESSAGE, memoDateAfter, Importance.LOW); + } + + @Before + public void setUp() { + if (!isSetupDone) { + staticTemplate = template; + template.createContainerIfNotExists(entityInformation); + } + repository.saveAll(Arrays.asList(testMemo1, testMemo2, testMemo3)); + isSetupDone = true; + } + + @After + public void cleanup() { + repository.deleteAll(); + } + + @AfterClass + public static void afterClassCleanup() { + staticTemplate.deleteContainer(entityInformation.getContainerName()); + } + + @Test + public void testFindAll() { + final List result = TestUtils.toList(repository.findAll()); + + assertThat(result.size()).isEqualTo(3); + } + + @Test + public void testFindByDate() { + final List result = repository.findMemoByDate(memoDate); + + assertThat(result.size()).isEqualTo(1); + assertMemoEquals(result.get(0), testMemo1); + } + + @Test + public void testFindByEnum() { + final List result = repository.findMemoByImportance(testMemo1.getImportance()); + + assertThat(result.size()).isEqualTo(1); + assertMemoEquals(result.get(0), testMemo1); + } + + private void assertMemoEquals(Memo actual, Memo expected) { + assertThat(actual.getId().equals(expected.getId())); + assertThat(actual.getMessage().equals(expected.getMessage())); + assertThat(actual.getDate().equals(expected.getDate())); + assertThat(actual.getImportance().equals(expected.getImportance())); + } + + @Test + public void testFindByBefore() { + List memos = this.repository.findByDateBefore(memoDateBefore); + + Assert.assertTrue(memos.isEmpty()); + + memos = this.repository.findByDateBefore(memoDate); + + Assert.assertEquals(1, memos.size()); + Assert.assertEquals(testMemo1, memos.get(0)); + + memos = this.repository.findByDateBefore(memoDateAfter); + final List reference = Arrays.asList(testMemo1, testMemo2); + + memos.sort(Comparator.comparing(Memo::getId)); + reference.sort(Comparator.comparing(Memo::getId)); + + Assert.assertEquals(reference.size(), memos.size()); + Assert.assertEquals(reference, memos); + } + + @Test + public void testFindByBeforeWithAndOr() { + List memos = this.repository.findByDateBeforeAndMessage(memoDate, TestConstants.NEW_MESSAGE); + + Assert.assertTrue(memos.isEmpty()); + + memos = this.repository.findByDateBeforeAndMessage(memoDate, TestConstants.MESSAGE); + + Assert.assertEquals(1, memos.size()); + Assert.assertEquals(testMemo1, memos.get(0)); + + memos = this.repository.findByDateBeforeOrMessage(memoDateAfter, TestConstants.MESSAGE); + final List reference = Arrays.asList(testMemo1, testMemo2); + + memos.sort(Comparator.comparing(Memo::getId)); + reference.sort(Comparator.comparing(Memo::getId)); + + Assert.assertEquals(reference.size(), memos.size()); + Assert.assertEquals(reference, memos); + } + + @Test + public void testFindByAfter() { + List memos = this.repository.findByDateAfter(memoDateAfter); + + Assert.assertTrue(memos.isEmpty()); + + memos = this.repository.findByDateAfter(memoDate); + + Assert.assertEquals(1, memos.size()); + Assert.assertEquals(testMemo3, memos.get(0)); + + memos = this.repository.findByDateAfter(memoDateBefore); + final List reference = Arrays.asList(testMemo2, testMemo3); + + memos.sort(Comparator.comparing(Memo::getId)); + reference.sort(Comparator.comparing(Memo::getId)); + + Assert.assertEquals(reference.size(), memos.size()); + Assert.assertEquals(reference, memos); + } + + @Test + public void testFindByAfterWithAndOr() { + List memos = this.repository.findByDateAfterAndMessage(memoDate, TestConstants.MESSAGE); + + Assert.assertTrue(memos.isEmpty()); + + memos = this.repository.findByDateAfterAndMessage(memoDate, TestConstants.NEW_MESSAGE); + + Assert.assertEquals(1, memos.size()); + Assert.assertEquals(testMemo3, memos.get(0)); + + memos = this.repository.findByDateAfterOrMessage(memoDateBefore, TestConstants.MESSAGE); + final List reference = Arrays.asList(testMemo1, testMemo2, testMemo3); + + memos.sort(Comparator.comparing(Memo::getId)); + reference.sort(Comparator.comparing(Memo::getId)); + + Assert.assertEquals(reference.size(), memos.size()); + Assert.assertEquals(reference, memos); + } + + @Test + public void testFindByBetween() { + List memos = this.repository + .findByDateBetween(testMemo1.getDate(), testMemo3.getDate()); + List reference = Arrays.asList(testMemo1, testMemo2, testMemo3); + + assertMemoListEquals(memos, reference); + + memos = this.repository.findByDateBetween(testMemo1.getDate(), testMemo2.getDate()); + reference = Arrays.asList(testMemo1, testMemo2); + + assertMemoListEquals(memos, reference); + + memos = this.repository.findByDateBetween(futureDate1, futureDate2); + reference = Arrays.asList(); + + assertMemoListEquals(memos, reference); + } + + @Test + public void testFindByBetweenWithAnd() { + final List memos = this.repository + .findByDateBetweenAndMessage(testMemo1.getDate(), testMemo2.getDate(), TestConstants.MESSAGE); + assertMemoListEquals(memos, Arrays.asList(testMemo1)); + } + + @Test + public void testFindByBetweenWithOr() { + final List memos = this.repository + .findByDateBetweenOrMessage(testMemo1.getDate(), testMemo2.getDate(), TestConstants.NEW_MESSAGE); + assertMemoListEquals(memos, Arrays.asList(testMemo1, testMemo2, testMemo3)); + } + + private void assertMemoListEquals(List memos, List reference) { + memos.sort(Comparator.comparing(Memo::getId)); + reference.sort(Comparator.comparing(Memo::getId)); + + Assert.assertEquals(reference.size(), memos.size()); + Assert.assertEquals(reference, memos); + } + + @Test(expected = CosmosDBAccessException.class) + @Ignore // TODO(pan): Ignore this test case for now, will update this from service update. + public void testFindByStartsWithWithException() { + repository.findByMessageStartsWith(testMemo1.getMessage()); + } + + @Test + public void testFindByStartsWith() { + final List result = repository.findByMessageStartsWith(testMemo1.getMessage().substring(0, 10)); + Assert.assertEquals(testMemo1, result.get(0)); + Assert.assertEquals(1, result.size()); + } + +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/PageableAddressRepositoryIT.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/PageableAddressRepositoryIT.java new file mode 100644 index 000000000000..7e1d9a984a34 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/PageableAddressRepositoryIT.java @@ -0,0 +1,202 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.integration; + +import com.azure.data.cosmos.CosmosClient; +import com.azure.data.cosmos.CosmosItemProperties; +import com.azure.data.cosmos.FeedOptions; +import com.azure.data.cosmos.FeedResponse; +import com.microsoft.azure.spring.data.cosmosdb.common.TestConstants; +import com.microsoft.azure.spring.data.cosmosdb.common.TestUtils; +import com.microsoft.azure.spring.data.cosmosdb.config.CosmosDBConfig; +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosTemplate; +import com.microsoft.azure.spring.data.cosmosdb.core.query.CosmosPageRequest; +import com.microsoft.azure.spring.data.cosmosdb.domain.PageableAddress; +import com.microsoft.azure.spring.data.cosmosdb.repository.TestRepositoryConfig; +import com.microsoft.azure.spring.data.cosmosdb.repository.repository.PageableAddressRepository; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.data.domain.Page; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import java.util.ArrayList; +import java.util.List; + +import static com.microsoft.azure.spring.data.cosmosdb.common.PageTestUtils.validateLastPage; +import static com.microsoft.azure.spring.data.cosmosdb.common.PageTestUtils.validateNonLastPage; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.PAGE_SIZE_1; +import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.PAGE_SIZE_3; +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class PageableAddressRepositoryIT { + private static final PageableAddress TEST_ADDRESS1_PARTITION1 = new PageableAddress( + TestConstants.POSTAL_CODE, TestConstants.STREET, TestConstants.CITY); + private static final PageableAddress TEST_ADDRESS2_PARTITION1 = new PageableAddress( + TestConstants.POSTAL_CODE_0, TestConstants.STREET, TestConstants.CITY); + private static final PageableAddress TEST_ADDRESS1_PARTITION2 = new PageableAddress( + TestConstants.POSTAL_CODE_1, TestConstants.STREET_0, TestConstants.CITY_0); + private static final PageableAddress TEST_ADDRESS4_PARTITION3 = new PageableAddress( + TestConstants.POSTAL_CODE, TestConstants.STREET_1, TestConstants.CITY_1); + + private static final CosmosEntityInformation entityInformation = + new CosmosEntityInformation<>(PageableAddress.class); + + private static CosmosTemplate staticTemplate; + private static boolean isSetupDone; + + @Autowired + private CosmosTemplate template; + + @Autowired + private PageableAddressRepository repository; + + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private CosmosDBConfig dbConfig; + + @Before + public void setUp() { + if (!isSetupDone) { + staticTemplate = template; + template.createContainerIfNotExists(entityInformation); + } + repository.save(TEST_ADDRESS1_PARTITION1); + repository.save(TEST_ADDRESS1_PARTITION2); + repository.save(TEST_ADDRESS2_PARTITION1); + repository.save(TEST_ADDRESS4_PARTITION3); + isSetupDone = true; + } + + @After + public void cleanup() { + repository.deleteAll(); + } + + @AfterClass + public static void afterClassCleanup() { + staticTemplate.deleteContainer(entityInformation.getContainerName()); + } + + @Test + public void testFindAll() { + final List result = TestUtils.toList(repository.findAll()); + + assertThat(result.size()).isEqualTo(4); + } + + @Test + public void testFindAllByPage() { + final CosmosPageRequest pageRequest = new CosmosPageRequest(0, PAGE_SIZE_3, null); + final Page page = repository.findAll(pageRequest); + + assertThat(page.getContent().size()).isLessThanOrEqualTo(PAGE_SIZE_3); + validateNonLastPage(page, PAGE_SIZE_3); + + final Page nextPage = repository.findAll(page.getPageable()); + assertThat(nextPage.getContent().size()).isLessThanOrEqualTo(PAGE_SIZE_3); + validateLastPage(nextPage, nextPage.getContent().size()); + } + + @Test + public void testFindWithPartitionKeySinglePage() { + final CosmosPageRequest pageRequest = new CosmosPageRequest(0, PAGE_SIZE_3, null); + final Page page = repository.findByCity(TestConstants.CITY, pageRequest); + + assertThat(page.getContent().size()).isEqualTo(2); + validateResultCityMatch(page, TestConstants.CITY); + validateLastPage(page, page.getContent().size()); + } + + @Test + public void testFindWithPartitionKeyMultiPages() { + final CosmosPageRequest pageRequest = new CosmosPageRequest(0, PAGE_SIZE_1, null); + final Page page = repository.findByCity(TestConstants.CITY, pageRequest); + + assertThat(page.getContent().size()).isEqualTo(PAGE_SIZE_1); + validateResultCityMatch(page, TestConstants.CITY); + validateNonLastPage(page, PAGE_SIZE_1); + + final Page nextPage = repository.findByCity(TestConstants.CITY, page.getPageable()); + + assertThat(nextPage.getContent().size()).isEqualTo(PAGE_SIZE_1); + validateResultCityMatch(page, TestConstants.CITY); + validateLastPage(nextPage, PAGE_SIZE_1); + } + + @Test + public void testFindWithoutPartitionKeySinglePage() { + final CosmosPageRequest pageRequest = new CosmosPageRequest(0, PAGE_SIZE_3, null); + final Page page = repository.findByStreet(TestConstants.STREET, pageRequest); + + assertThat(page.getContent().size()).isEqualTo(2); + validateResultStreetMatch(page, TestConstants.STREET); + validateLastPage(page, page.getContent().size()); + } + + @Test + public void testFindWithoutPartitionKeyMultiPages() { + final CosmosPageRequest pageRequest = new CosmosPageRequest(0, PAGE_SIZE_1, null); + final Page page = repository.findByStreet(TestConstants.STREET, pageRequest); + + assertThat(page.getContent().size()).isEqualTo(1); + validateResultStreetMatch(page, TestConstants.STREET); + validateNonLastPage(page, PAGE_SIZE_1); + + final Page nextPage = repository.findByStreet(TestConstants.STREET, page.getPageable()); + + assertThat(nextPage.getContent().size()).isEqualTo(PAGE_SIZE_1); + validateResultStreetMatch(page, TestConstants.STREET); + validateLastPage(nextPage, PAGE_SIZE_1); + } + + @Test + public void testOffsetAndLimit() { + final int skipCount = 2; + final int takeCount = 2; + final List results = new ArrayList<>(); + final FeedOptions options = new FeedOptions(); + options.enableCrossPartitionQuery(true); + options.maxDegreeOfParallelism(2); + + final String query = "SELECT * from c OFFSET " + skipCount + " LIMIT " + takeCount; + + final CosmosClient cosmosClient = applicationContext.getBean(CosmosClient.class); + final Flux> feedResponseFlux = + cosmosClient.getDatabase(dbConfig.getDatabase()) + .getContainer(entityInformation.getContainerName()) + .queryItems(query, options); + + StepVerifier.create(feedResponseFlux) + .consumeNextWith(cosmosItemPropertiesFeedResponse -> + results.addAll(cosmosItemPropertiesFeedResponse.results())) + .verifyComplete(); + assertThat(results.size()).isEqualTo(takeCount); + } + + private void validateResultCityMatch(Page page, String city) { + assertThat((int) page.getContent() + .stream() + .filter(address -> address.getCity().equals(city)) + .count()).isEqualTo(page.getContent().size()); + } + + private void validateResultStreetMatch(Page page, String street) { + assertThat((int) page.getContent() + .stream() + .filter(address -> address.getStreet().equals(street)) + .count()).isEqualTo(page.getContent().size()); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/PageableMemoRepositoryIT.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/PageableMemoRepositoryIT.java new file mode 100644 index 000000000000..35e4a60ee519 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/PageableMemoRepositoryIT.java @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.integration; + +import com.azure.data.cosmos.CosmosClient; +import com.azure.data.cosmos.CosmosItemProperties; +import com.azure.data.cosmos.FeedOptions; +import com.azure.data.cosmos.FeedResponse; +import com.microsoft.azure.spring.data.cosmosdb.config.CosmosDBConfig; +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosTemplate; +import com.microsoft.azure.spring.data.cosmosdb.core.query.CosmosPageRequest; +import com.microsoft.azure.spring.data.cosmosdb.domain.Importance; +import com.microsoft.azure.spring.data.cosmosdb.domain.PageableMemo; +import com.microsoft.azure.spring.data.cosmosdb.repository.TestRepositoryConfig; +import com.microsoft.azure.spring.data.cosmosdb.repository.repository.PageableMemoRepository; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class PageableMemoRepositoryIT { + + private static final int TOTAL_CONTENT_SIZE = 500; + + private static final CosmosEntityInformation entityInformation = + new CosmosEntityInformation<>(PageableMemo.class); + + private static CosmosTemplate staticTemplate; + + @Autowired + private CosmosTemplate template; + + @Autowired + private PageableMemoRepository repository; + + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private CosmosDBConfig dbConfig; + + private static Set memoSet; + + private static boolean isSetupDone; + + @Before + public void setUp() { + if (isSetupDone) { + return; + } + template.createContainerIfNotExists(entityInformation); + staticTemplate = template; + memoSet = new HashSet<>(); + final Random random = new Random(); + final Importance[] importanceValues = Importance.values(); + + // Create larger documents with size more than 10 kb + for (int i = 0; i < TOTAL_CONTENT_SIZE; i++) { + final String id = UUID.randomUUID().toString(); + final String message = UUID.randomUUID().toString(); + final int randomIndex = random.nextInt(3); + final PageableMemo memo = new PageableMemo(id, message, new Date(), importanceValues[randomIndex]); + repository.save(memo); + memoSet.add(memo); + } + isSetupDone = true; + } + + @AfterClass + public static void afterClassCleanup() { + staticTemplate.deleteContainer(entityInformation.getContainerName()); + } + + @Test + public void testFindAllWithPageSizeLessThanReturned() { + final Set memos = findAllWithPageSize(20); + assertThat(memos).isEqualTo(memoSet); + } + + @Test + public void testFindAllWithPageSizeLessThanTotal() { + final Set memos = findAllWithPageSize(200); + assertThat(memos).isEqualTo(memoSet); + } + + @Test + public void testFindAllWithPageSizeGreaterThanTotal() { + final Set memos = findAllWithPageSize(10000); + assertThat(memos).isEqualTo(memoSet); + } + + @Test + public void testOffsetAndLimitLessThanTotal() { + final int skipCount = 50; + final int takeCount = 200; + verifyItemsWithOffsetAndLimit(skipCount, takeCount, takeCount); + } + + @Test + public void testOffsetAndLimitEqualToTotal() { + final int skipCount = 100; + final int takeCount = 300; + verifyItemsWithOffsetAndLimit(skipCount, takeCount, takeCount); + } + + + @Test + public void testOffsetAndLimitGreaterThanTotal() { + final int skipCount = 300; + final int takeCount = 300; + verifyItemsWithOffsetAndLimit(skipCount, takeCount, TOTAL_CONTENT_SIZE - skipCount); + } + + private Flux> getItemsWithOffsetAndLimit(int skipCount, int takeCount) { + final FeedOptions options = new FeedOptions(); + options.enableCrossPartitionQuery(true); + options.maxDegreeOfParallelism(2); + + final String query = "SELECT * from c OFFSET " + skipCount + " LIMIT " + takeCount; + + final CosmosClient cosmosClient = applicationContext.getBean(CosmosClient.class); + return cosmosClient.getDatabase(dbConfig.getDatabase()) + .getContainer(entityInformation.getContainerName()) + .queryItems(query, options); + } + + private void verifyItemsWithOffsetAndLimit(int skipCount, int takeCount, int verifyCount) { + final List itemsWithOffsetAndLimit = new ArrayList<>(); + final Flux> itemsWithOffsetAndLimitFlux = + getItemsWithOffsetAndLimit(skipCount, takeCount); + StepVerifier.create(itemsWithOffsetAndLimitFlux) + .thenConsumeWhile(cosmosItemPropertiesFeedResponse -> { + itemsWithOffsetAndLimit.addAll(cosmosItemPropertiesFeedResponse.results()); + return true; + }) + .verifyComplete(); + assertThat(itemsWithOffsetAndLimit.size()).isEqualTo(verifyCount); + } + + private Set findAllWithPageSize(int pageSize) { + final CosmosPageRequest pageRequest = new CosmosPageRequest(0, pageSize, null); + Page page = repository.findAll(pageRequest); + final Set outputSet = new HashSet<>(page.getContent()); + while (page.hasNext()) { + final Pageable pageable = page.nextPageable(); + page = repository.findAll(pageable); + outputSet.addAll((page.getContent())); + } + return outputSet; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/PageablePersonRepositoryIT.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/PageablePersonRepositoryIT.java new file mode 100644 index 000000000000..bea46dbc742e --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/PageablePersonRepositoryIT.java @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.integration; + +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosTemplate; +import com.microsoft.azure.spring.data.cosmosdb.core.query.CosmosPageRequest; +import com.microsoft.azure.spring.data.cosmosdb.domain.Address; +import com.microsoft.azure.spring.data.cosmosdb.domain.PageablePerson; +import com.microsoft.azure.spring.data.cosmosdb.repository.TestRepositoryConfig; +import com.microsoft.azure.spring.data.cosmosdb.repository.repository.PageablePersonRepository; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class PageablePersonRepositoryIT { + + private static final int TOTAL_CONTENT_SIZE = 50; + + private static final CosmosEntityInformation entityInformation = + new CosmosEntityInformation<>(PageablePerson.class); + + private static CosmosTemplate staticTemplate; + + @Autowired + private CosmosTemplate template; + + @Autowired + private PageablePersonRepository repository; + + private static Set personSet; + + private static boolean isSetupDone; + + @Before + public void setUp() { + if (isSetupDone) { + return; + } + personSet = new HashSet<>(); + template.createContainerIfNotExists(entityInformation); + staticTemplate = template; + + // Create larger documents with size more than 10 kb + for (int i = 0; i < TOTAL_CONTENT_SIZE; i++) { + final List hobbies = new ArrayList<>(); + hobbies.add(StringUtils.repeat("hobbies-" + UUID.randomUUID().toString(), + (int) FileUtils.ONE_KB * 10)); + final List
address = new ArrayList<>(); + address.add(new Address("postalCode-" + UUID.randomUUID().toString(), + "street-" + UUID.randomUUID().toString(), + "city-" + UUID.randomUUID().toString())); + final PageablePerson person = new PageablePerson(UUID.randomUUID().toString(), + UUID.randomUUID().toString(), UUID.randomUUID().toString(), + hobbies, address); + repository.save(person); + personSet.add(person); + } + isSetupDone = true; + } + + @AfterClass + public static void afterClassCleanup() { + staticTemplate.deleteContainer(entityInformation.getContainerName()); + } + + // Cosmos DB can return any number of documents less than or equal to requested page size + // Because of available RUs, the number of return documents vary. + // With documents more than 10 KB, and collection RUs 400, + // it usually return documents less than total content size. + + // This test covers the case where page size is greater than returned documents + @Test + public void testFindAllWithPageSizeGreaterThanReturned() { + final Set outputSet = findAllWithPageSize(30, false); + assertThat(outputSet).isEqualTo(personSet); + } + + // This test covers the case where page size is less than returned documents + @Test + public void testFindAllWithPageSizeLessThanReturned() { + final Set outputSet = findAllWithPageSize(5, false); + assertThat(outputSet).isEqualTo(personSet); + } + + // This test covers the case where page size is greater than total number of documents + @Test + public void testFindAllWithPageSizeGreaterThanTotal() { + final Set outputSet = findAllWithPageSize(120, true); + assertThat(outputSet).isEqualTo(personSet); + } + + private Set findAllWithPageSize(int pageSize, boolean checkContentLimit) { + final CosmosPageRequest pageRequest = new CosmosPageRequest(0, pageSize, null); + Page page = repository.findAll(pageRequest); + final Set outputSet = new HashSet<>(page.getContent()); + if (checkContentLimit) { + // Make sure CosmosDB returns less number of documents than requested + // This will verify the functionality of new pagination implementation + assertThat(page.getContent().size()).isLessThan(pageSize); + } + while (page.hasNext()) { + final Pageable pageable = page.nextPageable(); + page = repository.findAll(pageable); + outputSet.addAll((page.getContent())); + } + return outputSet; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/PersonRepositoryIT.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/PersonRepositoryIT.java new file mode 100644 index 000000000000..1ffa71649534 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/PersonRepositoryIT.java @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.integration; + +public class PersonRepositoryIT { +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/ProjectRepositoryIT.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/ProjectRepositoryIT.java new file mode 100644 index 000000000000..1e0475d9257d --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/ProjectRepositoryIT.java @@ -0,0 +1,526 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.integration; + +import com.azure.data.cosmos.PartitionKey; +import com.google.common.collect.Lists; +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosTemplate; +import com.microsoft.azure.spring.data.cosmosdb.domain.Project; +import com.microsoft.azure.spring.data.cosmosdb.repository.TestRepositoryConfig; +import com.microsoft.azure.spring.data.cosmosdb.repository.repository.ProjectRepository; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.NonNull; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class ProjectRepositoryIT { + + private static final String ID_0 = "id-0"; + private static final String ID_1 = "id-1"; + private static final String ID_2 = "id-2"; + private static final String ID_3 = "id-3"; + private static final String ID_4 = "id-4"; + + private static final String NAME_0 = "name-0"; + private static final String NAME_1 = "name-1"; + private static final String NAME_2 = "name-2"; + private static final String NAME_3 = "name-3"; + private static final String FAKE_NAME = "fake-name"; + + private static final String CREATOR_0 = "creator-0"; + private static final String CREATOR_1 = "creator-1"; + private static final String CREATOR_2 = "creator-2"; + private static final String CREATOR_3 = "creator-3"; + private static final String FAKE_CREATOR = "fake-creator"; + + private static final Long STAR_COUNT_MIN = -1L; + private static final Long STAR_COUNT_0 = 0L; + private static final Long STAR_COUNT_1 = 1L; + private static final Long STAR_COUNT_2 = 2L; + private static final Long STAR_COUNT_3 = 3L; + private static final Long STAR_COUNT_MAX = 100L; + + private static final Long FORK_COUNT_0 = 0L; + private static final Long FORK_COUNT_1 = 1L; + private static final Long FORK_COUNT_2 = 2L; + private static final Long FORK_COUNT_3 = 3L; + private static final Long FAKE_COUNT = 123234L; + private static final Long FORK_COUNT_MAX = 100L; + + private static final Project PROJECT_0 = new Project(ID_0, NAME_0, CREATOR_0, true, STAR_COUNT_0, FORK_COUNT_0); + private static final Project PROJECT_1 = new Project(ID_1, NAME_1, CREATOR_1, true, STAR_COUNT_1, FORK_COUNT_1); + private static final Project PROJECT_2 = new Project(ID_2, NAME_2, CREATOR_2, true, STAR_COUNT_2, FORK_COUNT_2); + private static final Project PROJECT_3 = new Project(ID_3, NAME_3, CREATOR_3, true, STAR_COUNT_3, FORK_COUNT_3); + private static final Project PROJECT_4 = new Project(ID_4, NAME_0, CREATOR_0, false, STAR_COUNT_0, FORK_COUNT_0); + + private static final List PROJECTS = Arrays.asList(PROJECT_0, PROJECT_1, PROJECT_2, PROJECT_3, PROJECT_4); + + private static final CosmosEntityInformation entityInformation = + new CosmosEntityInformation<>(Project.class); + + private static CosmosTemplate staticTemplate; + private static boolean isSetupDone; + + @Autowired + private CosmosTemplate template; + + @Autowired + private ProjectRepository repository; + + @Before + public void setUp() { + if (!isSetupDone) { + staticTemplate = template; + template.createContainerIfNotExists(entityInformation); + } + this.repository.saveAll(PROJECTS); + isSetupDone = true; + } + + @After + public void cleanup() { + this.repository.deleteAll(); + } + + @AfterClass + public static void afterClassCleanup() { + staticTemplate.deleteContainer(entityInformation.getContainerName()); + } + + private void assertProjectListEquals(@NonNull List projects, @NonNull List reference) { + Assert.assertEquals(reference.size(), projects.size()); + + projects.sort(Comparator.comparing(Project::getId)); + reference.sort(Comparator.comparing(Project::getId)); + + Assert.assertEquals(reference, projects); + } + + @Test + public void testFindByWithAnd() { + List projects = this.repository.findByNameAndStarCount(NAME_1, STAR_COUNT_1); + + assertProjectListEquals(projects, Collections.singletonList(PROJECT_1)); + + projects = this.repository.findByNameAndStarCount(NAME_0, STAR_COUNT_1); + + Assert.assertTrue(projects.isEmpty()); + + projects = this.repository.findByNameAndStarCount(NAME_0, STAR_COUNT_0); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_4)); + } + + @Test + public void testFindByWithOr() { + List projects = this.repository.findByNameOrForkCount(NAME_2, STAR_COUNT_2); + + assertProjectListEquals(projects, Collections.singletonList(PROJECT_2)); + + projects = this.repository.findByNameOrForkCount(FAKE_NAME, FAKE_COUNT); + + Assert.assertTrue(projects.isEmpty()); + + projects = this.repository.findByNameOrForkCount(NAME_0, FORK_COUNT_1); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_1, PROJECT_4)); + } + + @Test + public void testFindByWithAndPartition() { + List projects = this.repository.findByNameAndCreator(NAME_1, CREATOR_1); + + assertProjectListEquals(projects, Collections.singletonList(PROJECT_1)); + + projects = this.repository.findByNameAndCreator(NAME_0, CREATOR_1); + + Assert.assertTrue(projects.isEmpty()); + + projects = this.repository.findByNameAndCreator(NAME_0, CREATOR_0); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_4)); + } + + @Test + public void testFindByWithOrPartition() { + List projects = this.repository.findByNameOrCreator(NAME_2, CREATOR_2); + + assertProjectListEquals(projects, Collections.singletonList(PROJECT_2)); + + projects = this.repository.findByNameOrCreator(FAKE_NAME, FAKE_CREATOR); + + Assert.assertTrue(projects.isEmpty()); + + projects = this.repository.findByNameOrCreator(NAME_0, CREATOR_1); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_1, PROJECT_4)); + } + + @Test + public void testFindByWithAndOr() { + List projects = repository.findByNameAndCreatorOrForkCount(NAME_0, CREATOR_1, FORK_COUNT_2); + + assertProjectListEquals(projects, Collections.singletonList(PROJECT_2)); + + projects = repository.findByNameAndCreatorOrForkCount(NAME_1, CREATOR_2, FAKE_COUNT); + + Assert.assertTrue(projects.isEmpty()); + + projects = repository.findByNameAndCreatorOrForkCount(NAME_1, CREATOR_1, FORK_COUNT_2); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_1, PROJECT_2)); + } + + @Test + public void testFindByWithOrAnd() { + List projects = repository.findByNameOrCreatorAndForkCount(NAME_0, CREATOR_1, FORK_COUNT_2); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_4)); + + projects = repository.findByNameOrCreatorAndForkCount(FAKE_NAME, CREATOR_1, FORK_COUNT_2); + + Assert.assertTrue(projects.isEmpty()); + + projects = repository.findByNameOrCreatorAndForkCount(NAME_1, CREATOR_2, FORK_COUNT_2); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_1, PROJECT_2)); + } + + @Test + public void testFindByWithOrOr() { + List projects = repository.findByNameOrCreatorOrForkCount(NAME_0, CREATOR_1, FORK_COUNT_2); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_1, PROJECT_2, PROJECT_4)); + + projects = repository.findByNameOrCreatorOrForkCount(FAKE_NAME, FAKE_CREATOR, FAKE_COUNT); + + Assert.assertTrue(projects.isEmpty()); + } + + @Test + public void testFindByWithOrAndOr() { + List projects = repository.findByNameOrCreatorAndForkCountOrStarCount(NAME_1, CREATOR_0, + FORK_COUNT_2, STAR_COUNT_3); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_1, PROJECT_3)); + + projects = repository.findByNameOrCreatorAndForkCountOrStarCount(NAME_1, CREATOR_0, FORK_COUNT_0, STAR_COUNT_3); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_1, PROJECT_3, PROJECT_4)); + + projects = repository.findByNameOrCreatorAndForkCountOrStarCount(FAKE_NAME, CREATOR_1, + FORK_COUNT_0, FAKE_COUNT); + + Assert.assertTrue(projects.isEmpty()); + } + + @Test + public void testFindByGreaterThan() { + List projects = repository.findByForkCountGreaterThan(FORK_COUNT_1); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_2, PROJECT_3)); + + projects = repository.findByForkCountGreaterThan(FAKE_COUNT); + + Assert.assertTrue(projects.isEmpty()); + } + + @Test + public void testFindByGreaterThanWithAndOr() { + List projects = repository.findByCreatorAndForkCountGreaterThan(CREATOR_2, FORK_COUNT_1); + + assertProjectListEquals(projects, Collections.singletonList(PROJECT_2)); + + projects = repository.findByCreatorAndForkCountGreaterThan(CREATOR_0, FORK_COUNT_1); + + Assert.assertTrue(projects.isEmpty()); + + projects = repository.findByCreatorOrForkCountGreaterThan(CREATOR_0, FORK_COUNT_2); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_3, PROJECT_4)); + } + + @Test + public void testFindByLessThan() { + List projects = repository.findByStarCountLessThan(STAR_COUNT_0); + + Assert.assertTrue(projects.isEmpty()); + + projects = repository.findByStarCountLessThan(STAR_COUNT_2); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_1, PROJECT_4)); + } + + @Test + public void testFindByLessThanEqual() { + List projects = repository.findByForkCountLessThanEqual(STAR_COUNT_MIN); + + Assert.assertTrue(projects.isEmpty()); + + projects = repository.findByForkCountLessThanEqual(STAR_COUNT_2); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_1, PROJECT_2, PROJECT_4)); + } + + @Test + public void testFindByLessThanAndGreaterThan() { + List projects = repository.findByStarCountLessThanAndForkCountGreaterThan(STAR_COUNT_0, FORK_COUNT_3); + + Assert.assertTrue(projects.isEmpty()); + + projects = repository.findByStarCountLessThanAndForkCountGreaterThan(STAR_COUNT_3, FORK_COUNT_0); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_1, PROJECT_2)); + } + + @Test + public void testFindByLessThanEqualsAndGreaterThanEquals() { + List projects = repository.findByForkCountLessThanEqualAndStarCountGreaterThan( + STAR_COUNT_MIN, FORK_COUNT_0); + + Assert.assertTrue(projects.isEmpty()); + + projects = repository.findByForkCountLessThanEqualAndStarCountGreaterThan(STAR_COUNT_3, FORK_COUNT_0); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_1, PROJECT_2, PROJECT_3)); + } + + @Test + public void testFindByGreaterThanEqual() { + List projects = repository.findByStarCountGreaterThanEqual(STAR_COUNT_MAX); + + Assert.assertTrue(projects.isEmpty()); + + projects = repository.findByStarCountGreaterThanEqual(STAR_COUNT_2); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_2, PROJECT_3)); + } + + @Test + public void testFindByGreaterThanEqualAnd() { + List projects = repository.findByForkCountGreaterThanEqualAndCreator(FORK_COUNT_MAX, CREATOR_2); + + Assert.assertTrue(projects.isEmpty()); + + projects = repository.findByForkCountGreaterThanEqualAndCreator(FORK_COUNT_0, CREATOR_0); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_4)); + } + + @Test + public void testFindByTrue() { + final List projects = repository.findByHasReleasedTrue(); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_1, PROJECT_2, PROJECT_3)); + } + + @Test + public void testFindByFalse() { + final List projects = repository.findByHasReleasedFalse(); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_4)); + } + + @Test + public void testFindByTrueFalseWithAnd() { + List projects = repository.findByHasReleasedTrueAndCreator(CREATOR_3); + assertProjectListEquals(projects, Arrays.asList(PROJECT_3)); + + projects = repository.findByHasReleasedFalseAndCreator(CREATOR_3); + assertProjectListEquals(projects, Arrays.asList()); + } + + @Test + public void testFindByTrueFalseWithOr() { + List projects = repository.findByHasReleasedTrueOrCreator(CREATOR_0); + assertProjectListEquals(projects, PROJECTS); + + projects = repository.findByHasReleasedFalseOrCreator(CREATOR_3); + assertProjectListEquals(projects, Arrays.asList(PROJECT_3, PROJECT_4)); + } + + @Test + public void findByIdWithPartitionKey() { + final Optional project = repository.findById(PROJECT_0.getId(), + new PartitionKey(entityInformation.getPartitionKeyFieldValue(PROJECT_0))); + + Assert.assertTrue(project.isPresent()); + + Assert.assertEquals(project.get(), PROJECT_0); + } + + @Test + public void findByIdWithPartitionKeyNotFound() { + final Optional project = repository.findById("unknown-id", + new PartitionKey("unknown-partition-key")); + + Assert.assertFalse(project.isPresent()); + } + + + @Test + public void testFindByIn() { + List projects = repository.findByCreatorIn(Collections.singleton(FAKE_CREATOR)); + + Assert.assertTrue(projects.isEmpty()); + + projects = repository.findByCreatorIn(Arrays.asList(CREATOR_1, CREATOR_2)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_1, PROJECT_2)); + + projects = repository.findByCreatorIn(Arrays.asList(CREATOR_0, FAKE_CREATOR)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_4)); + } + + @Test + public void testFindByInWithAnd() { + List projects = repository.findByCreatorInAndStarCountIn(Arrays.asList(CREATOR_0, CREATOR_1), + Arrays.asList(STAR_COUNT_2, STAR_COUNT_3)); + + Assert.assertTrue(projects.isEmpty()); + + projects = repository.findByCreatorInAndStarCountIn(Arrays.asList(CREATOR_0, CREATOR_1), + Arrays.asList(STAR_COUNT_0, STAR_COUNT_2)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_4)); + + projects = repository.findByCreatorInAndStarCountIn(Arrays.asList(CREATOR_0, CREATOR_1, CREATOR_2), + Arrays.asList(STAR_COUNT_0, STAR_COUNT_1, STAR_COUNT_2)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_1, PROJECT_2, PROJECT_4)); + } + + @Test + public void testFindByNotIn() { + List projects = repository.findByCreatorNotIn( + Arrays.asList(CREATOR_0, CREATOR_1, CREATOR_2, CREATOR_3)); + + Assert.assertTrue(projects.isEmpty()); + + projects = repository.findByCreatorNotIn(Arrays.asList(CREATOR_1, CREATOR_2)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_3, PROJECT_4)); + + projects = repository.findByCreatorNotIn(Arrays.asList(CREATOR_0, FAKE_CREATOR)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_1, PROJECT_2, PROJECT_3)); + } + + @Test + public void testFindByInWithNotIn() { + List projects = repository.findByCreatorInAndStarCountNotIn(Collections.singletonList(FAKE_CREATOR), + Arrays.asList(STAR_COUNT_2, STAR_COUNT_3)); + + Assert.assertTrue(projects.isEmpty()); + + projects = repository.findByCreatorInAndStarCountNotIn(Arrays.asList(CREATOR_0, CREATOR_1), + Arrays.asList(STAR_COUNT_0, STAR_COUNT_2)); + + assertProjectListEquals(projects, Collections.singletonList(PROJECT_1)); + + projects = repository.findByCreatorInAndStarCountNotIn(Arrays.asList(CREATOR_0, CREATOR_1, CREATOR_2), + Arrays.asList(STAR_COUNT_1, STAR_COUNT_2)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_4)); + } + + @Test + public void testFindByNameIsNull() { + List projects = repository.findByNameIsNull(); + + Assert.assertTrue(projects.isEmpty()); + + final Project nullNameProject = new Project("id-999", null, CREATOR_0, true, STAR_COUNT_0, + FORK_COUNT_0); + + this.repository.save(nullNameProject); + projects = repository.findByNameIsNull(); + + assertProjectListEquals(projects, Collections.singletonList(nullNameProject)); + } + + @Test + public void testFindByNameIsNotNull() { + List projects = repository.findByNameIsNotNull(); + + assertProjectListEquals(projects, PROJECTS); + + this.repository.deleteAll(); + this.repository.save(new Project("id-999", null, CREATOR_0, true, STAR_COUNT_0, FORK_COUNT_0)); + + projects = repository.findByNameIsNotNull(); + + Assert.assertTrue(projects.isEmpty()); + } + + @Test + public void testFindByNameIsNullWithAnd() { + List projects = repository.findByNameIsNullAndForkCount(FORK_COUNT_MAX); + + Assert.assertTrue(projects.isEmpty()); + + final Project nullNameProject = new Project("id-999", null, CREATOR_0, true, STAR_COUNT_0, + FORK_COUNT_0); + + this.repository.save(nullNameProject); + projects = repository.findByNameIsNullAndForkCount(FORK_COUNT_0); + + assertProjectListEquals(projects, Collections.singletonList(nullNameProject)); + } + + @Test + public void testFindByNameIsNotNullWithAnd() { + List projects = repository.findByNameIsNotNullAndHasReleased(true); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_1, PROJECT_2, PROJECT_3)); + + this.repository.deleteAll(); + this.repository.save(new Project("id-999", null, CREATOR_0, true, STAR_COUNT_0, FORK_COUNT_0)); + + projects = repository.findByNameIsNotNullAndHasReleased(true); + Assert.assertTrue(projects.isEmpty()); + } + + @Test + public void testFindAllByPartitionKey() { + List findAll = + repository.findAll(new PartitionKey(CREATOR_0)); + // Since there are two projects with creator_0 + assertThat(findAll.size()).isEqualTo(2); + assertThat(findAll.containsAll(Lists.newArrayList(PROJECT_0, PROJECT_4))).isTrue(); + + findAll = repository.findAll(new PartitionKey(CREATOR_1)); + // Since there is one projects with creator_1 + assertThat(findAll.size()).isEqualTo(1); + assertThat(findAll.contains(PROJECT_1)).isTrue(); + + + findAll = repository.findAll(new PartitionKey(CREATOR_2)); + // Since there is one projects with creator_2 + assertThat(findAll.size()).isEqualTo(1); + assertThat(findAll.contains(PROJECT_2)).isTrue(); + + findAll = repository.findAll(new PartitionKey(CREATOR_3)); + // Since there is one projects with creator_3 + assertThat(findAll.size()).isEqualTo(1); + assertThat(findAll.contains(PROJECT_3)).isTrue(); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/ProjectRepositorySortIT.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/ProjectRepositorySortIT.java new file mode 100644 index 000000000000..dd952e74dc53 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/ProjectRepositorySortIT.java @@ -0,0 +1,251 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.integration; + +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosTemplate; +import com.microsoft.azure.spring.data.cosmosdb.core.query.CosmosPageRequest; +import com.microsoft.azure.spring.data.cosmosdb.domain.SortedProject; +import com.microsoft.azure.spring.data.cosmosdb.exception.CosmosDBAccessException; +import com.microsoft.azure.spring.data.cosmosdb.repository.TestRepositoryConfig; +import com.microsoft.azure.spring.data.cosmosdb.repository.repository.SortedProjectRepository; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.assertj.core.util.Lists; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import static com.microsoft.azure.spring.data.cosmosdb.common.PageTestUtils.validateLastPage; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class ProjectRepositorySortIT { + + private static final String ID_0 = "id-0"; + private static final String ID_1 = "id-1"; + private static final String ID_2 = "id-2"; + private static final String ID_3 = "id-3"; + private static final String ID_4 = "id-4"; + + private static final String NAME_0 = "name-0"; + private static final String NAME_1 = "name-1"; + private static final String NAME_2 = "name-2"; + private static final String NAME_3 = "NAME-3"; + private static final String NAME_4 = "name-4"; + + private static final String CREATOR_0 = "creator-0"; + private static final String CREATOR_1 = "creator-1"; + private static final String CREATOR_2 = "creator-2"; + private static final String CREATOR_3 = "creator-3"; + private static final String CREATOR_4 = "creator-4"; + + private static final Long STAR_COUNT_0 = 0L; + private static final Long STAR_COUNT_1 = 1L; + private static final Long STAR_COUNT_2 = 2L; + private static final Long STAR_COUNT_3 = 3L; + private static final Long STAR_COUNT_4 = 4L; + + private static final Long FORK_COUNT_0 = 0L; + private static final Long FORK_COUNT_1 = 1L; + private static final Long FORK_COUNT_2 = 2L; + private static final Long FORK_COUNT_3 = 3L; + private static final Long FORK_COUNT_4 = FORK_COUNT_3; + + private static final SortedProject PROJECT_0 = new SortedProject(ID_0, NAME_0, CREATOR_0, + true, STAR_COUNT_0, FORK_COUNT_0); + private static final SortedProject PROJECT_1 = new SortedProject(ID_1, NAME_1, CREATOR_1, + true, STAR_COUNT_1, FORK_COUNT_1); + private static final SortedProject PROJECT_2 = new SortedProject(ID_2, NAME_2, CREATOR_2, + true, STAR_COUNT_2, FORK_COUNT_2); + private static final SortedProject PROJECT_3 = new SortedProject(ID_3, NAME_3, CREATOR_3, + true, STAR_COUNT_3, FORK_COUNT_3); + private static final SortedProject PROJECT_4 = new SortedProject(ID_4, NAME_4, CREATOR_4, + true, STAR_COUNT_4, FORK_COUNT_4); + + private static final List PROJECTS = Arrays.asList(PROJECT_4, PROJECT_3, + PROJECT_2, PROJECT_1, PROJECT_0); + + private static final CosmosEntityInformation entityInformation = + new CosmosEntityInformation<>(SortedProject.class); + + private static CosmosTemplate staticTemplate; + private static boolean isSetupDone; + + @Autowired + private CosmosTemplate template; + + @Autowired + private SortedProjectRepository repository; + + @Before + public void setUp() { + if (!isSetupDone) { + staticTemplate = template; + template.createContainerIfNotExists(entityInformation); + } + this.repository.saveAll(PROJECTS); + isSetupDone = true; + } + + @After + public void cleanup() { + this.repository.deleteAll(); + } + + @AfterClass + public static void afterClassCleanup() { + staticTemplate.deleteContainer(entityInformation.getContainerName()); + } + + @Test + public void testFindAllSortASC() { + final Sort sort = Sort.by(Sort.Direction.ASC, "starCount"); + final List projects = Lists.newArrayList(this.repository.findAll(sort)); + + PROJECTS.sort(Comparator.comparing(SortedProject::getStarCount)); + + Assert.assertEquals(PROJECTS.size(), projects.size()); + Assert.assertEquals(PROJECTS, projects); + } + + @Test + public void testFindAllSortDESC() { + final Sort sort = Sort.by(Sort.Direction.DESC, "creator"); + final List projects = Lists.newArrayList(this.repository.findAll(sort)); + + PROJECTS.sort(Comparator.comparing(SortedProject::getCreator).reversed()); + + Assert.assertEquals(PROJECTS.size(), projects.size()); + Assert.assertEquals(PROJECTS, projects); + } + + @Test + public void testFindAllUnSorted() { + final Sort sort = Sort.unsorted(); + final List projects = Lists.newArrayList(this.repository.findAll(sort)); + + PROJECTS.sort(Comparator.comparing(SortedProject::getId)); + projects.sort(Comparator.comparing(SortedProject::getId)); + + Assert.assertEquals(PROJECTS.size(), projects.size()); + Assert.assertEquals(PROJECTS, projects); + } + + @Test(expected = CosmosDBAccessException.class) + public void testFindAllSortMoreThanOneOrderException() { + final Sort sort = Sort.by(Sort.Direction.ASC, "name", "creator"); + + this.repository.findAll(sort); + } + + @Test(expected = IllegalArgumentException.class) + public void testFindAllSortIgnoreCaseException() { + final Sort.Order order = Sort.Order.by("name").ignoreCase(); + final Sort sort = Sort.by(order); + + this.repository.findAll(sort); + } + + @Test(expected = CosmosDBAccessException.class) + public void testFindAllSortMissMatchException() { + final Sort sort = Sort.by(Sort.Direction.ASC, "fake-name"); + + this.repository.findAll(sort); + } + + public void testFindAllSortWithIdName() { + final List projectListSortedById = Lists.newArrayList(PROJECTS); + projectListSortedById.sort(Comparator.comparing(SortedProject::getId)); + + final Sort sort = Sort.by(Sort.Direction.ASC, "id"); + final List results = StreamSupport.stream(this.repository.findAll(sort).spliterator(), + false) + .collect(Collectors.toList()); + + Assert.assertEquals(projectListSortedById, results); + } + + @Test + public void testFindSortWithOr() { + final Sort sort = Sort.by(Sort.Direction.ASC, "starCount"); + final List projects = Lists.newArrayList(this.repository.findByNameOrCreator(NAME_0, CREATOR_3, + sort)); + final List references = Arrays.asList(PROJECT_0, PROJECT_3); + + references.sort(Comparator.comparing(SortedProject::getStarCount)); + + Assert.assertEquals(references.size(), projects.size()); + Assert.assertEquals(references, projects); + } + + @Test + public void testFindSortWithAnd() { + final Sort sort = Sort.by(Sort.Direction.ASC, "forkCount"); + final List projects = Lists.newArrayList(repository.findByNameAndCreator(NAME_0, CREATOR_0, + sort)); + final List references = Arrays.asList(PROJECT_0); + + references.sort(Comparator.comparing(SortedProject::getStarCount)); + + Assert.assertEquals(references.size(), projects.size()); + Assert.assertEquals(references, projects); + } + + @Test + public void testFindSortWithEqual() { + final Sort sort = Sort.by(Sort.Direction.DESC, "name"); + final List projects = Lists.newArrayList(this.repository.findByForkCount(FORK_COUNT_3, sort)); + final List references = Arrays.asList(PROJECT_3, PROJECT_4); + + references.sort(Comparator.comparing(SortedProject::getName).reversed()); + + Assert.assertEquals(references.size(), projects.size()); + Assert.assertEquals(references, projects); + } + + @Test + public void testFindAllWithPageableAndSort() { + final Sort sort = Sort.by(Sort.Direction.DESC, "name"); + final Pageable pageable = new CosmosPageRequest(0, 5, null, sort); + + final Page result = this.repository.findAll(pageable); + + final List references = Arrays.asList(PROJECT_0, PROJECT_1, PROJECT_2, PROJECT_3, PROJECT_4); + references.sort(Comparator.comparing(SortedProject::getName).reversed()); + + Assert.assertEquals(references.size(), result.getContent().size()); + Assert.assertEquals(references, result.getContent()); + validateLastPage(result, 5); + } + + @Test + public void testFindWithPageableAndSort() { + final Sort sort = Sort.by(Sort.Direction.DESC, "name"); + final Pageable pageable = new CosmosPageRequest(0, 5, null, sort); + + final Page result = this.repository.findByForkCount(FORK_COUNT_3, pageable); + + final List references = Arrays.asList(PROJECT_3, PROJECT_4); + + references.sort(Comparator.comparing(SortedProject::getName).reversed()); + + Assert.assertEquals(references.size(), result.getContent().size()); + Assert.assertEquals(references, result.getContent()); + validateLastPage(result, result.getContent().size()); + } +} + diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/QuestionRepositoryIT.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/QuestionRepositoryIT.java new file mode 100644 index 000000000000..deba6ec4e626 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/QuestionRepositoryIT.java @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.integration; + +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosTemplate; +import com.microsoft.azure.spring.data.cosmosdb.domain.Question; +import com.microsoft.azure.spring.data.cosmosdb.repository.TestRepositoryConfig; +import com.microsoft.azure.spring.data.cosmosdb.repository.repository.ProjectRepository; +import com.microsoft.azure.spring.data.cosmosdb.repository.repository.QuestionRepository; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.assertj.core.util.Lists; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class QuestionRepositoryIT { + + private static final String QUESTION_ID = "question-id"; + + private static final String QUESTION_URL = "http://xxx.html"; + + private static final Question QUESTION = new Question(QUESTION_ID, QUESTION_URL); + + private static final CosmosEntityInformation entityInformation = + new CosmosEntityInformation<>(Question.class); + + private static CosmosTemplate staticTemplate; + private static boolean isSetupDone; + + @Autowired + private CosmosTemplate template; + + @Autowired + private QuestionRepository repository; + + @Autowired + private ProjectRepository projectRepository; + + @Before + public void setUp() { + if (!isSetupDone) { + staticTemplate = template; + template.createContainerIfNotExists(entityInformation); + } + this.repository.save(QUESTION); + isSetupDone = true; + } + + @After + public void cleanup() { + this.repository.deleteAll(); + } + + @AfterClass + public static void afterClassCleanup() { + staticTemplate.deleteContainer(entityInformation.getContainerName()); + } + + @Test + public void testFindById() { + final Optional optional = this.repository.findById(QUESTION_ID); + + Assert.assertTrue(optional.isPresent()); + Assert.assertEquals(QUESTION, optional.get()); + } + + @Test + public void testFindByIdNull() { + final Optional byId = this.repository.findById(QUESTION_URL); + Assert.assertFalse(byId.isPresent()); + } + + @Test + public void testFindAll() { + final List questions = Lists.newArrayList(this.repository.findAll()); + + Assert.assertEquals(Collections.singletonList(QUESTION), questions); + } + + @Test + public void testDelete() { + Optional optional = this.repository.findById(QUESTION_ID); + + Assert.assertTrue(optional.isPresent()); + Assert.assertEquals(QUESTION, optional.get()); + + this.repository.delete(QUESTION); + optional = this.repository.findById(QUESTION_ID); + + Assert.assertFalse(optional.isPresent()); + } +} + diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/ReactiveCourseRepositoryIT.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/ReactiveCourseRepositoryIT.java new file mode 100644 index 000000000000..140cff164df8 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/ReactiveCourseRepositoryIT.java @@ -0,0 +1,260 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.integration; + +import com.azure.data.cosmos.PartitionKey; +import com.microsoft.azure.spring.data.cosmosdb.core.ReactiveCosmosTemplate; +import com.microsoft.azure.spring.data.cosmosdb.domain.Course; +import com.microsoft.azure.spring.data.cosmosdb.exception.CosmosDBAccessException; +import com.microsoft.azure.spring.data.cosmosdb.repository.TestRepositoryConfig; +import com.microsoft.azure.spring.data.cosmosdb.repository.repository.ReactiveCourseRepository; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Sort; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicBoolean; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class ReactiveCourseRepositoryIT { + + private static final String COURSE_ID_1 = "1"; + private static final String COURSE_ID_2 = "2"; + private static final String COURSE_ID_3 = "3"; + private static final String COURSE_ID_4 = "4"; + private static final String COURSE_ID_5 = "5"; + + private static final String COURSE_NAME_1 = "Course1"; + private static final String COURSE_NAME_2 = "Course2"; + private static final String COURSE_NAME_3 = "Course3"; + private static final String COURSE_NAME_4 = "Course4"; + private static final String COURSE_NAME_5 = "Course5"; + + private static final String DEPARTMENT_NAME_1 = "Department1"; + private static final String DEPARTMENT_NAME_2 = "Department2"; + private static final String DEPARTMENT_NAME_3 = "Department3"; + + private static final Course COURSE_1 = new Course(COURSE_ID_1, COURSE_NAME_1, DEPARTMENT_NAME_3); + private static final Course COURSE_2 = new Course(COURSE_ID_2, COURSE_NAME_2, DEPARTMENT_NAME_2); + private static final Course COURSE_3 = new Course(COURSE_ID_3, COURSE_NAME_3, DEPARTMENT_NAME_2); + private static final Course COURSE_4 = new Course(COURSE_ID_4, COURSE_NAME_4, DEPARTMENT_NAME_1); + private static final Course COURSE_5 = new Course(COURSE_ID_5, COURSE_NAME_5, DEPARTMENT_NAME_1); + + private static final CosmosEntityInformation entityInformation = + new CosmosEntityInformation<>(Course.class); + + private static ReactiveCosmosTemplate staticTemplate; + private static boolean isSetupDone; + + @Autowired + private ReactiveCosmosTemplate template; + + @Autowired + private ReactiveCourseRepository repository; + + @Before + public void setUp() { + if (!isSetupDone) { + staticTemplate = template; + template.createContainerIfNotExists(entityInformation); + } + final Flux savedFlux = repository.saveAll(Arrays.asList(COURSE_1, COURSE_2, + COURSE_3, COURSE_4)); + StepVerifier.create(savedFlux).thenConsumeWhile(course -> true).expectComplete().verify(); + isSetupDone = true; + } + + @After + public void cleanup() { + final Mono deletedMono = repository.deleteAll(); + StepVerifier.create(deletedMono).thenAwait().verifyComplete(); + } + + @AfterClass + public static void afterClassCleanup() { + staticTemplate.deleteContainer(entityInformation.getContainerName()); + } + + @Test + public void testFindById() { + final Mono idMono = repository.findById(COURSE_ID_4); + StepVerifier.create(idMono).expectNext(COURSE_4).expectComplete().verify(); + } + + @Test + public void testFindByIdAndPartitionKey() { + final Mono idMono = repository.findById(COURSE_ID_4, + new PartitionKey(entityInformation.getPartitionKeyFieldValue(COURSE_4))); + StepVerifier.create(idMono).expectNext(COURSE_4).expectComplete().verify(); + } + + @Test + public void testFindByIdAsPublisher() { + final Mono byId = repository.findById(Mono.just(COURSE_ID_1)); + StepVerifier.create(byId).expectNext(COURSE_1).verifyComplete(); + } + + @Test + public void testFindAllWithSort() { + final Flux sortAll = repository.findAll(Sort.by(Sort.Order.desc("name"))); + StepVerifier.create(sortAll).expectNext(COURSE_4, COURSE_3, COURSE_2, COURSE_1).verifyComplete(); + } + + @Test + public void testFindByIdNotFound() { + final Mono idMono = repository.findById("10"); + // Expect an empty mono as return value + StepVerifier.create(idMono).expectComplete().verify(); + } + + @Test + public void testFindByIdAndPartitionKeyNotFound() { + final Mono idMono = repository.findById("10", + new PartitionKey(entityInformation.getPartitionKeyFieldValue(COURSE_1))); + // Expect an empty mono as return value + StepVerifier.create(idMono).expectComplete().verify(); + } + + @Test + public void testFindAll() { + final Flux allFlux = repository.findAll(); + StepVerifier.create(allFlux).expectNextCount(4).verifyComplete(); + } + + @Test + public void testInsert() { + final Mono save = repository.save(COURSE_5); + StepVerifier.create(save).expectNext(COURSE_5).verifyComplete(); + } + + @Test + public void testUpsert() { + Mono save = repository.save(COURSE_1); + StepVerifier.create(save).expectNext(COURSE_1).expectComplete().verify(); + + save = repository.save(COURSE_1); + StepVerifier.create(save).expectNext(COURSE_1).expectComplete().verify(); + } + + @Test + public void testDeleteByIdWithoutPartitionKey() { + final Mono deleteMono = repository.deleteById(COURSE_1.getCourseId()); + StepVerifier.create(deleteMono).expectError(CosmosDBAccessException.class).verify(); + } + + @Test + public void testDeleteByIdAndPartitionKey() { + final Mono deleteMono = repository.deleteById(COURSE_1.getCourseId(), + new PartitionKey(entityInformation.getPartitionKeyFieldValue(COURSE_1))); + StepVerifier.create(deleteMono).verifyComplete(); + + final Mono byId = repository.findById(COURSE_ID_1, + new PartitionKey(entityInformation.getPartitionKeyFieldValue(COURSE_1))); + // Expect an empty mono as return value + StepVerifier.create(byId).verifyComplete(); + } + + @Test + public void testDeleteByEntity() { + final Mono deleteMono = repository.delete(COURSE_4); + StepVerifier.create(deleteMono).verifyComplete(); + + final Mono byId = repository.findById(COURSE_ID_4); + // Expect an empty mono as return value + StepVerifier.create(byId).expectComplete().verify(); + } + + @Test + public void testDeleteByIdNotFound() { + final Mono deleteMono = repository.deleteById(COURSE_ID_5); + StepVerifier.create(deleteMono).expectError(CosmosDBAccessException.class).verify(); + } + + @Test + public void testDeleteByEntityNotFound() { + final Mono deleteMono = repository.delete(COURSE_5); + StepVerifier.create(deleteMono).expectError(CosmosDBAccessException.class).verify(); + } + + @Test + public void testCountAll() { + final Mono countMono = repository.count(); + StepVerifier.create(countMono).expectNext(4L).verifyComplete(); + } + + @Test + public void testFindByDepartmentIn() { + final Flux byDepartmentIn = + repository.findByDepartmentIn(Collections.singletonList(DEPARTMENT_NAME_2)); + StepVerifier.create(byDepartmentIn).expectNextCount(2).verifyComplete(); + } + + @Test + public void testFindAllByPartitionKey() { + final Mono save = repository.save(COURSE_5); + StepVerifier.create(save).expectNext(COURSE_5).verifyComplete(); + + Flux findAll = repository.findAll(new PartitionKey(DEPARTMENT_NAME_1)); + // Since there are two courses with department_1 + final AtomicBoolean courseFound = new AtomicBoolean(false); + StepVerifier.create(findAll).expectNextCount(2).verifyComplete(); + StepVerifier.create(findAll) + .expectNextMatches(course -> { + if (course.equals(COURSE_4)) { + courseFound.set(true); + } else if (course.equals(COURSE_5)) { + courseFound.set(false); + } else { + return false; + } + return true; + }) + .expectNextMatches(course -> { + if (courseFound.get()) { + return course.equals(COURSE_5); + } else { + return course.equals(COURSE_4); + } + }) + .verifyComplete(); + + findAll = repository.findAll(new PartitionKey(DEPARTMENT_NAME_3)); + // Since there are two courses with department_3 + StepVerifier.create(findAll).expectNext(COURSE_1).verifyComplete(); + + findAll = repository.findAll(new PartitionKey(DEPARTMENT_NAME_2)); + // Since there are two courses with department_2 + StepVerifier.create(findAll).expectNextCount(2).verifyComplete(); + StepVerifier.create(findAll) + .expectNextMatches(course -> { + if (course.equals(COURSE_2)) { + courseFound.set(true); + } else if (course.equals(COURSE_3)) { + courseFound.set(false); + } else { + return false; + } + return true; + }) + .expectNextMatches(course -> { + if (courseFound.get()) { + return course.equals(COURSE_3); + } else { + return course.equals(COURSE_2); + } + }) + .verifyComplete(); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/SpELCosmosDBAnnotationIT.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/SpELCosmosDBAnnotationIT.java new file mode 100644 index 000000000000..ab8965c8b67d --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/SpELCosmosDBAnnotationIT.java @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.integration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.azure.spring.data.cosmosdb.CosmosDbFactory; +import com.microsoft.azure.spring.data.cosmosdb.common.TestConstants; +import com.microsoft.azure.spring.data.cosmosdb.config.CosmosDBConfig; +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosTemplate; +import com.microsoft.azure.spring.data.cosmosdb.core.convert.MappingCosmosConverter; +import com.microsoft.azure.spring.data.cosmosdb.core.convert.ObjectMapperFactory; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.CosmosMappingContext; +import com.microsoft.azure.spring.data.cosmosdb.domain.SpELBeanStudent; +import com.microsoft.azure.spring.data.cosmosdb.domain.SpELPropertyStudent; +import com.microsoft.azure.spring.data.cosmosdb.repository.TestRepositoryConfig; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.domain.EntityScanner; +import org.springframework.context.ApplicationContext; +import org.springframework.data.annotation.Persistent; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class SpELCosmosDBAnnotationIT { + private static final SpELPropertyStudent TEST_PROPERTY_STUDENT = + new SpELPropertyStudent(TestConstants.ID_1, TestConstants.FIRST_NAME, + TestConstants.LAST_NAME); + + @Value("${cosmosdb.uri}") + private String dbUri; + + @Value("${cosmosdb.key}") + private String dbKey; + + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private CosmosTemplate cosmosTemplate; + + private static CosmosTemplate staticTemplate; + private static CosmosEntityInformation cosmosEntityInformation; + + @Before + public void setUp() { + if (staticTemplate == null) { + staticTemplate = cosmosTemplate; + } + } + + @AfterClass + public static void afterClassCleanup() { + if (cosmosEntityInformation != null) { + staticTemplate.deleteContainer(cosmosEntityInformation.getContainerName()); + } + } + + @Test + public void testDynamicContainerNameWithPropertySourceExpression() { + final CosmosEntityInformation propertyStudentInfo = + new CosmosEntityInformation<>(SpELPropertyStudent.class); + + assertEquals(TestConstants.DYNAMIC_PROPERTY_COLLECTION_NAME, propertyStudentInfo.getContainerName()); + } + + @Test + public void testDynamicContainerNameWithBeanExpression() { + final CosmosEntityInformation beanStudentInfo = + new CosmosEntityInformation<>(SpELBeanStudent.class); + + assertEquals(TestConstants.DYNAMIC_BEAN_COLLECTION_NAME, beanStudentInfo.getContainerName()); + } + + @Test + public void testDatabaseOperationsOnDynamicallyNamedCollection() throws ClassNotFoundException { + final CosmosDBConfig dbConfig = CosmosDBConfig.builder(dbUri, dbKey, TestConstants.DB_NAME).build(); + final CosmosDbFactory dbFactory = new CosmosDbFactory(dbConfig); + + cosmosEntityInformation = new CosmosEntityInformation<>(SpELPropertyStudent.class); + final CosmosMappingContext dbContext = new CosmosMappingContext(); + dbContext.setInitialEntitySet(new EntityScanner(this.applicationContext).scan(Persistent.class)); + + final ObjectMapper objectMapper = ObjectMapperFactory.getObjectMapper(); + final MappingCosmosConverter mappingConverter = new MappingCosmosConverter(dbContext, objectMapper); + staticTemplate = new CosmosTemplate(dbFactory, mappingConverter, TestConstants.DB_NAME); + + staticTemplate.createContainerIfNotExists(cosmosEntityInformation); + + final SpELPropertyStudent insertedRecord = + staticTemplate.insert(cosmosEntityInformation.getContainerName(), TEST_PROPERTY_STUDENT, null); + assertNotNull(insertedRecord); + + final SpELPropertyStudent readRecord = + staticTemplate.findById(TestConstants.DYNAMIC_PROPERTY_COLLECTION_NAME, + insertedRecord.getId(), SpELPropertyStudent.class); + assertNotNull(readRecord); + } + +} + diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/SquareRepositoryIT.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/SquareRepositoryIT.java new file mode 100644 index 000000000000..b186197af719 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/SquareRepositoryIT.java @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.integration; + +import com.microsoft.azure.spring.data.cosmosdb.common.TestUtils; +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosTemplate; +import com.microsoft.azure.spring.data.cosmosdb.domain.inheritance.Square; +import com.microsoft.azure.spring.data.cosmosdb.repository.TestRepositoryConfig; +import com.microsoft.azure.spring.data.cosmosdb.repository.repository.SquareRepository; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class SquareRepositoryIT { + private Square square1 = new Square("id_1", 1, 1); + private Square square2 = new Square("id_2", 2, 4); + + private static final CosmosEntityInformation entityInformation = + new CosmosEntityInformation<>(Square.class); + + private static CosmosTemplate staticTemplate; + private static boolean isSetupDone; + + @Autowired + private CosmosTemplate template; + + @Autowired + private SquareRepository repository; + + @Before + public void setUp() { + if (!isSetupDone) { + staticTemplate = template; + template.createContainerIfNotExists(entityInformation); + } + repository.save(square1); + repository.save(square2); + isSetupDone = true; + } + + @After + public void cleanup() { + repository.deleteAll(); + } + + @AfterClass + public static void afterClassCleanup() { + staticTemplate.deleteContainer(entityInformation.getContainerName()); + } + + @Test + public void testFindAll() { + final List result = TestUtils.toList(repository.findAll()); + + assertThat(result.size()).isEqualTo(2); + } + + @Test + public void testFindIncludeInheritedFields() { + final Optional result = repository.findById(square1.getId()); + + assertThat(result.get()).isNotNull(); + assertThat(result.get().getId().equals(square1.getId())); + assertThat(result.get().getLength()).isEqualTo(square1.getLength()); + assertThat(result.get().getArea()).isEqualTo(square1.getArea()); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/StudentRepositoryIT.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/StudentRepositoryIT.java new file mode 100644 index 000000000000..9b0058e7f905 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/integration/StudentRepositoryIT.java @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.integration; + +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosTemplate; +import com.microsoft.azure.spring.data.cosmosdb.domain.Student; +import com.microsoft.azure.spring.data.cosmosdb.repository.TestRepositoryConfig; +import com.microsoft.azure.spring.data.cosmosdb.repository.repository.StudentRepository; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosEntityInformation; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class StudentRepositoryIT { + public static final String ID_0 = "id-0"; + public static final String ID_1 = "id-1"; + public static final String ID_2 = "id-2"; + public static final String ID_3 = "id-3"; + + public static final String FIRST_NAME_0 = "Mary"; + public static final String FIRST_NAME_1 = "Cheng"; + public static final String FIRST_NAME_2 = "Zheng"; + public static final String FIRST_NAME_3 = "Zhen"; + + public static final String LAST_NAME_0 = "Chen"; + public static final String LAST_NAME_1 = "Ch"; + public static final String LAST_NAME_2 = "N"; + public static final String LAST_NAME_3 = "H"; + + public static final String SUB_FIRST_NAME = "eng"; + + private static final Student STUDENT_0 = new Student(ID_0, FIRST_NAME_0, LAST_NAME_0); + private static final Student STUDENT_1 = new Student(ID_1, FIRST_NAME_1, LAST_NAME_1); + private static final Student STUDENT_2 = new Student(ID_2, FIRST_NAME_2, LAST_NAME_2); + private static final Student STUDENT_3 = new Student(ID_3, FIRST_NAME_3, LAST_NAME_3); + private static final List PEOPLE = Arrays.asList(STUDENT_0, STUDENT_1, STUDENT_2, STUDENT_3); + + private static final CosmosEntityInformation entityInformation = + new CosmosEntityInformation<>(Student.class); + + private static CosmosTemplate staticTemplate; + private static boolean isSetupDone; + + @Autowired + private CosmosTemplate template; + + @Autowired + private StudentRepository repository; + + @Before + public void setUp() { + if (!isSetupDone) { + staticTemplate = template; + template.createContainerIfNotExists(entityInformation); + } + this.repository.saveAll(PEOPLE); + isSetupDone = true; + } + + @After + public void cleanup() { + this.repository.deleteAll(); + } + + @AfterClass + public static void afterClassCleanup() { + staticTemplate.deleteContainer(entityInformation.getContainerName()); + } + + @Test + public void testFindByContaining() { + final List people = repository.findByFirstNameContaining(SUB_FIRST_NAME); + final List reference = Arrays.asList(STUDENT_1, STUDENT_2); + + assertPeopleEquals(people, reference); + } + + @Test + public void testFindByContainingWithAnd() { + final List people = repository.findByFirstNameContainingAndLastNameContaining("eng", "h"); + final List reference = Arrays.asList(STUDENT_1); + + assertPeopleEquals(people, reference); + } + + @Test + public void testFindByEndsWith() { + final List people = repository.findByFirstNameEndsWith("en"); + final List reference = Arrays.asList(STUDENT_3); + + assertPeopleEquals(people, reference); + } + + @Test + public void testFindByNot() { + final List people = repository.findByFirstNameNot("Mary"); + final List reference = Arrays.asList(STUDENT_1, STUDENT_2, STUDENT_3); + + assertPeopleEquals(people, reference); + } + + @Test + public void testFindByStartsWith() { + List people = repository.findByFirstNameStartsWith("Z"); + + assertPeopleEquals(people, Arrays.asList(STUDENT_2, STUDENT_3)); + + people = repository.findByLastNameStartsWith("C"); + + assertPeopleEquals(people, Arrays.asList(STUDENT_0, STUDENT_1)); + } + + @Test + public void testFindByStartsWithAndEndsWith() { + List people = repository.findByFirstNameStartsWithAndLastNameEndingWith("Z", "H"); + + assertPeopleEquals(people, Arrays.asList(STUDENT_3)); + + people = repository.findByFirstNameStartsWithAndLastNameEndingWith("Z", "en"); + + assertPeopleEquals(people, Arrays.asList()); + } + + @Test + public void testFindByStartsWithOrContaining() { + List people = repository.findByFirstNameStartsWithOrLastNameContaining("Zhen", "C"); + + assertPeopleEquals(people, PEOPLE); + + people = repository.findByFirstNameStartsWithOrLastNameContaining("M", "N"); + + assertPeopleEquals(people, Arrays.asList(STUDENT_0, STUDENT_2)); + } + + @Test + public void testFindByContainingAndNot() { + final List people = repository.findByFirstNameContainingAndLastNameNot("Zhe", "N"); + + assertPeopleEquals(people, Arrays.asList(STUDENT_3)); + } + + private void assertPeopleEquals(List people, List reference) { + people.sort(Comparator.comparing(Student::getId)); + reference.sort(Comparator.comparing(Student::getId)); + + Assert.assertEquals(reference, people); + } + + @Test + public void testExists() { + assertTrue(repository.existsByFirstName(FIRST_NAME_0)); + assertFalse(repository.existsByFirstName("xxx")); + + assertTrue(repository.existsByLastNameContaining("N")); + assertFalse(repository.existsByLastNameContaining("X")); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/AddressRepository.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/AddressRepository.java new file mode 100644 index 000000000000..1f7a6055df0e --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/AddressRepository.java @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.repository; + +import com.microsoft.azure.spring.data.cosmosdb.domain.Address; +import com.microsoft.azure.spring.data.cosmosdb.repository.CosmosRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface AddressRepository extends CosmosRepository { + void deleteByPostalCodeAndCity(String postalCode, String city); + + void deleteByCity(String city); + + List
findByPostalCodeAndCity(String postalCode, String city); + + List
findByCity(String city); + + List
findByPostalCode(String postalCode); + + List
findByStreetOrCity(String street, String city); + +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/ContactRepository.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/ContactRepository.java new file mode 100644 index 000000000000..3d52e998547b --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/ContactRepository.java @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.repository; + +import com.microsoft.azure.spring.data.cosmosdb.domain.Contact; +import com.microsoft.azure.spring.data.cosmosdb.repository.CosmosRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ContactRepository extends CosmosRepository { + List findByTitle(String title); +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/CustomerRepository.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/CustomerRepository.java new file mode 100644 index 000000000000..9c1a0e5d33e4 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/CustomerRepository.java @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.repository; + +import com.microsoft.azure.spring.data.cosmosdb.domain.Customer; +import com.microsoft.azure.spring.data.cosmosdb.repository.CosmosRepository; + +import java.util.List; + +public interface CustomerRepository extends CosmosRepository { + List findByUser_Name(String name); +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/IntegerIdDomainRepository.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/IntegerIdDomainRepository.java new file mode 100644 index 000000000000..5c7cb1b9a575 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/IntegerIdDomainRepository.java @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.repository; + +import com.microsoft.azure.spring.data.cosmosdb.domain.IntegerIdDomain; +import com.microsoft.azure.spring.data.cosmosdb.repository.CosmosRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface IntegerIdDomainRepository extends CosmosRepository { + +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/MemoRepository.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/MemoRepository.java new file mode 100644 index 000000000000..f6544d0462c4 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/MemoRepository.java @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.repository; + +import com.microsoft.azure.spring.data.cosmosdb.domain.Memo; +import com.microsoft.azure.spring.data.cosmosdb.domain.Importance; +import com.microsoft.azure.spring.data.cosmosdb.repository.CosmosRepository; +import org.springframework.stereotype.Repository; + +import java.util.Date; +import java.util.List; + +@Repository +public interface MemoRepository extends CosmosRepository { + List findMemoByDate(Date date); + + List findMemoByImportance(Importance importance); + + List findByDateBefore(Date date); + + List findByDateBeforeAndMessage(Date date, String message); + + List findByDateBeforeOrMessage(Date date, String message); + + List findByDateAfter(Date date); + + List findByDateAfterAndMessage(Date date, String message); + + List findByDateAfterOrMessage(Date date, String message); + + List findByDateBetween(Date startDate, Date endDate); + + List findByDateBetweenAndMessage(Date startDate, Date endDate, String message); + + List findByDateBetweenOrMessage(Date startDate, Date endDate, String message); + + List findByMessageStartsWith(String message); +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/PageableAddressRepository.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/PageableAddressRepository.java new file mode 100644 index 000000000000..76d794a6eda1 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/PageableAddressRepository.java @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.repository; + +import com.microsoft.azure.spring.data.cosmosdb.domain.PageableAddress; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PageableAddressRepository extends PagingAndSortingRepository { + Page findByStreet(String street, Pageable pageable); + + Page findByCity(String city, Pageable pageable); +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/PageableMemoRepository.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/PageableMemoRepository.java new file mode 100644 index 000000000000..7630c84672d2 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/PageableMemoRepository.java @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.repository; + +import com.microsoft.azure.spring.data.cosmosdb.domain.PageableMemo; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PageableMemoRepository extends PagingAndSortingRepository { +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/PageablePersonRepository.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/PageablePersonRepository.java new file mode 100644 index 000000000000..afc831158210 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/PageablePersonRepository.java @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.repository; + +import com.microsoft.azure.spring.data.cosmosdb.domain.PageablePerson; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PageablePersonRepository extends PagingAndSortingRepository { +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/PartitionPersonRepository.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/PartitionPersonRepository.java new file mode 100644 index 000000000000..f88f6eb54b79 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/PartitionPersonRepository.java @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.repository; + +import com.microsoft.azure.spring.data.cosmosdb.domain.PartitionPerson; +import com.microsoft.azure.spring.data.cosmosdb.repository.CosmosRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PartitionPersonRepository extends CosmosRepository { +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/PersonRepository.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/PersonRepository.java new file mode 100644 index 000000000000..95cbe3ecc681 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/PersonRepository.java @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.repository; + +import com.microsoft.azure.spring.data.cosmosdb.domain.Person; +import com.microsoft.azure.spring.data.cosmosdb.repository.CosmosRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PersonRepository extends CosmosRepository { +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/ProjectRepository.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/ProjectRepository.java new file mode 100644 index 000000000000..a8b0049754df --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/ProjectRepository.java @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.repository; + +import com.microsoft.azure.spring.data.cosmosdb.domain.Project; +import com.microsoft.azure.spring.data.cosmosdb.repository.CosmosRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +import java.util.Collection; +import java.util.List; + +public interface ProjectRepository extends CosmosRepository { + + List findByNameAndStarCount(String name, Long startCount); + + List findByNameOrForkCount(String name, Long forkCount); + + List findByNameAndCreator(String name, String creator); + + List findByNameOrCreator(String name, String creator); + + List findByNameAndCreatorOrForkCount(String name, String creator, Long forkCount); + + List findByNameOrCreatorAndForkCount(String name, String creator, Long forkCount); + + List findByNameOrCreatorOrForkCount(String name, String creator, Long forkCount); + + List findByNameOrCreatorAndForkCountOrStarCount(String name, String creator, + Long forkCount, Long starCount); + + List findByForkCountGreaterThan(Long forkCount); + + List findByCreatorAndForkCountGreaterThan(String creator, Long forkCount); + + List findByCreatorOrForkCountGreaterThan(String creator, Long forkCount); + + List findByNameOrCreator(String name, String creator, Sort sort); + + List findByNameAndCreator(String name, String creator, Sort sort); + + List findByForkCount(Long forkCount, Sort sort); + + List findByStarCountLessThan(Long starCount); + + List findByForkCountLessThanEqual(Long forkCount); + + List findByStarCountLessThanAndForkCountGreaterThan(Long max, Long min); + + List findByForkCountLessThanEqualAndStarCountGreaterThan(Long max, Long min); + + List findByStarCountGreaterThanEqual(Long count); + + List findByForkCountGreaterThanEqualAndCreator(Long count, String creator); + + List findByHasReleasedTrue(); + + List findByHasReleasedFalse(); + + List findByHasReleasedTrueAndCreator(String creator); + + List findByHasReleasedFalseAndCreator(String creator); + + List findByHasReleasedTrueOrCreator(String creator); + + List findByHasReleasedFalseOrCreator(String creator); + + List findByCreatorIn(Collection creators); + + List findByCreatorInAndStarCountIn(Collection creators, Collection starCounts); + + List findByCreatorNotIn(Collection creators); + + List findByCreatorInAndStarCountNotIn(Collection creators, Collection starCounts); + + List findByNameIsNull(); + + List findByNameIsNullAndForkCount(Long forkCount); + + List findByNameIsNotNull(); + + List findByNameIsNotNullAndHasReleased(boolean hasReleased); + + Page findByForkCount(Long forkCount, Pageable pageable); +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/QuestionRepository.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/QuestionRepository.java new file mode 100644 index 000000000000..0e27bd1381e0 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/QuestionRepository.java @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.repository; + +import com.microsoft.azure.spring.data.cosmosdb.domain.Question; +import com.microsoft.azure.spring.data.cosmosdb.repository.CosmosRepository; + +public interface QuestionRepository extends CosmosRepository { +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/ReactiveCourseRepository.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/ReactiveCourseRepository.java new file mode 100644 index 000000000000..6ad3a66ea4b4 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/ReactiveCourseRepository.java @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.repository; + +import com.microsoft.azure.spring.data.cosmosdb.domain.Course; +import com.microsoft.azure.spring.data.cosmosdb.repository.ReactiveCosmosRepository; +import reactor.core.publisher.Flux; + +import java.util.Collection; + +public interface ReactiveCourseRepository extends ReactiveCosmosRepository { + + Flux findByDepartmentIn(Collection departments); +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/SortedProjectRepository.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/SortedProjectRepository.java new file mode 100644 index 000000000000..b6606d8fe59b --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/SortedProjectRepository.java @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.repository; + +import com.microsoft.azure.spring.data.cosmosdb.domain.SortedProject; +import com.microsoft.azure.spring.data.cosmosdb.repository.CosmosRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +import java.util.List; + +public interface SortedProjectRepository extends CosmosRepository { + + List findByNameOrCreator(String name, String creator, Sort sort); + + List findByNameAndCreator(String name, String creator, Sort sort); + + List findByForkCount(Long forkCount, Sort sort); + + Page findByForkCount(Long forkCount, Pageable pageable); +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/SquareRepository.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/SquareRepository.java new file mode 100644 index 000000000000..295f881e24a6 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/SquareRepository.java @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.repository; + +import com.microsoft.azure.spring.data.cosmosdb.domain.inheritance.Square; +import com.microsoft.azure.spring.data.cosmosdb.repository.CosmosRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface SquareRepository extends CosmosRepository { + +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/StudentRepository.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/StudentRepository.java new file mode 100644 index 000000000000..4428770df373 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/repository/StudentRepository.java @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.repository; + +import com.microsoft.azure.spring.data.cosmosdb.domain.Student; +import com.microsoft.azure.spring.data.cosmosdb.repository.CosmosRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface StudentRepository extends CosmosRepository { + + List findByFirstNameContaining(String firstName); + + List findByFirstNameContainingAndLastNameContaining(String firstName, String lastName); + + List findByFirstNameEndsWith(String firstName); + + List findByFirstNameStartsWith(String firstName); + + List findByLastNameStartsWith(String lastName); + + List findByFirstNameStartsWithAndLastNameEndingWith(String firstName, String lastName); + + List findByFirstNameStartsWithOrLastNameContaining(String firstName, String lastName); + + List findByFirstNameNot(String firstName); + + List findByFirstNameContainingAndLastNameNot(String firstName, String lastName); + + Boolean existsByFirstName(String firstName); + + Boolean existsByLastNameContaining(String lastName); +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/CosmosEntityInformationUnitTest.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/CosmosEntityInformationUnitTest.java new file mode 100644 index 000000000000..598f0359d8c7 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/CosmosEntityInformationUnitTest.java @@ -0,0 +1,384 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.support; + +import java.util.List; +import java.util.Objects; + +import com.microsoft.azure.spring.data.cosmosdb.common.TestConstants; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.PartitionKey; +import com.microsoft.azure.spring.data.cosmosdb.domain.Address; +import com.microsoft.azure.spring.data.cosmosdb.domain.Person; +import com.microsoft.azure.spring.data.cosmosdb.domain.Student; +import org.junit.Test; + +import org.springframework.data.annotation.Version; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CosmosEntityInformationUnitTest { + private static final String ID = "entity_info_test_id"; + private static final String FIRST_NAME = "first name"; + private static final String LAST_NAME = "last name"; + private static final List HOBBIES = TestConstants.HOBBIES; + private static final List
ADDRESSES = TestConstants.ADDRESSES; + + @Test + public void testGetId() { + final Person testPerson = new Person(ID, FIRST_NAME, LAST_NAME, HOBBIES, ADDRESSES); + final CosmosEntityInformation entityInformation = + new CosmosEntityInformation(Person.class); + + final String idField = entityInformation.getId(testPerson); + + assertThat(idField).isEqualTo(testPerson.getId()); + } + + @Test + public void testGetIdType() { + final CosmosEntityInformation entityInformation = + new CosmosEntityInformation(Person.class); + + final Class idType = entityInformation.getIdType(); + assertThat(idType.getSimpleName()).isEqualTo(String.class.getSimpleName()); + } + + @Test + public void testGetContainerName() { + final CosmosEntityInformation entityInformation = + new CosmosEntityInformation(Person.class); + + final String containerName = entityInformation.getContainerName(); + assertThat(containerName).isEqualTo(Person.class.getSimpleName()); + } + + @Test + public void testCustomContainerName() { + final CosmosEntityInformation entityInformation = + new CosmosEntityInformation(VersionedVolunteer.class); + + final String containerName = entityInformation.getContainerName(); + assertThat(containerName).isEqualTo("testCollection"); + } + + @Test + public void testGetPartitionKeyName() { + final CosmosEntityInformation entityInformation = + new CosmosEntityInformation<>(VolunteerWithPartitionKey.class); + + final String partitionKeyName = entityInformation.getPartitionKeyFieldName(); + assertThat(partitionKeyName).isEqualTo("name"); + } + + @Test + public void testNullPartitionKeyName() { + final CosmosEntityInformation entityInformation = + new CosmosEntityInformation<>(Volunteer.class); + + final String partitionKeyName = entityInformation.getPartitionKeyFieldName(); + assertThat(partitionKeyName).isEqualTo(null); + } + + @Test + public void testCustomPartitionKeyName() { + final CosmosEntityInformation entityInformation = + new CosmosEntityInformation<>(VolunteerWithCustomPartitionKey.class); + + final String partitionKeyName = entityInformation.getPartitionKeyFieldName(); + assertThat(partitionKeyName).isEqualTo("vol_name"); + } + + @Test + public void testVersionedEntity() { + final CosmosEntityInformation entityInformation = + new CosmosEntityInformation(VersionedVolunteer.class); + + final boolean isVersioned = entityInformation.isVersioned(); + assertThat(isVersioned).isTrue(); + } + + @Test + public void testEntityShouldNotBeVersionedWithWrongType() { + final CosmosEntityInformation entityInformation = + new CosmosEntityInformation(WrongVersionType.class); + + final boolean isVersioned = entityInformation.isVersioned(); + assertThat(isVersioned).isFalse(); + } + + @Test + public void testEntityShouldNotBeVersionedWithoutAnnotationOnEtag() { + final CosmosEntityInformation entityInformation = + new CosmosEntityInformation(VersionOnWrongField.class); + + final boolean isVersioned = entityInformation.isVersioned(); + assertThat(isVersioned).isFalse(); + } + + @Test + public void testNonVersionedEntity() { + final CosmosEntityInformation entityInformation = + new CosmosEntityInformation(Student.class); + + final boolean isVersioned = entityInformation.isVersioned(); + assertThat(isVersioned).isFalse(); + } + + @Document(collection = "testCollection") + private static class Volunteer { + String id; + String name; + } + + @Document + private static class VolunteerWithCustomPartitionKey { + private String id; + @PartitionKey("vol_name") + private String name; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @Document + private static class VolunteerWithPartitionKey { + private String id; + @PartitionKey + private String name; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @Document(collection = "testCollection") + private static class VersionedVolunteer { + private String id; + private String name; + @Version + private String _etag; + + VersionedVolunteer() { + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String get_etag() { + return _etag; + } + + public void set_etag(String _etag) { + this._etag = _etag; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + VersionedVolunteer that = (VersionedVolunteer) o; + return Objects.equals(id, that.id) + && Objects.equals(name, that.name) + && Objects.equals(_etag, that._etag); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, _etag); + } + + @Override + public String toString() { + return "VersionedVolunteer{" + + "id='" + + id + + '\'' + + ", name='" + + name + + '\'' + + ", _etag='" + + _etag + + '\'' + + '}'; + } + } + + @Document + private static class WrongVersionType { + private String id; + private String name; + private long _etag; + + WrongVersionType() { + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public long get_etag() { + return _etag; + } + + public void set_etag(long _etag) { + this._etag = _etag; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + WrongVersionType that = (WrongVersionType) o; + return _etag == that._etag + && Objects.equals(id, that.id) + && Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, _etag); + } + + @Override + public String toString() { + return "WrongVersionType{" + + "id='" + + id + + '\'' + + ", name='" + + name + + '\'' + + ", _etag=" + + _etag + + '}'; + } + } + + @Document + private static class VersionOnWrongField { + private String id; + @Version + private String name; + private String _etag; + + VersionOnWrongField() { + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String get_etag() { + return _etag; + } + + public void set_etag(String _etag) { + this._etag = _etag; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + VersionOnWrongField that = (VersionOnWrongField) o; + return Objects.equals(id, that.id) + && Objects.equals(name, that.name) + && Objects.equals(_etag, that._etag); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, _etag); + } + + @Override + public String toString() { + return "VersionOnWrongField{" + + "id='" + + id + + '\'' + + ", name='" + + name + + '\'' + + ", _etag='" + + _etag + + '\'' + + '}'; + } + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/CosmosRepositoryFactoryBeanUnitTest.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/CosmosRepositoryFactoryBeanUnitTest.java new file mode 100644 index 000000000000..e6bbdcaf5787 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/CosmosRepositoryFactoryBeanUnitTest.java @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.support; + +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosTemplate; +import com.microsoft.azure.spring.data.cosmosdb.repository.repository.PersonRepository; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("unchecked") +@RunWith(MockitoJUnitRunner.class) +public class CosmosRepositoryFactoryBeanUnitTest { + @Mock + CosmosTemplate dbTemplate; + + @Test + public void testCreateRepositoryFactory() { + final CosmosRepositoryFactoryBean factoryBean = + new CosmosRepositoryFactoryBean<>(PersonRepository.class); + final RepositoryFactorySupport factory = factoryBean.createRepositoryFactory(); + assertThat(factory).isNotNull(); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/CosmosRepositoryFactoryUnitTest.java b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/CosmosRepositoryFactoryUnitTest.java new file mode 100644 index 000000000000..b9252c4ca758 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/java/com.microsoft.azure.spring.data.cosmosdb/repository/support/CosmosRepositoryFactoryUnitTest.java @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.data.cosmosdb.repository.support; + +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosTemplate; +import com.microsoft.azure.spring.data.cosmosdb.domain.Person; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.data.repository.core.EntityInformation; + +import static org.junit.Assert.assertTrue; + +@RunWith(MockitoJUnitRunner.class) +public class CosmosRepositoryFactoryUnitTest { + + @Mock + CosmosTemplate dbTemplate; + + @Autowired + ApplicationContext applicationContext; + + @Test + public void useMappingCosmosDBEntityInfoIfMappingContextSet() { + final CosmosRepositoryFactory factory = new CosmosRepositoryFactory(dbTemplate, applicationContext); + final EntityInformation entityInfo = factory.getEntityInformation(Person.class); + assertTrue(entityInfo instanceof CosmosEntityInformation); + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/resources/application.properties b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/resources/application.properties new file mode 100644 index 000000000000..85ff3ddb4a40 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/resources/application.properties @@ -0,0 +1,15 @@ +cosmosdb.uri=${DOCUMENTDB_URI} +cosmosdb.key=${DOCUMENTDB_KEY} +cosmosdb.secondaryKey=${COSMOSDB_SECONDARY_KEY} + +#You can also use connection string instead of uri and key to connect to cosmos DB +#cosmosdb.connection-string=${DOCUMENTDB_CONNECTION_STRING} + +dynamic.collection.name=spel-property-collection +# Performance test configurations +perf.recursive.times=10 +perf.batch.size=3 +perf.acceptance.percentage=10 + +# Populate query metrics +cosmosdb.populateQueryMetrics=true diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/resources/application.yml b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/resources/application.yml new file mode 100644 index 000000000000..0c17f1c5ef0b --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/resources/application.yml @@ -0,0 +1,4 @@ +cosmosdb: + key: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw== + uri: ${DOCUMENTDB_URI} + telemetryAllowed: false diff --git a/sdk/cosmos/azure-spring-data-cosmosdb/src/test/resources/logback-test.xml b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/resources/logback-test.xml new file mode 100644 index 000000000000..45f2eda3202c --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmosdb/src/test/resources/logback-test.xml @@ -0,0 +1,15 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + \ No newline at end of file diff --git a/sdk/cosmos/ci.yml b/sdk/cosmos/ci.yml index 956cef15a89f..5fda6a2dcdf8 100644 --- a/sdk/cosmos/ci.yml +++ b/sdk/cosmos/ci.yml @@ -43,6 +43,9 @@ extends: - name: azure-cosmos groupId: com.azure safeName: azurecosmos + - name: spring-data-cosmosdb + groupId: com.microsoft.azure + safeName: springdatacosmosdb AdditionalModules: - name: azure-cosmos-benchmark - groupId: com.azure \ No newline at end of file + groupId: com.azure diff --git a/sdk/cosmos/pom.xml b/sdk/cosmos/pom.xml index 3cb34026e472..3ef7f2e8daa9 100644 --- a/sdk/cosmos/pom.xml +++ b/sdk/cosmos/pom.xml @@ -11,6 +11,7 @@ azure-cosmos azure-cosmos-benchmark + azure-spring-data-cosmosdb diff --git a/sdk/spring/azure-spring-boot-starter-cosmosdb/pom.xml b/sdk/spring/azure-spring-boot-starter-cosmosdb/pom.xml index 009481b70cc7..c4c36f84f876 100644 --- a/sdk/spring/azure-spring-boot-starter-cosmosdb/pom.xml +++ b/sdk/spring/azure-spring-boot-starter-cosmosdb/pom.xml @@ -38,7 +38,7 @@ com.microsoft.azure spring-data-cosmosdb - 2.2.3.FIX1 + 2.3.0 @@ -53,7 +53,7 @@ com.microsoft.azure:azure-spring-boot:[2.3.3-beta.1] - com.microsoft.azure:spring-data-cosmosdb:[2.2.3.FIX1] + com.microsoft.azure:spring-data-cosmosdb:[2.3.0] org.springframework.boot:spring-boot-starter:[2.3.0.RELEASE] org.springframework.boot:spring-boot-starter-validation:[2.3.0.RELEASE] diff --git a/sdk/spring/azure-spring-boot-starter-keyvault-secrets/pom.xml b/sdk/spring/azure-spring-boot-starter-keyvault-secrets/pom.xml index 53c97383bd0d..096a0b2cc56f 100644 --- a/sdk/spring/azure-spring-boot-starter-keyvault-secrets/pom.xml +++ b/sdk/spring/azure-spring-boot-starter-keyvault-secrets/pom.xml @@ -179,7 +179,7 @@ - + diff --git a/sdk/spring/azure-spring-boot-tests/azure-spring-boot-test-aad/src/test/java/com/microsoft/azure/test/aad/approle/AADAppRoleStatelessAuthenticationFilterIT.java b/sdk/spring/azure-spring-boot-tests/azure-spring-boot-test-aad/src/test/java/com/microsoft/azure/test/aad/approle/AADAppRoleStatelessAuthenticationFilterIT.java index 8b4002486a27..504e9eac4633 100644 --- a/sdk/spring/azure-spring-boot-tests/azure-spring-boot-test-aad/src/test/java/com/microsoft/azure/test/aad/approle/AADAppRoleStatelessAuthenticationFilterIT.java +++ b/sdk/spring/azure-spring-boot-tests/azure-spring-boot-test-aad/src/test/java/com/microsoft/azure/test/aad/approle/AADAppRoleStatelessAuthenticationFilterIT.java @@ -45,7 +45,7 @@ public class AADAppRoleStatelessAuthenticationFilterIT { @Test public void testAADAppRoleStatelessAuthenticationFilter() { final OAuthResponse authResponse = OAuthUtils.executeOAuth2ROPCFlow(System.getenv(AAD_CLIENT_ID), - System.getenv(AAD_CLIENT_SECRET)); + System.getenv(AAD_CLIENT_SECRET)); assertNotNull(authResponse); try (AppRunner app = new AppRunner(DumbApp.class)) { @@ -56,13 +56,13 @@ public void testAADAppRoleStatelessAuthenticationFilter() { app.start(); final ResponseEntity response = restTemplate.exchange(app.root() + "public", - HttpMethod.GET, new HttpEntity<>(new HttpHeaders()), String.class, new HashMap<>()); + HttpMethod.GET, new HttpEntity<>(new HttpHeaders()), String.class, new HashMap<>()); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals("public endpoint response", response.getBody()); try { restTemplate.exchange(app.root() + "authorized", - HttpMethod.GET, new HttpEntity<>(new HttpHeaders()), String.class, new HashMap<>()); + HttpMethod.GET, new HttpEntity<>(new HttpHeaders()), String.class, new HashMap<>()); } catch (Exception e) { assertEquals(HttpClientErrorException.Forbidden.class, e.getClass()); } @@ -72,7 +72,7 @@ public void testAADAppRoleStatelessAuthenticationFilter() { final HttpEntity entity = new HttpEntity<>(headers); final ResponseEntity response2 = restTemplate.exchange(app.root() + "authorized", - HttpMethod.GET, entity, String.class, new HashMap<>()); + HttpMethod.GET, entity, String.class, new HashMap<>()); assertEquals(HttpStatus.OK, response2.getStatusCode()); assertEquals("authorized endpoint response", response2.getBody()); @@ -102,9 +102,9 @@ protected void configure(HttpSecurity http) throws Exception { http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER); http.authorizeRequests() - .antMatchers("/admin/**").hasRole("Admin") - .antMatchers("/", "/index.html", "/public").permitAll() - .anyRequest().authenticated(); + .antMatchers("/admin/**").hasRole("Admin") + .antMatchers("/", "/index.html", "/public").permitAll() + .anyRequest().authenticated(); http.addFilterBefore(aadAuthFilter, UsernamePasswordAuthenticationFilter.class); } diff --git a/sdk/spring/azure-spring-boot/pom.xml b/sdk/spring/azure-spring-boot/pom.xml index f209c269eeb2..e403ac72f0fc 100644 --- a/sdk/spring/azure-spring-boot/pom.xml +++ b/sdk/spring/azure-spring-boot/pom.xml @@ -116,7 +116,7 @@ com.microsoft.azure spring-data-cosmosdb - 2.2.3.FIX1 + 2.3.0 true @@ -268,7 +268,7 @@ net.minidev:json-smart:[2.3] com.microsoft.azure:azure-servicebus-jms:[0.0.2] com.microsoft.azure:msal4j:[1.3.0] - com.microsoft.azure:spring-data-cosmosdb:[2.2.3.FIX1] + com.microsoft.azure:spring-data-cosmosdb:[2.3.0] com.microsoft.spring.data.gremlin:spring-data-gremlin:[2.2.3] com.nimbusds:nimbus-jose-jwt:[7.9] io.micrometer:micrometer-core:[1.3.0] diff --git a/sdk/spring/ci.yml b/sdk/spring/ci.yml index 760553812278..7ccde32252d2 100644 --- a/sdk/spring/ci.yml +++ b/sdk/spring/ci.yml @@ -83,4 +83,4 @@ extends: - name: azure-spring-boot-sample-servicebus-jms-topic groupId: com.microsoft.azure - name: azure-spring-boot-sample-storage-blob - groupId: com.microsoft.azure \ No newline at end of file + groupId: com.microsoft.azure